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,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);
}
}