feat(04-01): add Vec3.lerp and ParticleTrail sampler with JUnit suite
- 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
This commit is contained in:
@@ -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.
|
||||
* <p>
|
||||
* 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).
|
||||
* <p>
|
||||
* Density is expressed in particles per block. The sample count for a given
|
||||
* segment is {@code ceil(distance * density)}.
|
||||
* <p>
|
||||
* 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<Vec3> 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<Vec3> 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Vec3> 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<Vec3> 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<Vec3> 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<Vec3> 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<Vec3> 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<Vec3> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user