feat(03-06): add ParticleEdgeEmitter + tests (Task 2)

This commit is contained in:
2026-04-23 15:42:26 +02:00
parent 7bbd65dad2
commit ee1d1b9bdb
2 changed files with 223 additions and 0 deletions
@@ -0,0 +1,100 @@
package com.mythlane.gravityflip.viz;
import com.hypixel.hytale.math.shape.Box;
import com.hypixel.hytale.math.vector.Vector3d;
import org.junit.jupiter.api.Test;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for {@link ParticleEdgeEmitter}. Verifies the 12-edge AABB emission
* contract — no diagonals, no interior points, corner-dedup, density clamping.
*/
class ParticleEdgeEmitterTest {
private static final double EPS = 1e-9;
@Test
void unitBox_density1_returnsExactly8Corners() {
// 1x1x1 box, density=1 → each edge of length 1 → ceil(1*1)=1 → max(2,1)=2
// points per edge (endpoints only), dedup → 8 corners total.
Box b = new Box(new Vector3d(0, 0, 0), new Vector3d(1, 1, 1));
List<Vector3d> pts = ParticleEdgeEmitter.edgePoints(b, 1.0);
assertEquals(8, pts.size(), "unit box at density=1 should emit exactly 8 corner points");
// All 8 canonical corners present.
Set<String> expected = new HashSet<>();
for (double x : new double[]{0, 1})
for (double y : new double[]{0, 1})
for (double z : new double[]{0, 1})
expected.add(key(x, y, z));
Set<String> actual = new HashSet<>();
for (Vector3d p : pts) actual.add(key(p.x, p.y, p.z));
assertEquals(expected, actual);
}
@Test
void largeBox_density1_allPointsOnBoxSurfaceAndOnEdges() {
// 10x10x10 box, density=1 → 11 points per edge (incl. endpoints).
Box b = new Box(new Vector3d(0, 0, 0), new Vector3d(10, 10, 10));
List<Vector3d> pts = ParticleEdgeEmitter.edgePoints(b, 1.0);
// Edge membership: each point must lie on ≥ 2 of the 6 box planes
// (i.e. at least 2 of its coords are on {min, max} of their axis).
for (Vector3d p : pts) {
int onPlane = 0;
if (approx(p.x, 0) || approx(p.x, 10)) onPlane++;
if (approx(p.y, 0) || approx(p.y, 10)) onPlane++;
if (approx(p.z, 0) || approx(p.z, 10)) onPlane++;
assertTrue(onPlane >= 2,
"point " + p + " must be on ≥ 2 box planes (edge membership), was on " + onPlane);
}
// Sanity: no duplicate points (corners must be deduped).
Set<String> keys = new HashSet<>();
for (Vector3d p : pts) {
assertTrue(keys.add(key(p.x, p.y, p.z)),
"duplicate point " + p + " — corners should be dedup'd");
}
// Expected count: ceil(10*1) = 10 points/edge (incl. endpoints) → 8 interior/edge.
// Total = 8 corners + 12 edges * 8 interior = 8 + 96 = 104.
assertEquals(104, pts.size());
}
@Test
void density_zeroClampedToMin_density1000ClampedToMax() {
Box b = new Box(new Vector3d(0, 0, 0), new Vector3d(1, 1, 1));
// density=0 → clamp to 0.1 → per-edge ceil(1*0.1)=1 → max(2,1)=2 endpoints only.
List<Vector3d> lo = ParticleEdgeEmitter.edgePoints(b, 0.0);
assertEquals(8, lo.size(), "density=0 should clamp to MIN_DENSITY, yielding 8 corners on unit box");
// density=1000 → clamp to 10 → per-edge ceil(1*10)=10 → 10 total points/edge.
// 8 corners + 12 * 8 interior = 8 + 96 = 104.
List<Vector3d> hi = ParticleEdgeEmitter.edgePoints(b, 1000.0);
assertEquals(104, hi.size(), "density=1000 should clamp to MAX_DENSITY=10");
}
@Test
void density_negativeAlsoClamped() {
Box b = new Box(new Vector3d(0, 0, 0), new Vector3d(1, 1, 1));
List<Vector3d> pts = ParticleEdgeEmitter.edgePoints(b, -5.0);
assertEquals(8, pts.size());
}
// ---------- helpers ----------
private static boolean approx(double a, double b) {
return Math.abs(a - b) < EPS;
}
private static String key(double x, double y, double z) {
// Round to 6 decimals to avoid floating-point noise in the dedup check.
return String.format("%.6f,%.6f,%.6f", x, y, z);
}
}