From 8d868a28caa918dedb6a3ce6e629d526654567c6 Mon Sep 17 00:00:00 2001 From: kayjaydee Date: Mon, 27 Apr 2026 13:00:47 +0200 Subject: [PATCH] feat(04-01): add Vec3.lerp and ParticleTrail sampler with JUnit suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Vec3.lerp(other, t) for linear interpolation between two points - ParticleTrail.sample(from, to, density) emits density-based interpolated points strictly between endpoints (endpoints excluded by construction) - TRAIL_DENSITY = 4.0 particles per block - 6 ParticleTrail tests + 3 Vec3.lerp tests, all green - Zero Hytale imports — chain/ frontier preserved --- .../chainlightning/chain/ParticleTrail.java | 56 +++++++++++++ .../mythlane/chainlightning/chain/Vec3.java | 9 ++ .../chain/ParticleTrailTest.java | 82 +++++++++++++++++++ .../chainlightning/chain/Vec3Test.java | 30 +++++++ 4 files changed, 177 insertions(+) create mode 100644 src/main/java/com/mythlane/chainlightning/chain/ParticleTrail.java create mode 100644 src/test/java/com/mythlane/chainlightning/chain/ParticleTrailTest.java diff --git a/src/main/java/com/mythlane/chainlightning/chain/ParticleTrail.java b/src/main/java/com/mythlane/chainlightning/chain/ParticleTrail.java new file mode 100644 index 0000000..99944a0 --- /dev/null +++ b/src/main/java/com/mythlane/chainlightning/chain/ParticleTrail.java @@ -0,0 +1,56 @@ +package com.mythlane.chainlightning.chain; + +import java.util.ArrayList; +import java.util.List; + +/** + * Pure-Java sampler for the electric trail between two endpoints. + *

+ * Produces evenly-spaced interpolated points strictly between {@code from} and + * {@code to}; both endpoints are excluded by construction (the parametric + * interpolation parameter {@code t} never reaches 0 or 1). + *

+ * Density is expressed in particles per block. The sample count for a given + * segment is {@code ceil(distance * density)}. + *

+ * This class has ZERO Hytale imports — it lives behind the Phase 2 sealed + * frontier so it can be unit-tested without runtime dependencies. + */ +public final class ParticleTrail { + + /** Default trail density in particles per block. */ + public static final double TRAIL_DENSITY = 4.0; + + private ParticleTrail() { + // utility class — no instances + } + + /** + * Sample interpolated points strictly between {@code from} and {@code to}. + * Endpoints are NOT included in the returned list. The list is freshly + * allocated on each call. + * + * @param from start endpoint (excluded from output) + * @param to end endpoint (excluded from output) + * @param density particles per block + * @return newly-allocated list of interpolated points; empty if the + * endpoints coincide or {@code density} produces a non-positive + * count. + */ + public static List sample(Vec3 from, Vec3 to, double density) { + double dist = from.distance(to); + if (dist <= 0) { + return List.of(); + } + int count = (int) Math.ceil(dist * density); + if (count <= 0) { + return List.of(); + } + List out = new ArrayList<>(count); + for (int i = 1; i <= count; i++) { + double t = (double) i / (count + 1); + out.add(from.lerp(to, t)); + } + return out; + } +} diff --git a/src/main/java/com/mythlane/chainlightning/chain/Vec3.java b/src/main/java/com/mythlane/chainlightning/chain/Vec3.java index 1ebcacd..4b91bcc 100644 --- a/src/main/java/com/mythlane/chainlightning/chain/Vec3.java +++ b/src/main/java/com/mythlane/chainlightning/chain/Vec3.java @@ -22,4 +22,13 @@ public record Vec3(double x, double y, double z) { public double distance(Vec3 other) { return Math.sqrt(distanceSquared(other)); } + + /** Linear interpolation: t=0 returns this, t=1 returns other. */ + public Vec3 lerp(Vec3 other, double t) { + return new Vec3( + this.x + (other.x - this.x) * t, + this.y + (other.y - this.y) * t, + this.z + (other.z - this.z) * t + ); + } } diff --git a/src/test/java/com/mythlane/chainlightning/chain/ParticleTrailTest.java b/src/test/java/com/mythlane/chainlightning/chain/ParticleTrailTest.java new file mode 100644 index 0000000..852bc93 --- /dev/null +++ b/src/test/java/com/mythlane/chainlightning/chain/ParticleTrailTest.java @@ -0,0 +1,82 @@ +package com.mythlane.chainlightning.chain; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class ParticleTrailTest { + + private static final double EPS = 1e-9; + + @Test + void zeroDistance() { + Vec3 p = new Vec3(2, 3, 4); + List out = ParticleTrail.sample(p, p, 4.0); + assertTrue(out.isEmpty()); + } + + @Test + void subBlockDistance() { + // distance 0.5 along X with density 4 → ceil(2.0) = 2 + Vec3 from = new Vec3(0, 0, 0); + Vec3 to = new Vec3(0.5, 0, 0); + List out = ParticleTrail.sample(from, to, 4.0); + assertEquals(2, out.size()); + // Both points strictly between endpoints + for (Vec3 v : out) { + assertTrue(from.distanceSquared(v) > EPS, "sample equals from"); + assertTrue(to.distanceSquared(v) > EPS, "sample equals to"); + } + } + + @Test + void integerDistanceCount() { + Vec3 from = new Vec3(0, 0, 0); + Vec3 to = new Vec3(5, 0, 0); + List out = ParticleTrail.sample(from, to, 4.0); + assertEquals(20, out.size()); + } + + @Test + void fractionalDistanceCount() { + Vec3 from = new Vec3(0, 0, 0); + Vec3 to = new Vec3(3.7, 0, 0); + List out = ParticleTrail.sample(from, to, 4.0); + // ceil(3.7 * 4) = ceil(14.8) = 15 + assertEquals(15, out.size()); + } + + @Test + void endpointsExcluded() { + Vec3 from = new Vec3(1, 2, 3); + Vec3 to = new Vec3(7, 8, 9); + List out = ParticleTrail.sample(from, to, 4.0); + assertTrue(out.size() > 0); + for (Vec3 v : out) { + assertTrue(from.distanceSquared(v) > EPS, "sample equals from endpoint"); + assertTrue(to.distanceSquared(v) > EPS, "sample equals to endpoint"); + } + } + + @Test + void uniformSpacing() { + Vec3 from = new Vec3(0, 0, 0); + Vec3 to = new Vec3(5, 0, 0); + List out = ParticleTrail.sample(from, to, 4.0); + assertEquals(20, out.size()); + double total = from.distance(to); + double expectedSpacing = total / (out.size() + 1); + + // from -> first + assertEquals(expectedSpacing, from.distance(out.get(0)), EPS); + // last -> to + assertEquals(expectedSpacing, out.get(out.size() - 1).distance(to), EPS); + // consecutive + for (int i = 1; i < out.size(); i++) { + assertEquals(expectedSpacing, out.get(i - 1).distance(out.get(i)), EPS); + } + } +} diff --git a/src/test/java/com/mythlane/chainlightning/chain/Vec3Test.java b/src/test/java/com/mythlane/chainlightning/chain/Vec3Test.java index 85542f1..b3f18d1 100644 --- a/src/test/java/com/mythlane/chainlightning/chain/Vec3Test.java +++ b/src/test/java/com/mythlane/chainlightning/chain/Vec3Test.java @@ -41,4 +41,34 @@ final class Vec3Test { assertEquals(12.0, a.distanceSquared(b), 1e-9); assertTrue(a.distance(b) > 3.4 && a.distance(b) < 3.5); } + + @Test + void lerpAtZeroReturnsFrom() { + Vec3 from = Vec3.ZERO; + Vec3 to = new Vec3(10, 10, 10); + Vec3 result = from.lerp(to, 0.0); + assertEquals(from.x(), result.x(), 1e-9); + assertEquals(from.y(), result.y(), 1e-9); + assertEquals(from.z(), result.z(), 1e-9); + } + + @Test + void lerpAtOneReturnsTo() { + Vec3 from = Vec3.ZERO; + Vec3 to = new Vec3(10, 10, 10); + Vec3 result = from.lerp(to, 1.0); + assertEquals(to.x(), result.x(), 1e-9); + assertEquals(to.y(), result.y(), 1e-9); + assertEquals(to.z(), result.z(), 1e-9); + } + + @Test + void lerpAtHalfReturnsMidpoint() { + Vec3 from = new Vec3(0, 0, 0); + Vec3 to = new Vec3(10, 0, 0); + Vec3 result = from.lerp(to, 0.5); + assertEquals(5.0, result.x(), 1e-9); + assertEquals(0.0, result.y(), 1e-9); + assertEquals(0.0, result.z(), 1e-9); + } }