@woosh/meep-engine 2.153.0 → 2.155.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts +112 -0
  3. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -0
  4. package/src/core/geom/3d/shape/ConvexHullShape3D.js +325 -0
  5. package/src/core/geom/vec3/v3_array_copy.d.ts +3 -3
  6. package/src/core/geom/vec3/v3_array_copy.d.ts.map +1 -1
  7. package/src/core/geom/vec3/v3_array_copy.js +2 -2
  8. package/src/core/geom/vec3/v3_cross.d.ts +17 -0
  9. package/src/core/geom/vec3/v3_cross.d.ts.map +1 -0
  10. package/src/core/geom/vec3/v3_cross.js +20 -0
  11. package/src/core/geom/vec3/v3_subtract.d.ts +16 -0
  12. package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -0
  13. package/src/core/geom/vec3/v3_subtract.js +19 -0
  14. package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.d.ts.map +1 -1
  15. package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +8 -0
  16. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +4 -0
  17. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
  18. package/src/engine/graphics/ecs/trail2d/Trail2D.js +21 -0
  19. package/src/engine/physics/PLAN.md +4 -4
  20. package/src/engine/physics/body/BodyStorage.d.ts +3 -1
  21. package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
  22. package/src/engine/physics/body/BodyStorage.js +452 -450
  23. package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -1
  24. package/src/engine/physics/body/SolverBodyState.js +6 -5
  25. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  26. package/src/engine/physics/broadphase/generate_pairs.js +9 -1
  27. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
  28. package/src/engine/physics/ccd/linear_sweep.js +237 -238
  29. package/src/engine/physics/computeInterceptPoint.d.ts.map +1 -1
  30. package/src/engine/physics/computeInterceptPoint.js +8 -3
  31. package/src/engine/physics/contact/ManifoldStore.d.ts +0 -16
  32. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  33. package/src/engine/physics/contact/ManifoldStore.js +1 -38
  34. package/src/engine/physics/ecs/BodyKind.d.ts +3 -2
  35. package/src/engine/physics/ecs/BodyKind.d.ts.map +1 -1
  36. package/src/engine/physics/ecs/BodyKind.js +25 -24
  37. package/src/engine/physics/ecs/PhysicsEvents.d.ts +4 -5
  38. package/src/engine/physics/ecs/PhysicsEvents.d.ts.map +1 -1
  39. package/src/engine/physics/ecs/PhysicsEvents.js +15 -16
  40. package/src/engine/physics/ecs/PhysicsSystem.d.ts +5 -30
  41. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  42. package/src/engine/physics/ecs/PhysicsSystem.js +13 -45
  43. package/src/engine/physics/ecs/RigidBodySerializationAdapter.d.ts.map +1 -1
  44. package/src/engine/physics/ecs/RigidBodySerializationAdapter.js +85 -81
  45. package/src/engine/physics/ecs/is_sensor.d.ts +18 -0
  46. package/src/engine/physics/ecs/is_sensor.d.ts.map +1 -0
  47. package/src/engine/physics/ecs/is_sensor.js +27 -0
  48. package/src/engine/physics/events/ContactEventBuffer.d.ts +2 -1
  49. package/src/engine/physics/events/ContactEventBuffer.d.ts.map +1 -1
  50. package/src/engine/physics/events/ContactEventBuffer.js +84 -83
  51. package/src/engine/physics/gjk/gjk.d.ts +0 -26
  52. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  53. package/src/engine/physics/gjk/gjk.js +3 -52
  54. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts +20 -0
  55. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts.map +1 -0
  56. package/src/engine/physics/gjk/gjk_epa_penetration.js +548 -0
  57. package/src/engine/physics/gjk/minkowski_support.d.ts +4 -9
  58. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -1
  59. package/src/engine/physics/gjk/minkowski_support.js +70 -75
  60. package/src/engine/physics/gjk/mpr.d.ts +1 -1
  61. package/src/engine/physics/gjk/mpr.d.ts.map +1 -1
  62. package/src/engine/physics/gjk/mpr.js +362 -344
  63. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  64. package/src/engine/physics/island/IslandBuilder.js +431 -428
  65. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  66. package/src/engine/physics/narrowphase/box_box_manifold.js +4 -81
  67. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
  68. package/src/engine/physics/narrowphase/box_triangle_contact.js +4 -39
  69. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  70. package/src/engine/physics/narrowphase/capsule_contacts.js +459 -462
  71. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts.map +1 -1
  72. package/src/engine/physics/narrowphase/clip_against_axis_uv.js +4 -1
  73. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +83 -0
  74. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -0
  75. package/src/engine/physics/narrowphase/convex_convex_manifold.js +425 -0
  76. package/src/engine/physics/narrowphase/convex_decomposition.d.ts +32 -0
  77. package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -0
  78. package/src/engine/physics/narrowphase/convex_decomposition.js +293 -0
  79. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts +41 -0
  80. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts.map +1 -0
  81. package/src/engine/physics/narrowphase/mesh_convex_hull.js +106 -0
  82. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts +8 -0
  83. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts.map +1 -0
  84. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.js +117 -0
  85. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  86. package/src/engine/physics/narrowphase/narrowphase_step.js +105 -102
  87. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts +29 -0
  88. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts.map +1 -0
  89. package/src/engine/physics/narrowphase/reduce_manifold_contacts.js +69 -0
  90. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  91. package/src/engine/physics/narrowphase/refine_ray_concave.js +152 -145
  92. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  93. package/src/engine/physics/narrowphase/sphere_box_contact.js +132 -123
  94. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  95. package/src/engine/physics/queries/overlap_shape.js +16 -17
  96. package/src/engine/physics/queries/raycast.d.ts +5 -0
  97. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  98. package/src/engine/physics/queries/raycast.js +16 -8
  99. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -1
  100. package/src/engine/physics/queries/shape_cast.js +13 -7
  101. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  102. package/src/engine/physics/solver/solve_contacts.js +8 -11
  103. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
  104. package/src/engine/physics/vehicle/RaycastVehicle.js +339 -333
  105. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +0 -13
  106. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +0 -1
  107. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +0 -399
@@ -1 +1 @@
1
- {"version":3,"file":"clip_against_axis_uv.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/clip_against_axis_uv.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,gDARW,YAAY,eACZ,MAAM,cACN,YAAY,aACZ,MAAM,SACN,MAAM,cACN,OAAO,GACL,MAAM,CAoClB"}
1
+ {"version":3,"file":"clip_against_axis_uv.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/clip_against_axis_uv.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,gDARW,YAAY,eACZ,MAAM,cACN,YAAY,aACZ,MAAM,SACN,MAAM,cACN,OAAO,GACL,MAAM,CAuClB"}
@@ -35,8 +35,11 @@ export function clip_against_axis_uv(points_in, point_count, points_out, coord_i
35
35
 
36
36
  if (a_inside !== b_inside) {
37
37
 
38
+ // a_inside !== b_inside ⇒ av and bv are on opposite sides of `bound`,
39
+ // so av !== bv and the denominator is non-zero (the `:0` fallback was
40
+ // unreachable here).
38
41
  const denom = bv - av;
39
- const t = denom !== 0 ? (bound - av) / denom : 0;
42
+ const t = (bound - av) / denom;
40
43
 
41
44
  points_out[out_count * 2] = ax + (bx - ax) * t;
42
45
  points_out[out_count * 2 + 1] = ay + (by - ay) * t;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * SAT entry point — finds the minimum-translation axis itself, then clips.
3
+ * For the SMALL convex pieces of a decomposed mesh.
4
+ *
5
+ * @param {Float64Array|number[]} out length ≥ CONVEX_CONVEX_OUT_LENGTH
6
+ * @param {{vertices:Float32Array, face_offsets:Uint32Array, face_loops:Uint32Array}} A
7
+ * @param {{x,y,z}} posA @param {{x,y,z,w}} rotA
8
+ * @param {{vertices:Float32Array, face_offsets:Uint32Array, face_loops:Uint32Array}} B
9
+ * @param {{x,y,z}} posB @param {{x,y,z,w}} rotB
10
+ * @returns {boolean}
11
+ */
12
+ export function convex_convex_manifold(out: Float64Array | number[], A: {
13
+ vertices: Float32Array;
14
+ face_offsets: Uint32Array;
15
+ face_loops: Uint32Array;
16
+ }, posA: {
17
+ x;
18
+ y;
19
+ z;
20
+ }, rotA: {
21
+ x;
22
+ y;
23
+ z;
24
+ w;
25
+ }, B: {
26
+ vertices: Float32Array;
27
+ face_offsets: Uint32Array;
28
+ face_loops: Uint32Array;
29
+ }, posB: {
30
+ x;
31
+ y;
32
+ z;
33
+ }, rotB: {
34
+ x;
35
+ y;
36
+ z;
37
+ w;
38
+ }): boolean;
39
+ /**
40
+ * Clip-only entry point — the caller supplies the separating axis `n` (B→A,
41
+ * unit) from a robust GJK+EPA query ({@link gjk_epa_penetration}); we only build
42
+ * the multi-point manifold. This is Bullet's `clipHullAgainstHull(sepNormal,…)`
43
+ * with the normal sourced from GJK rather than SAT. Used for LARGE convex
44
+ * polytopes (authored hulls, a convex mesh as one piece) where SAT's O(Fa·Fb +
45
+ * Ea·Eb) edge term is prohibitive but GJK+EPA is O(support queries). Always
46
+ * emits ≥ 1 contact (the caller already established penetration): if the face
47
+ * clip empties (a vertex/edge feature) it falls back to the deepest support-pair.
48
+ *
49
+ * @param {Float64Array|number[]} out length ≥ CONVEX_CONVEX_OUT_LENGTH
50
+ * @param {{vertices,face_offsets,face_loops}} A @param {{x,y,z}} posA @param {{x,y,z,w}} rotA
51
+ * @param {{vertices,face_offsets,face_loops}} B @param {{x,y,z}} posB @param {{x,y,z,w}} rotB
52
+ * @param {number} nx @param {number} ny @param {number} nz unit axis, B→A
53
+ * @returns {boolean} always true
54
+ */
55
+ export function convex_hull_clip(out: Float64Array | number[], A: {
56
+ vertices;
57
+ face_offsets;
58
+ face_loops;
59
+ }, posA: {
60
+ x;
61
+ y;
62
+ z;
63
+ }, rotA: {
64
+ x;
65
+ y;
66
+ z;
67
+ w;
68
+ }, B: {
69
+ vertices;
70
+ face_offsets;
71
+ face_loops;
72
+ }, posB: {
73
+ x;
74
+ y;
75
+ z;
76
+ }, rotB: {
77
+ x;
78
+ y;
79
+ z;
80
+ w;
81
+ }, nx: number, ny: number, nz: number): boolean;
82
+ export const CONVEX_CONVEX_OUT_LENGTH: number;
83
+ //# sourceMappingURL=convex_convex_manifold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convex_convex_manifold.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/convex_convex_manifold.js"],"names":[],"mappings":"AAoJA;;;;;;;;;;GAUG;AACH,4CAPW,YAAY,GAAC,MAAM,EAAE,KACrB;IAAC,QAAQ,EAAC,YAAY,CAAC;IAAC,YAAY,EAAC,WAAW,CAAC;IAAC,UAAU,EAAC,WAAW,CAAA;CAAC,QACzE;IAAC,CAAC,CAAC;IAAA,CAAC,CAAC;IAAA,CAAC,CAAA;CAAC,QAAe;IAAC,CAAC,CAAC;IAAA,CAAC,CAAC;IAAA,CAAC,CAAC;IAAA,CAAC,CAAA;CAAC,KAC/B;IAAC,QAAQ,EAAC,YAAY,CAAC;IAAC,YAAY,EAAC,WAAW,CAAC;IAAC,UAAU,EAAC,WAAW,CAAA;CAAC,QACzE;IAAC,CAAC,CAAC;IAAA,CAAC,CAAC;IAAA,CAAC,CAAA;CAAC,QAAe;IAAC,CAAC,CAAC;IAAA,CAAC,CAAC;IAAA,CAAC,CAAC;IAAA,CAAC,CAAA;CAAC,GAC7B,OAAO,CAsDnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,sCANW,YAAY,GAAC,MAAM,EAAE,KACrB;IAAC,QAAQ,CAAC;IAAA,YAAY,CAAC;IAAA,UAAU,CAAA;CAAC,QAAY;IAAC,CAAC,CAAC;IAAA,CAAC,CAAC;IAAA,CAAC,CAAA;CAAC,QAAe;IAAC,CAAC,CAAC;IAAA,CAAC,CAAC;IAAA,CAAC,CAAC;IAAA,CAAC,CAAA;CAAC,KAC7E;IAAC,QAAQ,CAAC;IAAA,YAAY,CAAC;IAAA,UAAU,CAAA;CAAC,QAAY;IAAC,CAAC,CAAC;IAAA,CAAC,CAAC;IAAA,CAAC,CAAA;CAAC,QAAe;IAAC,CAAC,CAAC;IAAA,CAAC,CAAC;IAAA,CAAC,CAAC;IAAA,CAAC,CAAA;CAAC,MAC7E,MAAM,MAAa,MAAM,MAAa,MAAM,GAC1C,OAAO,CA+CnB;AA9ND,8CAA0E"}
@@ -0,0 +1,425 @@
1
+ import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
2
+ import { reduce_manifold_contacts } from "./reduce_manifold_contacts.js";
3
+
4
+ /**
5
+ * Multi-point contact manifold between two convex polytopes with **polygon
6
+ * faces**, computed entirely from the current poses — no cross-frame state, so
7
+ * it preserves reset-and-resimulate (rollback) determinism.
8
+ *
9
+ * A piece is described by:
10
+ * - `vertices` : Float32Array, flat (x,y,z) per vertex, local frame
11
+ * - `face_offsets` : Uint32Array length F+1, CSR offsets into `face_loops`
12
+ * - `face_loops` : Uint32Array, each face's vertex indices, ordered CCW so
13
+ * the face normal (from its first three loop verts) points
14
+ * outward.
15
+ * A triangle-faced piece (a tetrahedron) is just the special case where every
16
+ * face loop has length 3; a merged/box piece has quads or larger polygons.
17
+ * Handling polygon faces directly is what lets a flat face produce a full
18
+ * face-on-face patch instead of mismatched triangle slivers.
19
+ *
20
+ * The separating axis comes from SAT (the structure mirrors Bullet's
21
+ * `clipHullAgainstHull`, which clips around a `separatingNormal`). A GJK+EPA
22
+ * axis source was prototyped — Bullet sources the normal from EITHER SAT OR
23
+ * `btGjkPairDetector`, the source being orthogonal to the clip — but this
24
+ * engine's EPA returns a non-minimal, non-scaling axis for polytopes (a 0.05 m
25
+ * overlap reports ~0.8 m depth), so clipping around it produces a garbage
26
+ * manifold. SAT is exact and, for the low-poly hulls authored as collision
27
+ * proxies and the small pieces of a decomposed mesh, cheap. See memory:
28
+ * feedback_epa_unreliable_polytopes.
29
+ *
30
+ * Algorithm (general form of {@link box_box_manifold}):
31
+ * 1. SAT over every face normal of A, every face normal of B, and every
32
+ * edge(A)×edge(B) cross finds the minimum-translation axis `n` (B→A).
33
+ * 2. Face-axis winner ⇒ reference face = the polygon that owns the axis;
34
+ * incident face = the polygon on the other piece most antiparallel to the
35
+ * reference outward normal. Clip the incident polygon against the reference
36
+ * polygon's side planes (3-D Sutherland–Hodgman) and keep penetrating points.
37
+ * 3. Edge-axis winner ⇒ a single deepest support-pair contact.
38
+ * 4. Reduce survivors to ≤ {@link MAX_CONTACTS} (deepest + spread).
39
+ *
40
+ * Output layout (matches {@link box_box_manifold}):
41
+ * out[0..2] : world normal (B→A)
42
+ * out[3] : contact count (0 .. MAX_CONTACTS)
43
+ * out[4 + k*7 + 0..2] : world contact on A's surface
44
+ * out[4 + k*7 + 3..5] : world contact on B's surface
45
+ * out[4 + k*7 + 6] : penetration depth (positive)
46
+ *
47
+ * @author Alex Goldring
48
+ * @copyright Company Named Limited (c) 2026
49
+ */
50
+
51
+ const MAX_CONTACTS = 4;
52
+ const CONTACT_STRIDE = 7;
53
+ export const CONVEX_CONVEX_OUT_LENGTH = 4 + MAX_CONTACTS * CONTACT_STRIDE;
54
+
55
+ const INITIAL_V = 256; // initial world-vertex scratch capacity (verts per piece/hull)
56
+ const MAX_POLY = 64; // polygon verts during clipping
57
+
58
+ const TIE_REL = 0.02;
59
+ const TIE_ABS = 1e-5;
60
+ const PARALLEL_EPS = 1e-9;
61
+
62
+ // --- scratch ----------------------------------------------------------------
63
+ // World-vertex buffers. `let` + grow-on-demand: a convex MESH routed as a single
64
+ // hull (get_mesh_convex_hull) can have far more than the initial capacity of
65
+ // surface vertices (a subdivided icosphere is hundreds). Without the grow, the
66
+ // excess writes were silently dropped and SAT/clip read NaN past the end,
67
+ // computing a truncated (wrong) manifold against part of the hull.
68
+ let wvA = new Float64Array(INITIAL_V * 3);
69
+ let wvB = new Float64Array(INITIAL_V * 3);
70
+ const poly_in = new Float64Array(MAX_POLY * 3);
71
+ const poly_out = new Float64Array(MAX_POLY * 3);
72
+ const cand = new Float64Array(MAX_POLY * 4);
73
+ const cenA = new Float64Array(3);
74
+ const cenB = new Float64Array(3);
75
+ const tmp_n = new Float64Array(3);
76
+
77
+ const sat = { overlap: Infinity, nx: 0, ny: 0, nz: 0, kind: -1, face: -1 };
78
+
79
+ // --- per-call geometry refs (single-thread re-entrancy, like the scratch) ---
80
+ let g_foA, g_flA, g_foB, g_flB, g_fA, g_fB, g_nA, g_nB;
81
+
82
+ function set_geom(A, B) {
83
+ g_nA = A.vertices.length / 3; g_nB = B.vertices.length / 3;
84
+ g_foA = A.face_offsets; g_flA = A.face_loops; g_fA = g_foA.length - 1;
85
+ g_foB = B.face_offsets; g_flB = B.face_loops; g_fB = g_foB.length - 1;
86
+ // Grow the world-vertex scratch to fit, so large hulls aren't truncated.
87
+ if (wvA.length < g_nA * 3) wvA = new Float64Array(g_nA * 3);
88
+ if (wvB.length < g_nB * 3) wvB = new Float64Array(g_nB * 3);
89
+ }
90
+
91
+ function load_world(out, verts, n, p, q) {
92
+ for (let i = 0; i < n; i++) {
93
+ v3_quat3_apply(out, i * 3, verts[i * 3], verts[i * 3 + 1], verts[i * 3 + 2], q.x, q.y, q.z, q.w);
94
+ out[i * 3] += p.x; out[i * 3 + 1] += p.y; out[i * 3 + 2] += p.z;
95
+ }
96
+ }
97
+
98
+ function centroid(out, wv, n) {
99
+ let cx = 0, cy = 0, cz = 0;
100
+ for (let i = 0; i < n; i++) { cx += wv[i * 3]; cy += wv[i * 3 + 1]; cz += wv[i * 3 + 2]; }
101
+ out[0] = cx / n; out[1] = cy / n; out[2] = cz / n;
102
+ }
103
+
104
+ /**
105
+ * Outward normal of a polygon face (Newell's method over the whole loop, world).
106
+ * Robust against collinear consecutive vertices, unlike a first-three cross.
107
+ */
108
+ function face_normal(out, wv, loops, off, end) {
109
+ let nx = 0, ny = 0, nz = 0;
110
+ for (let i = off; i < end; i++) {
111
+ const p = loops[i] * 3, q = loops[i + 1 < end ? i + 1 : off] * 3;
112
+ nx += (wv[p + 1] - wv[q + 1]) * (wv[p + 2] + wv[q + 2]);
113
+ ny += (wv[p + 2] - wv[q + 2]) * (wv[p] + wv[q]);
114
+ nz += (wv[p] - wv[q]) * (wv[p + 1] + wv[q + 1]);
115
+ }
116
+ out[0] = nx; out[1] = ny; out[2] = nz;
117
+ }
118
+
119
+ /** SAT axis test; true if the axis SEPARATES the pieces. */
120
+ function test_axis(ux, uy, uz, nA, nB, kind, face) {
121
+ const len2 = ux * ux + uy * uy + uz * uz;
122
+ if (len2 < PARALLEL_EPS) return false;
123
+ const inv = 1 / Math.sqrt(len2);
124
+ ux *= inv; uy *= inv; uz *= inv;
125
+
126
+ let minA = Infinity, maxA = -Infinity;
127
+ for (let i = 0; i < nA; i++) {
128
+ const d = ux * wvA[i * 3] + uy * wvA[i * 3 + 1] + uz * wvA[i * 3 + 2];
129
+ if (d < minA) minA = d; if (d > maxA) maxA = d;
130
+ }
131
+ let minB = Infinity, maxB = -Infinity;
132
+ for (let i = 0; i < nB; i++) {
133
+ const d = ux * wvB[i * 3] + uy * wvB[i * 3 + 1] + uz * wvB[i * 3 + 2];
134
+ if (d < minB) minB = d; if (d > maxB) maxB = d;
135
+ }
136
+ if (maxA < minB || maxB < minA) return true;
137
+
138
+ const overlap = Math.min(maxA, maxB) - Math.max(minA, minB);
139
+ if (sat.kind === -1 || overlap < sat.overlap - (sat.overlap * TIE_REL + TIE_ABS)) {
140
+ sat.overlap = overlap;
141
+ const dcx = cenA[0] - cenB[0], dcy = cenA[1] - cenB[1], dcz = cenA[2] - cenB[2];
142
+ const sign = (ux * dcx + uy * dcy + uz * dcz) >= 0 ? 1 : -1;
143
+ sat.nx = ux * sign; sat.ny = uy * sign; sat.nz = uz * sign;
144
+ sat.kind = kind; sat.face = face;
145
+ }
146
+ return false;
147
+ }
148
+
149
+ /**
150
+ * SAT entry point — finds the minimum-translation axis itself, then clips.
151
+ * For the SMALL convex pieces of a decomposed mesh.
152
+ *
153
+ * @param {Float64Array|number[]} out length ≥ CONVEX_CONVEX_OUT_LENGTH
154
+ * @param {{vertices:Float32Array, face_offsets:Uint32Array, face_loops:Uint32Array}} A
155
+ * @param {{x,y,z}} posA @param {{x,y,z,w}} rotA
156
+ * @param {{vertices:Float32Array, face_offsets:Uint32Array, face_loops:Uint32Array}} B
157
+ * @param {{x,y,z}} posB @param {{x,y,z,w}} rotB
158
+ * @returns {boolean}
159
+ */
160
+ export function convex_convex_manifold(out, A, posA, rotA, B, posB, rotB) {
161
+ set_geom(A, B);
162
+
163
+ load_world(wvA, A.vertices, g_nA, posA, rotA);
164
+ load_world(wvB, B.vertices, g_nB, posB, rotB);
165
+ centroid(cenA, wvA, g_nA);
166
+ centroid(cenB, wvB, g_nB);
167
+
168
+ sat.kind = -1; sat.overlap = Infinity;
169
+
170
+ // A face normals.
171
+ for (let f = 0; f < g_fA; f++) {
172
+ face_normal(tmp_n, wvA, g_flA, g_foA[f], g_foA[f + 1]);
173
+ if (test_axis(tmp_n[0], tmp_n[1], tmp_n[2], g_nA, g_nB, 0, f)) return false;
174
+ }
175
+ // B face normals.
176
+ for (let f = 0; f < g_fB; f++) {
177
+ face_normal(tmp_n, wvB, g_flB, g_foB[f], g_foB[f + 1]);
178
+ if (test_axis(tmp_n[0], tmp_n[1], tmp_n[2], g_nA, g_nB, 1, f)) return false;
179
+ }
180
+ // Edge × edge crosses (face-loop edges; shared edges retest harmlessly).
181
+ for (let fa = 0; fa < g_fA; fa++) {
182
+ const sa = g_foA[fa], la = g_foA[fa + 1] - sa;
183
+ for (let i = 0; i < la; i++) {
184
+ const a0 = g_flA[sa + i] * 3, a1 = g_flA[sa + (i + 1) % la] * 3;
185
+ const eax = wvA[a1] - wvA[a0], eay = wvA[a1 + 1] - wvA[a0 + 1], eaz = wvA[a1 + 2] - wvA[a0 + 2];
186
+ for (let fb = 0; fb < g_fB; fb++) {
187
+ const sb = g_foB[fb], lb = g_foB[fb + 1] - sb;
188
+ for (let j = 0; j < lb; j++) {
189
+ const b0 = g_flB[sb + j] * 3, b1 = g_flB[sb + (j + 1) % lb] * 3;
190
+ const ebx = wvB[b1] - wvB[b0], eby = wvB[b1 + 1] - wvB[b0 + 1], ebz = wvB[b1 + 2] - wvB[b0 + 2];
191
+ const cx = eay * ebz - eaz * eby, cy = eaz * ebx - eax * ebz, cz = eax * eby - eay * ebx;
192
+ if (test_axis(cx, cy, cz, g_nA, g_nB, 2, -1)) return false;
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ if (sat.kind === -1) return false;
199
+
200
+ const nx = sat.nx, ny = sat.ny, nz = sat.nz;
201
+ out[0] = nx; out[1] = ny; out[2] = nz;
202
+
203
+ // Edge-cross winner: single deepest support-pair contact (v1).
204
+ if (sat.kind === 2) {
205
+ emit_single_contact(out, nx, ny, nz, sat.overlap);
206
+ return true;
207
+ }
208
+
209
+ // Face winner: reference / incident polygon clipping.
210
+ emit_face_manifold(out, sat.kind === 0, sat.face, nx, ny, nz);
211
+ return true;
212
+ }
213
+
214
+ /**
215
+ * Clip-only entry point — the caller supplies the separating axis `n` (B→A,
216
+ * unit) from a robust GJK+EPA query ({@link gjk_epa_penetration}); we only build
217
+ * the multi-point manifold. This is Bullet's `clipHullAgainstHull(sepNormal,…)`
218
+ * with the normal sourced from GJK rather than SAT. Used for LARGE convex
219
+ * polytopes (authored hulls, a convex mesh as one piece) where SAT's O(Fa·Fb +
220
+ * Ea·Eb) edge term is prohibitive but GJK+EPA is O(support queries). Always
221
+ * emits ≥ 1 contact (the caller already established penetration): if the face
222
+ * clip empties (a vertex/edge feature) it falls back to the deepest support-pair.
223
+ *
224
+ * @param {Float64Array|number[]} out length ≥ CONVEX_CONVEX_OUT_LENGTH
225
+ * @param {{vertices,face_offsets,face_loops}} A @param {{x,y,z}} posA @param {{x,y,z,w}} rotA
226
+ * @param {{vertices,face_offsets,face_loops}} B @param {{x,y,z}} posB @param {{x,y,z,w}} rotB
227
+ * @param {number} nx @param {number} ny @param {number} nz unit axis, B→A
228
+ * @returns {boolean} always true
229
+ */
230
+ export function convex_hull_clip(out, A, posA, rotA, B, posB, rotB, nx, ny, nz) {
231
+ set_geom(A, B);
232
+ load_world(wvA, A.vertices, g_nA, posA, rotA);
233
+ load_world(wvB, B.vertices, g_nB, posB, rotB);
234
+
235
+ // Orient the axis B→A using the shapes' GEOMETRIC centres (centroids of the
236
+ // world vertices), the same robust reference the SAT path uses. A body-frame
237
+ // guard (transform positions) degenerates when the two origins nearly
238
+ // coincide — e.g. a tet resting base-to-base on another, both transforms at
239
+ // the contact plane — leaving the normal mis-oriented and the stack tunnels.
240
+ centroid(cenA, wvA, g_nA);
241
+ centroid(cenB, wvB, g_nB);
242
+ if (nx * (cenA[0] - cenB[0]) + ny * (cenA[1] - cenB[1]) + nz * (cenA[2] - cenB[2]) < 0) {
243
+ nx = -nx; ny = -ny; nz = -nz;
244
+ }
245
+ out[0] = nx; out[1] = ny; out[2] = nz;
246
+
247
+ // Reference face = the face (A or B) whose outward normal is most aligned
248
+ // with the axis pointing toward the other body. A's contact face points
249
+ // toward B (along −n); B's toward A (along +n). The flatter (better-aligned)
250
+ // one is the reference. Mirrors Bullet's "face most opposing the normal",
251
+ // made symmetric across A and B.
252
+ let bestA = -Infinity, faceA = 0;
253
+ for (let f = 0; f < g_fA; f++) {
254
+ face_normal(tmp_n, wvA, g_flA, g_foA[f], g_foA[f + 1]);
255
+ const l = Math.sqrt(tmp_n[0] * tmp_n[0] + tmp_n[1] * tmp_n[1] + tmp_n[2] * tmp_n[2]) || 1;
256
+ const d = (tmp_n[0] * -nx + tmp_n[1] * -ny + tmp_n[2] * -nz) / l;
257
+ if (d > bestA) { bestA = d; faceA = f; }
258
+ }
259
+ let bestB = -Infinity, faceB = 0;
260
+ for (let f = 0; f < g_fB; f++) {
261
+ face_normal(tmp_n, wvB, g_flB, g_foB[f], g_foB[f + 1]);
262
+ const l = Math.sqrt(tmp_n[0] * tmp_n[0] + tmp_n[1] * tmp_n[1] + tmp_n[2] * tmp_n[2]) || 1;
263
+ const d = (tmp_n[0] * nx + tmp_n[1] * ny + tmp_n[2] * nz) / l;
264
+ if (d > bestB) { bestB = d; faceB = f; }
265
+ }
266
+
267
+ const ref_is_A = bestA >= bestB;
268
+ emit_face_manifold(out, ref_is_A, ref_is_A ? faceA : faceB, nx, ny, nz);
269
+
270
+ if ((out[3] | 0) === 0) {
271
+ const overlap = projection_overlap(nx, ny, nz);
272
+ emit_single_contact(out, nx, ny, nz, overlap > 0 ? overlap : 0);
273
+ }
274
+ return true;
275
+ }
276
+
277
+ /** Overlap of the two pieces' projections onto axis `n` (B→A); A is on the +n side. */
278
+ function projection_overlap(nx, ny, nz) {
279
+ let minA = Infinity, maxB = -Infinity;
280
+ for (let i = 0; i < g_nA; i++) {
281
+ const d = nx * wvA[i * 3] + ny * wvA[i * 3 + 1] + nz * wvA[i * 3 + 2];
282
+ if (d < minA) minA = d;
283
+ }
284
+ for (let i = 0; i < g_nB; i++) {
285
+ const d = nx * wvB[i * 3] + ny * wvB[i * 3 + 1] + nz * wvB[i * 3 + 2];
286
+ if (d > maxB) maxB = d;
287
+ }
288
+ return maxB - minA;
289
+ }
290
+
291
+ /** Single deepest support-pair contact along axis `n` (B→A). */
292
+ function emit_single_contact(out, nx, ny, nz, depth) {
293
+ let sa = -Infinity, ai = 0;
294
+ for (let i = 0; i < g_nA; i++) {
295
+ const d = -(nx * wvA[i * 3] + ny * wvA[i * 3 + 1] + nz * wvA[i * 3 + 2]);
296
+ if (d > sa) { sa = d; ai = i; }
297
+ }
298
+ let sb = -Infinity, bi = 0;
299
+ for (let i = 0; i < g_nB; i++) {
300
+ const d = nx * wvB[i * 3] + ny * wvB[i * 3 + 1] + nz * wvB[i * 3 + 2];
301
+ if (d > sb) { sb = d; bi = i; }
302
+ }
303
+ out[3] = 1;
304
+ out[4] = wvA[ai * 3]; out[5] = wvA[ai * 3 + 1]; out[6] = wvA[ai * 3 + 2];
305
+ out[7] = wvB[bi * 3]; out[8] = wvB[bi * 3 + 1]; out[9] = wvB[bi * 3 + 2];
306
+ out[10] = depth;
307
+ }
308
+
309
+ /**
310
+ * Reference/incident polygon clipping given the reference face. Writes the
311
+ * manifold (count + contacts) into `out`; leaves out[0..2] (the normal) as set
312
+ * by the caller. Shared by both the SAT and GJK+EPA entry points.
313
+ *
314
+ * @param {boolean} ref_is_A reference face belongs to A (else B)
315
+ * @param {number} ref_face reference face index within its piece
316
+ * @param {number} nx @param {number} ny @param {number} nz axis (B→A), unit
317
+ */
318
+ function emit_face_manifold(out, ref_is_A, ref_face, nx, ny, nz) {
319
+ const ref_wv = ref_is_A ? wvA : wvB;
320
+ const ref_fo = ref_is_A ? g_foA : g_foB;
321
+ const ref_fl = ref_is_A ? g_flA : g_flB;
322
+ const inc_wv = ref_is_A ? wvB : wvA;
323
+ const inc_fo = ref_is_A ? g_foB : g_foA;
324
+ const inc_fl = ref_is_A ? g_flB : g_flA;
325
+ const inc_fcount = ref_is_A ? g_fB : g_fA;
326
+
327
+ // Reference outward normal points ref → incident: −n if ref is A, +n if ref is B.
328
+ const rmx = ref_is_A ? -nx : nx, rmy = ref_is_A ? -ny : ny, rmz = ref_is_A ? -nz : nz;
329
+
330
+ const ref_s = ref_fo[ref_face], ref_len = ref_fo[ref_face + 1] - ref_s;
331
+ const r0 = ref_fl[ref_s] * 3;
332
+ const r0x = ref_wv[r0], r0y = ref_wv[r0 + 1], r0z = ref_wv[r0 + 2];
333
+
334
+ // Incident face = the polygon on the other piece most antiparallel to rm.
335
+ let inc_face = 0, most_anti = Infinity;
336
+ for (let f = 0; f < inc_fcount; f++) {
337
+ face_normal(tmp_n, inc_wv, inc_fl, inc_fo[f], inc_fo[f + 1]);
338
+ const l = Math.sqrt(tmp_n[0] * tmp_n[0] + tmp_n[1] * tmp_n[1] + tmp_n[2] * tmp_n[2]) || 1;
339
+ const dot = (tmp_n[0] * rmx + tmp_n[1] * rmy + tmp_n[2] * rmz) / l;
340
+ if (dot < most_anti) { most_anti = dot; inc_face = f; }
341
+ }
342
+
343
+ // Incident polygon (world).
344
+ const inc_s = inc_fo[inc_face], inc_len = inc_fo[inc_face + 1] - inc_s;
345
+ let n_poly = inc_len;
346
+ for (let i = 0; i < inc_len; i++) {
347
+ const vi = inc_fl[inc_s + i] * 3;
348
+ poly_in[i * 3] = inc_wv[vi]; poly_in[i * 3 + 1] = inc_wv[vi + 1]; poly_in[i * 3 + 2] = inc_wv[vi + 2];
349
+ }
350
+
351
+ // Reference polygon centroid (orients side planes inward).
352
+ let rcx = 0, rcy = 0, rcz = 0;
353
+ for (let i = 0; i < ref_len; i++) {
354
+ const vi = ref_fl[ref_s + i] * 3;
355
+ rcx += ref_wv[vi]; rcy += ref_wv[vi + 1]; rcz += ref_wv[vi + 2];
356
+ }
357
+ rcx /= ref_len; rcy /= ref_len; rcz /= ref_len;
358
+
359
+ // Clip the incident polygon against each reference-face side plane.
360
+ let src = poly_in, dst = poly_out;
361
+ for (let e = 0; e < ref_len && n_poly > 0; e++) {
362
+ const va = ref_fl[ref_s + e] * 3, vb = ref_fl[ref_s + (e + 1) % ref_len] * 3;
363
+ const edx = ref_wv[vb] - ref_wv[va], edy = ref_wv[vb + 1] - ref_wv[va + 1], edz = ref_wv[vb + 2] - ref_wv[va + 2];
364
+ let sx = edy * rmz - edz * rmy, sy = edz * rmx - edx * rmz, sz = edx * rmy - edy * rmx;
365
+ const pax = ref_wv[va], pay = ref_wv[va + 1], paz = ref_wv[va + 2];
366
+ if ((rcx - pax) * sx + (rcy - pay) * sy + (rcz - paz) * sz > 0) { sx = -sx; sy = -sy; sz = -sz; }
367
+ n_poly = clip_poly(src, n_poly, dst, sx, sy, sz, pax, pay, paz);
368
+ const t = src; src = dst; dst = t;
369
+ }
370
+ if (n_poly === 0) { out[3] = 0; return; }
371
+
372
+ let nc = 0;
373
+ for (let i = 0; i < n_poly; i++) {
374
+ const px = src[i * 3], py = src[i * 3 + 1], pz = src[i * 3 + 2];
375
+ const sep = (px - r0x) * rmx + (py - r0y) * rmy + (pz - r0z) * rmz;
376
+ const depth = -sep;
377
+ if (depth < 0) continue;
378
+ cand[nc * 4] = px; cand[nc * 4 + 1] = py; cand[nc * 4 + 2] = pz; cand[nc * 4 + 3] = depth;
379
+ nc++;
380
+ }
381
+ if (nc === 0) { out[3] = 0; return; }
382
+
383
+ const kept = reduce_manifold_contacts(cand, nc, 4, MAX_CONTACTS);
384
+ out[3] = kept;
385
+ for (let k = 0; k < kept; k++) {
386
+ const px = cand[k * 4], py = cand[k * 4 + 1], pz = cand[k * 4 + 2], depth = cand[k * 4 + 3];
387
+ const d2p = (px - r0x) * rmx + (py - r0y) * rmy + (pz - r0z) * rmz;
388
+ const rpx = px - rmx * d2p, rpy = py - rmy * d2p, rpz = pz - rmz * d2p;
389
+ const base = 4 + k * CONTACT_STRIDE;
390
+ if (ref_is_A) {
391
+ out[base] = rpx; out[base + 1] = rpy; out[base + 2] = rpz;
392
+ out[base + 3] = px; out[base + 4] = py; out[base + 5] = pz;
393
+ } else {
394
+ out[base] = px; out[base + 1] = py; out[base + 2] = pz;
395
+ out[base + 3] = rpx; out[base + 4] = rpy; out[base + 5] = rpz;
396
+ }
397
+ out[base + 6] = depth;
398
+ }
399
+ }
400
+
401
+ /** Sutherland–Hodgman: keep polygon points where (P − a)·s ≤ 0, with crossings. */
402
+ function clip_poly(src, n, dst, sx, sy, sz, ax, ay, az) {
403
+ let m = 0;
404
+ for (let i = 0; i < n; i++) {
405
+ const j = (i + 1) % n;
406
+ const px = src[i * 3], py = src[i * 3 + 1], pz = src[i * 3 + 2];
407
+ const qx = src[j * 3], qy = src[j * 3 + 1], qz = src[j * 3 + 2];
408
+ const dp = (px - ax) * sx + (py - ay) * sy + (pz - az) * sz;
409
+ const dq = (qx - ax) * sx + (qy - ay) * sy + (qz - az) * sz;
410
+ const p_in = dp <= 0, q_in = dq <= 0;
411
+ if (p_in) { dst[m * 3] = px; dst[m * 3 + 1] = py; dst[m * 3 + 2] = pz; m++; }
412
+ if (p_in !== q_in) {
413
+ const t = dp / (dp - dq);
414
+ dst[m * 3] = px + (qx - px) * t;
415
+ dst[m * 3 + 1] = py + (qy - py) * t;
416
+ dst[m * 3 + 2] = pz + (qz - pz) * t;
417
+ m++;
418
+ if (m >= MAX_POLY) break;
419
+ }
420
+ }
421
+ return m;
422
+ }
423
+
424
+ // Contact reduction (deepest + farthest-point spread) is shared with
425
+ // box_box_manifold via reduce_manifold_contacts (stride 4).
@@ -0,0 +1,32 @@
1
+ export function face_key(a: any, b: any, c: any): string;
2
+ /**
3
+ * @param {{tet_mesh, tet_positions, compute_bounding_box}} meshShape
4
+ * @returns {{pieces: Array<{vertices:Float32Array, face_offsets:Uint32Array, face_loops:Uint32Array, aabb:Float64Array}>, bvh: BVH}}
5
+ */
6
+ export function build_convex_decomposition(meshShape: {
7
+ tet_mesh;
8
+ tet_positions;
9
+ compute_bounding_box;
10
+ }): {
11
+ pieces: Array<{
12
+ vertices: Float32Array;
13
+ face_offsets: Uint32Array;
14
+ face_loops: Uint32Array;
15
+ aabb: Float64Array;
16
+ }>;
17
+ bvh: BVH;
18
+ };
19
+ /** Build a clippable piece from its boundary triangles: coplanar-merge into
20
+ * polygon faces and localise vertex indices. `boundary` is a Map keyed by
21
+ * face_key → {a,b,c} (any winding; orientation is re-derived from the piece
22
+ * centroid). Exported so the convex-mesh fast path can build a single
23
+ * coplanar-merged hull from a whole mesh's boundary. */
24
+ export function build_piece(tp: any, boundary: any): {
25
+ vertices: Float32Array;
26
+ face_offsets: Uint32Array;
27
+ face_loops: Uint32Array;
28
+ aabb: Float64Array;
29
+ };
30
+ export const FACE_OPP: number[][];
31
+ import { BVH } from "../../../core/bvh2/bvh3/BVH.js";
32
+ //# sourceMappingURL=convex_decomposition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convex_decomposition.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/convex_decomposition.js"],"names":[],"mappings":"AAsCA,yDAOC;AAED;;;GAGG;AACH,sDAHW;IAAC,QAAQ,CAAC;IAAC,aAAa,CAAC;IAAC,oBAAoB,CAAA;CAAC;YACpC,MAAM;QAAC,QAAQ,EAAC,YAAY,CAAC;QAAC,YAAY,EAAC,WAAW,CAAC;QAAC,UAAU,EAAC,WAAW,CAAC;QAAC,IAAI,EAAC,YAAY,CAAA;KAAC,CAAC;SAAO,GAAG;EA+IlI;AAED;;;;yDAIyD;AACzD;;;;;EA6FC;AAhQD,kCAAiF;oBApC7D,gCAAgC"}