feat(03-06): add ParticleEdgeEmitter + tests (Task 2)
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
package com.mythlane.gravityflip.viz;
|
||||
|
||||
import com.hypixel.hytale.math.shape.Box;
|
||||
import com.hypixel.hytale.math.vector.Vector3d;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Plan 03-06 Task 2 — helper pur qui génère la liste des points d'émission de
|
||||
* particules le long des 12 arêtes d'une {@link Box} AABB, sans aucune
|
||||
* dépendance sur {@code World} (testable en JVM standard).
|
||||
*
|
||||
* <p><b>Contract:</b> {@link #edgePoints(Box, double)} retourne une liste de
|
||||
* {@link Vector3d} répartis uniformément sur les 12 arêtes de la box, avec les
|
||||
* 8 coins inclus exactement une fois (déduplication inter-arêtes). Les 4 arêtes
|
||||
* verticales, les 4 arêtes du plancher (Y=minY) et les 4 arêtes du plafond
|
||||
* (Y=maxY) sont couvertes — aucune diagonale, aucun point intérieur.
|
||||
*
|
||||
* <p><b>Density:</b> particules par mètre. Pour chaque arête de longueur {@code L}
|
||||
* on émet {@code max(2, ceil(L * density))} points uniformément espacés,
|
||||
* endpoints inclus. {@code density} est clampé à {@code [0.1, 10.0]} pour borner
|
||||
* la charge réseau (threat : valeur pathologique type 10 000 → DoS clients).
|
||||
*
|
||||
* <p><b>Dédup des coins :</b> les 12 arêtes partagent leurs endpoints ; on
|
||||
* émet chaque coin exactement 1 fois en excluant l'endpoint "fin" de chaque
|
||||
* arête et en n'émettant les 8 coins qu'une fois via un parcours explicite.
|
||||
*/
|
||||
public final class ParticleEdgeEmitter {
|
||||
|
||||
/** Plancher de densité (particules/m) — évite density ≤ 0 qui produirait l'ensemble vide. */
|
||||
public static final double MIN_DENSITY = 0.1;
|
||||
/** Plafond de densité (particules/m) — borne la charge réseau par box. */
|
||||
public static final double MAX_DENSITY = 10.0;
|
||||
|
||||
private ParticleEdgeEmitter() {}
|
||||
|
||||
/**
|
||||
* @param box la box dont on veut matérialiser les 12 arêtes.
|
||||
* @param density particules/mètre (clampé à {@code [0.1, 10.0]}).
|
||||
* @return liste de points uniformément répartis sur les 12 arêtes, 8 coins
|
||||
* dédupliqués. Jamais null. Taille ≥ 8.
|
||||
*/
|
||||
public static List<Vector3d> edgePoints(Box box, double density) {
|
||||
double d = clamp(density, MIN_DENSITY, MAX_DENSITY);
|
||||
|
||||
double x0 = box.min.x, y0 = box.min.y, z0 = box.min.z;
|
||||
double x1 = box.max.x, y1 = box.max.y, z1 = box.max.z;
|
||||
|
||||
List<Vector3d> out = new ArrayList<>();
|
||||
|
||||
// 1) Émettre explicitement les 8 coins (dédupliqués par construction).
|
||||
double[][] corners = new double[][] {
|
||||
{x0, y0, z0}, {x1, y0, z0}, {x0, y0, z1}, {x1, y0, z1},
|
||||
{x0, y1, z0}, {x1, y1, z0}, {x0, y1, z1}, {x1, y1, z1},
|
||||
};
|
||||
for (double[] c : corners) {
|
||||
out.add(new Vector3d(c[0], c[1], c[2]));
|
||||
}
|
||||
|
||||
// 2) Pour chaque arête, émettre les points INTÉRIEURS (sans endpoints).
|
||||
// 4 arêtes bas (Y=y0) : varient sur X ou Z.
|
||||
addInteriorLineX(out, x0, x1, y0, z0, d);
|
||||
addInteriorLineX(out, x0, x1, y0, z1, d);
|
||||
addInteriorLineZ(out, x0, y0, z0, z1, d);
|
||||
addInteriorLineZ(out, x1, y0, z0, z1, d);
|
||||
// 4 arêtes haut (Y=y1).
|
||||
addInteriorLineX(out, x0, x1, y1, z0, d);
|
||||
addInteriorLineX(out, x0, x1, y1, z1, d);
|
||||
addInteriorLineZ(out, x0, y1, z0, z1, d);
|
||||
addInteriorLineZ(out, x1, y1, z0, z1, d);
|
||||
// 4 arêtes verticales (varient sur Y).
|
||||
addInteriorLineY(out, x0, y0, y1, z0, d);
|
||||
addInteriorLineY(out, x1, y0, y1, z0, d);
|
||||
addInteriorLineY(out, x0, y0, y1, z1, d);
|
||||
addInteriorLineY(out, x1, y0, y1, z1, d);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Points intérieurs (sans endpoints) d'une arête parallèle à X. */
|
||||
private static void addInteriorLineX(List<Vector3d> out,
|
||||
double xMin, double xMax,
|
||||
double y, double z, double density) {
|
||||
int n = pointCount(xMax - xMin, density);
|
||||
// n = total points incl. endpoints ; intérieurs = n-2.
|
||||
for (int i = 1; i < n - 1; i++) {
|
||||
double t = (double) i / (double) (n - 1);
|
||||
out.add(new Vector3d(xMin + t * (xMax - xMin), y, z));
|
||||
}
|
||||
}
|
||||
|
||||
private static void addInteriorLineY(List<Vector3d> out,
|
||||
double x, double yMin, double yMax,
|
||||
double z, double density) {
|
||||
int n = pointCount(yMax - yMin, density);
|
||||
for (int i = 1; i < n - 1; i++) {
|
||||
double t = (double) i / (double) (n - 1);
|
||||
out.add(new Vector3d(x, yMin + t * (yMax - yMin), z));
|
||||
}
|
||||
}
|
||||
|
||||
private static void addInteriorLineZ(List<Vector3d> out,
|
||||
double x, double y,
|
||||
double zMin, double zMax, double density) {
|
||||
int n = pointCount(zMax - zMin, density);
|
||||
for (int i = 1; i < n - 1; i++) {
|
||||
double t = (double) i / (double) (n - 1);
|
||||
out.add(new Vector3d(x, y, zMin + t * (zMax - zMin)));
|
||||
}
|
||||
}
|
||||
|
||||
/** {@code max(2, ceil(length * density))} — 2 endpoints garantis. */
|
||||
static int pointCount(double length, double density) {
|
||||
double l = Math.max(0.0, length);
|
||||
int n = (int) Math.ceil(l * density);
|
||||
return Math.max(2, n);
|
||||
}
|
||||
|
||||
static double clamp(double v, double lo, double hi) {
|
||||
return v < lo ? lo : (v > hi ? hi : v);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user