@woosh/meep-engine 2.152.0 → 2.154.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 (99) hide show
  1. package/package.json +1 -1
  2. package/src/core/color/Color.d.ts +26 -6
  3. package/src/core/color/Color.d.ts.map +1 -1
  4. package/src/core/color/Color.js +38 -6
  5. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts +112 -0
  6. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -0
  7. package/src/core/geom/3d/shape/ConvexHullShape3D.js +325 -0
  8. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +4 -0
  9. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
  10. package/src/engine/graphics/ecs/trail2d/Trail2D.js +21 -0
  11. package/src/engine/physics/PLAN.md +4 -4
  12. package/src/engine/physics/body/BodyStorage.d.ts +3 -1
  13. package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
  14. package/src/engine/physics/body/BodyStorage.js +452 -450
  15. package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -1
  16. package/src/engine/physics/body/SolverBodyState.js +6 -5
  17. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  18. package/src/engine/physics/broadphase/generate_pairs.js +9 -1
  19. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
  20. package/src/engine/physics/ccd/linear_sweep.js +237 -238
  21. package/src/engine/physics/computeInterceptPoint.d.ts.map +1 -1
  22. package/src/engine/physics/computeInterceptPoint.js +8 -3
  23. package/src/engine/physics/contact/ManifoldStore.d.ts +0 -16
  24. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  25. package/src/engine/physics/contact/ManifoldStore.js +1 -38
  26. package/src/engine/physics/ecs/BodyKind.d.ts +3 -2
  27. package/src/engine/physics/ecs/BodyKind.d.ts.map +1 -1
  28. package/src/engine/physics/ecs/BodyKind.js +25 -24
  29. package/src/engine/physics/ecs/PhysicsEvents.d.ts +4 -5
  30. package/src/engine/physics/ecs/PhysicsEvents.d.ts.map +1 -1
  31. package/src/engine/physics/ecs/PhysicsEvents.js +15 -16
  32. package/src/engine/physics/ecs/PhysicsSystem.d.ts +5 -30
  33. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  34. package/src/engine/physics/ecs/PhysicsSystem.js +13 -45
  35. package/src/engine/physics/ecs/RigidBodySerializationAdapter.d.ts.map +1 -1
  36. package/src/engine/physics/ecs/RigidBodySerializationAdapter.js +85 -81
  37. package/src/engine/physics/ecs/is_sensor.d.ts +18 -0
  38. package/src/engine/physics/ecs/is_sensor.d.ts.map +1 -0
  39. package/src/engine/physics/ecs/is_sensor.js +27 -0
  40. package/src/engine/physics/events/ContactEventBuffer.d.ts +2 -1
  41. package/src/engine/physics/events/ContactEventBuffer.d.ts.map +1 -1
  42. package/src/engine/physics/events/ContactEventBuffer.js +84 -83
  43. package/src/engine/physics/gjk/gjk.d.ts +0 -26
  44. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  45. package/src/engine/physics/gjk/gjk.js +3 -52
  46. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts +16 -0
  47. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts.map +1 -0
  48. package/src/engine/physics/gjk/gjk_epa_penetration.js +255 -0
  49. package/src/engine/physics/gjk/minkowski_support.d.ts +4 -9
  50. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -1
  51. package/src/engine/physics/gjk/minkowski_support.js +70 -75
  52. package/src/engine/physics/gjk/mpr.d.ts +1 -1
  53. package/src/engine/physics/gjk/mpr.d.ts.map +1 -1
  54. package/src/engine/physics/gjk/mpr.js +362 -344
  55. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  56. package/src/engine/physics/island/IslandBuilder.js +431 -428
  57. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  58. package/src/engine/physics/narrowphase/box_box_manifold.js +4 -81
  59. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
  60. package/src/engine/physics/narrowphase/box_triangle_contact.js +4 -39
  61. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  62. package/src/engine/physics/narrowphase/capsule_contacts.js +459 -462
  63. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts.map +1 -1
  64. package/src/engine/physics/narrowphase/clip_against_axis_uv.js +4 -1
  65. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +83 -0
  66. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -0
  67. package/src/engine/physics/narrowphase/convex_convex_manifold.js +425 -0
  68. package/src/engine/physics/narrowphase/convex_decomposition.d.ts +32 -0
  69. package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -0
  70. package/src/engine/physics/narrowphase/convex_decomposition.js +293 -0
  71. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts +41 -0
  72. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts.map +1 -0
  73. package/src/engine/physics/narrowphase/mesh_convex_hull.js +106 -0
  74. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts +8 -0
  75. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts.map +1 -0
  76. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.js +117 -0
  77. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  78. package/src/engine/physics/narrowphase/narrowphase_step.js +105 -102
  79. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts +29 -0
  80. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts.map +1 -0
  81. package/src/engine/physics/narrowphase/reduce_manifold_contacts.js +69 -0
  82. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  83. package/src/engine/physics/narrowphase/refine_ray_concave.js +152 -145
  84. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  85. package/src/engine/physics/narrowphase/sphere_box_contact.js +132 -123
  86. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  87. package/src/engine/physics/queries/overlap_shape.js +16 -17
  88. package/src/engine/physics/queries/raycast.d.ts +5 -0
  89. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  90. package/src/engine/physics/queries/raycast.js +16 -8
  91. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -1
  92. package/src/engine/physics/queries/shape_cast.js +13 -7
  93. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  94. package/src/engine/physics/solver/solve_contacts.js +8 -11
  95. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
  96. package/src/engine/physics/vehicle/RaycastVehicle.js +339 -333
  97. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +0 -13
  98. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +0 -1
  99. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +0 -399
@@ -0,0 +1,293 @@
1
+ import { BVH } from "../../../core/bvh2/bvh3/BVH.js";
2
+
3
+ /**
4
+ * Greedy convex decomposition of a tetrahedralised mesh into a small set of
5
+ * convex pieces, each with coplanar-merged polygon faces — the representation
6
+ * the contact clipper ({@link convex_convex_manifold}) consumes.
7
+ *
8
+ * Why: colliding two meshes as their raw tetrahedra means a SAT+clip per
9
+ * contacting tet pair. Merging adjacent tets into larger convex pieces collapses
10
+ * a convex sub-region (e.g. a box) to a SINGLE piece, cutting both the pair
11
+ * count and the per-pair work — the path to large tet counts.
12
+ *
13
+ * Pipeline:
14
+ * 1. Build face → tets adjacency from shared faces (sorted vertex triples).
15
+ * 2. Greedy region-grow: seed a piece from a tet, BFS-absorb face-neighbours
16
+ * while the union stays convex. Convexity test is definitive: every vertex
17
+ * of the candidate union lies behind (or on) every boundary-face plane.
18
+ * 3. Per piece: XOR the member tets' faces to get the boundary triangles
19
+ * (outward-oriented), coplanar-merge them into polygon faces by walking the
20
+ * directed boundary edges, and localise the vertex indices.
21
+ * 4. Build a BVH over the pieces' local AABBs for piece-pair culling.
22
+ *
23
+ * Output (cached on the MeshShape3D): `{ pieces, bvh }` where each piece is
24
+ * `{ vertices, face_offsets, face_loops, aabb }` — directly clippable.
25
+ *
26
+ * @author Alex Goldring
27
+ * @copyright Company Named Limited (c) 2026
28
+ */
29
+
30
+ // Quantisation for grouping coplanar faces (normal + plane offset).
31
+ const PLANE_NORMAL_TOL = 1e-4;
32
+ const PLANE_OFFSET_TOL = 1e-4;
33
+
34
+ // A tet face by index: the 3 face-vertex slots + the opposite-vertex slot.
35
+ // Exported so the convex-mesh fast path (mesh_convex_hull.js) shares the same
36
+ // tet-face convention rather than keeping a parallel copy.
37
+ export const FACE_OPP = [[1, 2, 3, 0], [0, 2, 3, 1], [0, 1, 3, 2], [0, 1, 2, 3]];
38
+
39
+ export function face_key(a, b, c) {
40
+ // Exact, collision-free order-independent key for a triangle's vertex triple.
41
+ let x = a, y = b, z = c, t;
42
+ if (x > y) { t = x; x = y; y = t; }
43
+ if (y > z) { t = y; y = z; z = t; }
44
+ if (x > y) { t = x; x = y; y = t; }
45
+ return x + "," + y + "," + z;
46
+ }
47
+
48
+ /**
49
+ * @param {{tet_mesh, tet_positions, compute_bounding_box}} meshShape
50
+ * @returns {{pieces: Array<{vertices:Float32Array, face_offsets:Uint32Array, face_loops:Uint32Array, aabb:Float64Array}>, bvh: BVH}}
51
+ */
52
+ export function build_convex_decomposition(meshShape) {
53
+ const tm = meshShape.tet_mesh;
54
+ const tp = meshShape.tet_positions;
55
+ const N = tm.count;
56
+
57
+ // Scale-relative epsilon for the convexity test.
58
+ const bb = new Float64Array(6);
59
+ meshShape.compute_bounding_box(bb);
60
+ const diag = Math.hypot(bb[3] - bb[0], bb[4] - bb[1], bb[5] - bb[2]) || 1;
61
+ const EPS = 1e-6 * diag;
62
+
63
+ // Tet vertices (global indices) and the 4 faces (each: 3 face verts + the
64
+ // opposite vertex, for outward orientation).
65
+ const tetV = new Int32Array(N * 4);
66
+ for (let t = 0; t < N; t++) {
67
+ for (let i = 0; i < 4; i++) tetV[t * 4 + i] = tm.getVertexIndex(t, i);
68
+ }
69
+
70
+ // Adjacency: face-key → list of (tet) owners. Two tets sharing a face are
71
+ // neighbours. Exact triple stored alongside to resolve hash collisions.
72
+ const faceOwners = new Map(); // key → array of { tet, fi }
73
+ for (let t = 0; t < N; t++) {
74
+ for (let f = 0; f < 4; f++) {
75
+ const o = FACE_OPP[f];
76
+ const a = tetV[t * 4 + o[0]], b = tetV[t * 4 + o[1]], c = tetV[t * 4 + o[2]];
77
+ const k = face_key(a, b, c);
78
+ let arr = faceOwners.get(k);
79
+ if (arr === undefined) { arr = []; faceOwners.set(k, arr); }
80
+ arr.push({ tet: t, fi: f, a, b, c });
81
+ }
82
+ }
83
+ // tet → neighbour tets (sharing a face).
84
+ const neighbours = (t) => {
85
+ const out = [];
86
+ for (let f = 0; f < 4; f++) {
87
+ const o = FACE_OPP[f];
88
+ const a = tetV[t * 4 + o[0]], b = tetV[t * 4 + o[1]], c = tetV[t * 4 + o[2]];
89
+ const arr = faceOwners.get(face_key(a, b, c));
90
+ if (arr === undefined) continue;
91
+ for (const e of arr) if (e.tet !== t) out.push(e.tet);
92
+ }
93
+ return out;
94
+ };
95
+
96
+ const visited = new Uint8Array(N);
97
+ const pieces = [];
98
+
99
+ // Reusable boundary structure for the growing piece: key → oriented face.
100
+ for (let seed = 0; seed < N; seed++) {
101
+ if (visited[seed]) continue;
102
+
103
+ const boundary = new Map(); // key → {a,b,c (oriented), nx,ny,nz}
104
+ const pieceVerts = new Set();
105
+ const pieceTets = [];
106
+
107
+ // Toggle a tet's 4 faces against the boundary (its own inverse): a face
108
+ // shared with a piece tet cancels (becomes interior). Store just the
109
+ // triple — orientation is resolved later from the piece centroid.
110
+ const toggle_tet_faces = (t) => {
111
+ for (let f = 0; f < 4; f++) {
112
+ const o = FACE_OPP[f];
113
+ const a = tetV[t * 4 + o[0]], b = tetV[t * 4 + o[1]], c = tetV[t * 4 + o[2]];
114
+ const k = face_key(a, b, c);
115
+ if (boundary.has(k)) boundary.delete(k);
116
+ else boundary.set(k, { a, b, c });
117
+ }
118
+ };
119
+ const add_verts = (t) => { for (let i = 0; i < 4; i++) pieceVerts.add(tetV[t * 4 + i]); };
120
+
121
+ // Convex iff every boundary-face PLANE is supporting: all vertices lie
122
+ // on one side of it. Orientation-free — no need to know which side is
123
+ // outward, so it's immune to sliver/orientation issues.
124
+ const convex_with = (candVerts) => {
125
+ for (const [, fc] of boundary) {
126
+ const ax = tp[fc.a * 3], ay = tp[fc.a * 3 + 1], az = tp[fc.a * 3 + 2];
127
+ const e1x = tp[fc.b * 3] - ax, e1y = tp[fc.b * 3 + 1] - ay, e1z = tp[fc.b * 3 + 2] - az;
128
+ const e2x = tp[fc.c * 3] - ax, e2y = tp[fc.c * 3 + 1] - ay, e2z = tp[fc.c * 3 + 2] - az;
129
+ let nx = e1y * e2z - e1z * e2y, ny = e1z * e2x - e1x * e2z, nz = e1x * e2y - e1y * e2x;
130
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
131
+ nx /= len; ny /= len; nz /= len;
132
+ let pos = false, neg = false;
133
+ for (const v of pieceVerts) {
134
+ const d = nx * (tp[v * 3] - ax) + ny * (tp[v * 3 + 1] - ay) + nz * (tp[v * 3 + 2] - az);
135
+ if (d > EPS) pos = true; else if (d < -EPS) neg = true;
136
+ if (pos && neg) return false;
137
+ }
138
+ for (let i = 0; i < 4; i++) {
139
+ const v = candVerts[i];
140
+ const d = nx * (tp[v * 3] - ax) + ny * (tp[v * 3 + 1] - ay) + nz * (tp[v * 3 + 2] - az);
141
+ if (d > EPS) pos = true; else if (d < -EPS) neg = true;
142
+ if (pos && neg) return false;
143
+ }
144
+ }
145
+ return true;
146
+ };
147
+
148
+ visited[seed] = 1;
149
+ pieceTets.push(seed);
150
+ toggle_tet_faces(seed);
151
+ add_verts(seed);
152
+
153
+ const tried = new Set([seed]);
154
+ const frontier = neighbours(seed);
155
+ for (const n of frontier) tried.add(n);
156
+
157
+ while (frontier.length > 0) {
158
+ const t = frontier.pop();
159
+ if (visited[t]) continue;
160
+
161
+ // Tentatively add t: toggle its faces into the boundary, test
162
+ // convexity, then commit or revert (toggle is its own inverse).
163
+ const cand = [tetV[t * 4], tetV[t * 4 + 1], tetV[t * 4 + 2], tetV[t * 4 + 3]];
164
+ toggle_tet_faces(t);
165
+ if (convex_with(cand)) {
166
+ visited[t] = 1;
167
+ pieceTets.push(t);
168
+ add_verts(t);
169
+ for (const n of neighbours(t)) {
170
+ if (!visited[n] && !tried.has(n)) { tried.add(n); frontier.push(n); }
171
+ }
172
+ } else {
173
+ toggle_tet_faces(t); // revert
174
+ }
175
+ }
176
+
177
+ pieces.push(build_piece(tp, boundary));
178
+ }
179
+
180
+ // BVH over piece local AABBs.
181
+ const bvh = new BVH();
182
+ bvh.release_all();
183
+ bvh.node_capacity = Math.max(1, pieces.length * 2 - 1);
184
+ for (let i = 0; i < pieces.length; i++) {
185
+ const node = bvh.allocate_node();
186
+ const a = pieces[i].aabb;
187
+ bvh.node_set_aabb_primitive(node, a[0], a[1], a[2], a[3], a[4], a[5]);
188
+ bvh.node_set_user_data(node, i);
189
+ bvh.insert_leaf(node);
190
+ }
191
+
192
+ return { pieces, bvh };
193
+ }
194
+
195
+ /** Build a clippable piece from its boundary triangles: coplanar-merge into
196
+ * polygon faces and localise vertex indices. `boundary` is a Map keyed by
197
+ * face_key → {a,b,c} (any winding; orientation is re-derived from the piece
198
+ * centroid). Exported so the convex-mesh fast path can build a single
199
+ * coplanar-merged hull from a whole mesh's boundary. */
200
+ export function build_piece(tp, boundary) {
201
+ // Piece centroid (from boundary vertices) — strictly interior for a convex
202
+ // piece, so it orients every face normal outward unambiguously.
203
+ const cset = new Set();
204
+ for (const [, fc] of boundary) { cset.add(fc.a); cset.add(fc.b); cset.add(fc.c); }
205
+ let pcx = 0, pcy = 0, pcz = 0;
206
+ for (const v of cset) { pcx += tp[v * 3]; pcy += tp[v * 3 + 1]; pcz += tp[v * 3 + 2]; }
207
+ const pn = cset.size || 1; pcx /= pn; pcy /= pn; pcz /= pn;
208
+
209
+ // Group boundary triangles by plane; each normal oriented away from the centroid.
210
+ const groups = []; // { nx,ny,nz, off, tris: [[a,b,c], ...] }
211
+ for (const [, fc] of boundary) {
212
+ const ax = tp[fc.a * 3], ay = tp[fc.a * 3 + 1], az = tp[fc.a * 3 + 2];
213
+ const e1x = tp[fc.b * 3] - ax, e1y = tp[fc.b * 3 + 1] - ay, e1z = tp[fc.b * 3 + 2] - az;
214
+ const e2x = tp[fc.c * 3] - ax, e2y = tp[fc.c * 3 + 1] - ay, e2z = tp[fc.c * 3 + 2] - az;
215
+ let nx = e1y * e2z - e1z * e2y, ny = e1z * e2x - e1x * e2z, nz = e1x * e2y - e1y * e2x;
216
+ const len = Math.hypot(nx, ny, nz) || 1;
217
+ nx /= len; ny /= len; nz /= len;
218
+ if (nx * (ax - pcx) + ny * (ay - pcy) + nz * (az - pcz) < 0) { nx = -nx; ny = -ny; nz = -nz; }
219
+ const off = nx * ax + ny * ay + nz * az;
220
+ let g = null;
221
+ for (const cand of groups) {
222
+ if (Math.abs(cand.nx * nx + cand.ny * ny + cand.nz * nz - 1) < PLANE_NORMAL_TOL
223
+ && Math.abs(cand.off - off) < PLANE_OFFSET_TOL) { g = cand; break; }
224
+ }
225
+ if (g === null) { g = { nx, ny, nz, off, tris: [] }; groups.push(g); }
226
+ g.tris.push([fc.a, fc.b, fc.c]);
227
+ }
228
+
229
+ // Per group: each face of a convex piece is a convex polygon, so order its
230
+ // (boundary) vertices by angle around the face centroid in the plane, CCW
231
+ // w.r.t. the known outward normal. Robust regardless of triangulation.
232
+ const loops = []; // arrays of global vertex indices, CCW outward
233
+ for (const g of groups) {
234
+ const vset = new Set();
235
+ for (const [a, b, c] of g.tris) { vset.add(a); vset.add(b); vset.add(c); }
236
+ if (vset.size < 3) continue;
237
+ const varr = Array.from(vset);
238
+
239
+ let cx = 0, cy = 0, cz = 0;
240
+ for (const gv of varr) { cx += tp[gv * 3]; cy += tp[gv * 3 + 1]; cz += tp[gv * 3 + 2]; }
241
+ const inv = 1 / varr.length; cx *= inv; cy *= inv; cz *= inv;
242
+
243
+ // In-plane basis (u, v=n×u) from the outward normal n.
244
+ const nx = g.nx, ny = g.ny, nz = g.nz;
245
+ let ux, uy, uz;
246
+ const ax = Math.abs(nx), ay = Math.abs(ny), az = Math.abs(nz);
247
+ if (ax <= ay && ax <= az) { ux = 0; uy = -nz; uz = ny; }
248
+ else if (ay <= az) { ux = -nz; uy = 0; uz = nx; }
249
+ else { ux = -ny; uy = nx; uz = 0; }
250
+ const ul = Math.hypot(ux, uy, uz) || 1; ux /= ul; uy /= ul; uz /= ul;
251
+ const vx = ny * uz - nz * uy, vy = nz * ux - nx * uz, vz = nx * uy - ny * ux;
252
+
253
+ const items = varr.map((gv) => {
254
+ const dx = tp[gv * 3] - cx, dy = tp[gv * 3 + 1] - cy, dz = tp[gv * 3 + 2] - cz;
255
+ return { gv, ang: Math.atan2(dx * vx + dy * vy + dz * vz, dx * ux + dy * uy + dz * uz) };
256
+ });
257
+ items.sort((p, q) => p.ang - q.ang);
258
+ loops.push(items.map((it) => it.gv));
259
+ }
260
+
261
+ // Localise vertices used by the loops.
262
+ const remap = new Map();
263
+ const verts = [];
264
+ const local_of = (g) => {
265
+ let l = remap.get(g);
266
+ if (l === undefined) {
267
+ l = verts.length / 3;
268
+ verts.push(tp[g * 3], tp[g * 3 + 1], tp[g * 3 + 2]);
269
+ remap.set(g, l);
270
+ }
271
+ return l;
272
+ };
273
+ const face_offsets = new Uint32Array(loops.length + 1);
274
+ const loop_flat = [];
275
+ for (let i = 0; i < loops.length; i++) {
276
+ face_offsets[i] = loop_flat.length;
277
+ for (const g of loops[i]) loop_flat.push(local_of(g));
278
+ }
279
+ face_offsets[loops.length] = loop_flat.length;
280
+
281
+ const vertices = new Float32Array(verts);
282
+ const aabb = new Float64Array(6);
283
+ let mnx = Infinity, mny = Infinity, mnz = Infinity, mxx = -Infinity, mxy = -Infinity, mxz = -Infinity;
284
+ for (let i = 0; i < vertices.length / 3; i++) {
285
+ const x = vertices[i * 3], y = vertices[i * 3 + 1], z = vertices[i * 3 + 2];
286
+ if (x < mnx) mnx = x; if (x > mxx) mxx = x;
287
+ if (y < mny) mny = y; if (y > mxy) mxy = y;
288
+ if (z < mnz) mnz = z; if (z > mxz) mxz = z;
289
+ }
290
+ aabb[0] = mnx; aabb[1] = mny; aabb[2] = mnz; aabb[3] = mxx; aabb[4] = mxy; aabb[5] = mxz;
291
+
292
+ return { vertices, face_offsets, face_loops: new Uint32Array(loop_flat), aabb };
293
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Convex fast path for a triangulated {@link MeshShape3D}: if the whole mesh is
3
+ * convex, represent it as a single coplanar-merged convex piece so it collides
4
+ * via one GJK + EPA + face-clip manifold (O(support queries)) instead of being
5
+ * greedily decomposed into many small SAT pieces.
6
+ *
7
+ * Why this matters: the greedy convex decomposition fragments even a convex mesh
8
+ * (a partial union of tets is generally NOT convex, so the order-dependent grow
9
+ * can't reconstruct the single convex region — a curved convex surface splits
10
+ * into dozens of pieces). Detecting global convexity and emitting ONE piece
11
+ * collapses that to a single collider, the path to large convex meshes.
12
+ *
13
+ * The piece is built by {@link build_piece} from the mesh's whole boundary, so
14
+ * its faces are coplanar-MERGED polygons (a box → 6 quads, not 12 triangles) —
15
+ * essential for the clipper to produce a full face-on-face patch rather than
16
+ * triangle slivers. A `support()` is attached (linear scan over the piece's
17
+ * localised surface vertices, so it stays O(surface), not O(all tets)).
18
+ *
19
+ * Cached on the (immutable) shape as `__convex_hull`: the piece if convex, or
20
+ * `null` if concave. Returned verbatim on subsequent calls.
21
+ *
22
+ * @author Alex Goldring
23
+ * @copyright Company Named Limited (c) 2026
24
+ */
25
+ /**
26
+ * @param {{tet_mesh, tet_positions, compute_bounding_box}} meshShape
27
+ * @returns {{vertices:Float32Array, face_offsets:Uint32Array, face_loops:Uint32Array, aabb:Float64Array, support:Function}|null}
28
+ * a single coplanar-merged convex piece (with support) if the mesh is convex, else null
29
+ */
30
+ export function get_mesh_convex_hull(meshShape: {
31
+ tet_mesh;
32
+ tet_positions;
33
+ compute_bounding_box;
34
+ }): {
35
+ vertices: Float32Array;
36
+ face_offsets: Uint32Array;
37
+ face_loops: Uint32Array;
38
+ aabb: Float64Array;
39
+ support: Function;
40
+ } | null;
41
+ //# sourceMappingURL=mesh_convex_hull.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mesh_convex_hull.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/mesh_convex_hull.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;GAIG;AACH,gDAJW;IAAC,QAAQ,CAAC;IAAC,aAAa,CAAC;IAAC,oBAAoB,CAAA;CAAC,GAC7C;IAAC,QAAQ,EAAC,YAAY,CAAC;IAAC,YAAY,EAAC,WAAW,CAAC;IAAC,UAAU,EAAC,WAAW,CAAC;IAAC,IAAI,EAAC,YAAY,CAAC;IAAC,OAAO,WAAS;CAAC,GAAC,IAAI,CAQ/H"}
@@ -0,0 +1,106 @@
1
+ import { build_piece, face_key, FACE_OPP } from "./convex_decomposition.js";
2
+
3
+ /**
4
+ * Convex fast path for a triangulated {@link MeshShape3D}: if the whole mesh is
5
+ * convex, represent it as a single coplanar-merged convex piece so it collides
6
+ * via one GJK + EPA + face-clip manifold (O(support queries)) instead of being
7
+ * greedily decomposed into many small SAT pieces.
8
+ *
9
+ * Why this matters: the greedy convex decomposition fragments even a convex mesh
10
+ * (a partial union of tets is generally NOT convex, so the order-dependent grow
11
+ * can't reconstruct the single convex region — a curved convex surface splits
12
+ * into dozens of pieces). Detecting global convexity and emitting ONE piece
13
+ * collapses that to a single collider, the path to large convex meshes.
14
+ *
15
+ * The piece is built by {@link build_piece} from the mesh's whole boundary, so
16
+ * its faces are coplanar-MERGED polygons (a box → 6 quads, not 12 triangles) —
17
+ * essential for the clipper to produce a full face-on-face patch rather than
18
+ * triangle slivers. A `support()` is attached (linear scan over the piece's
19
+ * localised surface vertices, so it stays O(surface), not O(all tets)).
20
+ *
21
+ * Cached on the (immutable) shape as `__convex_hull`: the piece if convex, or
22
+ * `null` if concave. Returned verbatim on subsequent calls.
23
+ *
24
+ * @author Alex Goldring
25
+ * @copyright Company Named Limited (c) 2026
26
+ */
27
+
28
+ /**
29
+ * @param {{tet_mesh, tet_positions, compute_bounding_box}} meshShape
30
+ * @returns {{vertices:Float32Array, face_offsets:Uint32Array, face_loops:Uint32Array, aabb:Float64Array, support:Function}|null}
31
+ * a single coplanar-merged convex piece (with support) if the mesh is convex, else null
32
+ */
33
+ export function get_mesh_convex_hull(meshShape) {
34
+ if (meshShape.__convex_hull !== undefined) return meshShape.__convex_hull;
35
+ const hull = build_if_convex(meshShape);
36
+ meshShape.__convex_hull = hull;
37
+ return hull;
38
+ }
39
+
40
+ function build_if_convex(meshShape) {
41
+ const tm = meshShape.tet_mesh;
42
+ const tp = meshShape.tet_positions;
43
+ const N = tm.count;
44
+ if (N === 0) return null;
45
+
46
+ const bb = new Float64Array(6);
47
+ meshShape.compute_bounding_box(bb);
48
+ const diag = Math.hypot(bb[3] - bb[0], bb[4] - bb[1], bb[5] - bb[2]) || 1;
49
+ const EPS = 1e-6 * diag;
50
+
51
+ // Boundary faces = those owned by exactly one tet. Keep the owning tet's
52
+ // opposite vertex to orient the face outward for the convexity test.
53
+ const owners = new Map(); // key → { a, b, c, opp, count }
54
+ for (let t = 0; t < N; t++) {
55
+ for (let f = 0; f < 4; f++) {
56
+ const o = FACE_OPP[f];
57
+ const a = tm.getVertexIndex(t, o[0]);
58
+ const b = tm.getVertexIndex(t, o[1]);
59
+ const c = tm.getVertexIndex(t, o[2]);
60
+ const opp = tm.getVertexIndex(t, o[3]);
61
+ const k = face_key(a, b, c);
62
+ const e = owners.get(k);
63
+ if (e === undefined) owners.set(k, { a, b, c, opp, count: 1 });
64
+ else e.count++;
65
+ }
66
+ }
67
+
68
+ // Convexity: every vertex behind (or on) every boundary-face plane.
69
+ const nVerts = tp.length / 3;
70
+ const boundary = new Map(); // key → {a,b,c} for build_piece
71
+ let boundaryCount = 0;
72
+ for (const [k, fc] of owners) {
73
+ if (fc.count !== 1) continue;
74
+ boundaryCount++;
75
+ const ax = tp[fc.a * 3], ay = tp[fc.a * 3 + 1], az = tp[fc.a * 3 + 2];
76
+ const e1x = tp[fc.b * 3] - ax, e1y = tp[fc.b * 3 + 1] - ay, e1z = tp[fc.b * 3 + 2] - az;
77
+ const e2x = tp[fc.c * 3] - ax, e2y = tp[fc.c * 3 + 1] - ay, e2z = tp[fc.c * 3 + 2] - az;
78
+ let nx = e1y * e2z - e1z * e2y, ny = e1z * e2x - e1x * e2z, nz = e1x * e2y - e1y * e2x;
79
+ const ox = tp[fc.opp * 3] - ax, oy = tp[fc.opp * 3 + 1] - ay, oz = tp[fc.opp * 3 + 2] - az;
80
+ if (nx * ox + ny * oy + nz * oz > 0) { nx = -nx; ny = -ny; nz = -nz; } // outward
81
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
82
+ const ix = nx / len, iy = ny / len, iz = nz / len;
83
+ for (let v = 0; v < nVerts; v++) {
84
+ const d = ix * (tp[v * 3] - ax) + iy * (tp[v * 3 + 1] - ay) + iz * (tp[v * 3 + 2] - az);
85
+ if (d > EPS) return null; // a vertex in front of a face → concave
86
+ }
87
+ boundary.set(k, { a: fc.a, b: fc.b, c: fc.c });
88
+ }
89
+ if (boundaryCount < 4) return null; // not a closed volume
90
+
91
+ // Coplanar-merged hull piece + a linear-scan support over its local verts.
92
+ const piece = build_piece(tp, boundary);
93
+ const v = piece.vertices;
94
+ const vc = v.length / 3;
95
+ piece.support = function (result, result_offset, dx, dy, dz) {
96
+ let bi = 0, best = -Infinity;
97
+ for (let i = 0; i < vc; i++) {
98
+ const d = dx * v[i * 3] + dy * v[i * 3 + 1] + dz * v[i * 3 + 2];
99
+ if (d > best) { best = d; bi = i; }
100
+ }
101
+ result[result_offset] = v[bi * 3];
102
+ result[result_offset + 1] = v[bi * 3 + 1];
103
+ result[result_offset + 2] = v[bi * 3 + 2];
104
+ };
105
+ return piece;
106
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Append the mesh-vs-mesh contacts via `append`, the narrowphase's
3
+ * `append_contact(count, wax,way,waz, wbx,wby,wbz, nx,ny,nz, depth, fid) → newCount`.
4
+ *
5
+ * @returns {number} new candidate count
6
+ */
7
+ export function mesh_mesh_tet_contacts(count: any, shapeA: any, trA: any, shapeB: any, trB: any, append: any): number;
8
+ //# sourceMappingURL=mesh_mesh_tet_manifold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mesh_mesh_tet_manifold.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/mesh_mesh_tet_manifold.js"],"names":[],"mappings":"AAkDA;;;;;GAKG;AACH,+GAFa,MAAM,CA8DlB"}
@@ -0,0 +1,117 @@
1
+ import { bvh_query_user_data_overlaps_aabb } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js";
2
+ import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
3
+ import { aabb_world_to_local } from "./decomposition/aabb_world_to_local.js";
4
+ import { build_convex_decomposition } from "./convex_decomposition.js";
5
+ import { convex_convex_manifold, CONVEX_CONVEX_OUT_LENGTH } from "./convex_convex_manifold.js";
6
+
7
+ /**
8
+ * Contact generation between two triangulated meshes, each treated as the union
9
+ * of its CONVEX PIECES (from greedy tet-merge — see {@link build_convex_decomposition}).
10
+ * Concave-vs-concave reduced to convex piece-vs-piece, the standard
11
+ * convex-decomposition approach.
12
+ *
13
+ * Only piece pairs whose AABBs overlap reach the SAT + face-clip manifold:
14
+ * project mesh B's world AABB into A's local frame and query A's piece BVH; for
15
+ * each candidate piece of A, project its world AABB into B's local frame and
16
+ * query B's piece BVH. One top-level broadphase leaf per body; the per-shape
17
+ * piece BVHs do the culling. Merging keeps the piece count (and the per-pair
18
+ * SAT cost) far below the raw tet count — the path to large tet counts.
19
+ *
20
+ * The decomposition is built once per shape and cached (shapes are immutable,
21
+ * shared, and the decomposition is in the shape's local frame). Per-frame, no
22
+ * cross-frame state → reset-and-resimulate determinism preserved.
23
+ *
24
+ * @author Alex Goldring
25
+ * @copyright Company Named Limited (c) 2026
26
+ */
27
+
28
+ /** Stop emitting once the candidate buffer is nearly full (capped at 4
29
+ * downstream); a localised resting contact never approaches this. */
30
+ const MAX_EMIT = 60;
31
+
32
+ const piece_out = new Float64Array(CONVEX_CONVEX_OUT_LENGTH);
33
+ let cand_a = new Uint32Array(4096);
34
+ let cand_b = new Uint32Array(4096);
35
+ const local_bbox = new Float64Array(6);
36
+ const world_aabb = new Float64Array(6);
37
+ const query_aabb = new Float64Array(6);
38
+
39
+ /** Build + cache the convex decomposition on the (immutable, shared) shape. */
40
+ function get_decomposition(shape) {
41
+ let d = shape.__convex_decomposition;
42
+ if (d === undefined) {
43
+ d = build_convex_decomposition(shape);
44
+ shape.__convex_decomposition = d;
45
+ }
46
+ return d;
47
+ }
48
+
49
+ function ensure(buf, n) { return buf.length >= n ? buf : new Uint32Array(n); }
50
+
51
+ /**
52
+ * Append the mesh-vs-mesh contacts via `append`, the narrowphase's
53
+ * `append_contact(count, wax,way,waz, wbx,wby,wbz, nx,ny,nz, depth, fid) → newCount`.
54
+ *
55
+ * @returns {number} new candidate count
56
+ */
57
+ export function mesh_mesh_tet_contacts(count, shapeA, trA, shapeB, trB, append) {
58
+ const decompA = get_decomposition(shapeA);
59
+ const decompB = get_decomposition(shapeB);
60
+ const piecesA = decompA.pieces;
61
+ const piecesB = decompB.pieces;
62
+
63
+ // Candidate pieces of A: those whose AABB overlaps B's world AABB (in A-local).
64
+ shapeB.compute_bounding_box(local_bbox);
65
+ aabb3_transform_oriented(
66
+ world_aabb, 0,
67
+ local_bbox[0], local_bbox[1], local_bbox[2], local_bbox[3], local_bbox[4], local_bbox[5],
68
+ trB.position.x, trB.position.y, trB.position.z,
69
+ trB.rotation.x, trB.rotation.y, trB.rotation.z, trB.rotation.w
70
+ );
71
+ aabb_world_to_local(
72
+ query_aabb, 0, world_aabb,
73
+ trA.position.x, trA.position.y, trA.position.z,
74
+ trA.rotation.x, trA.rotation.y, trA.rotation.z, trA.rotation.w
75
+ );
76
+
77
+ cand_a = ensure(cand_a, piecesA.length);
78
+ const nA = bvh_query_user_data_overlaps_aabb(cand_a, 0, decompA.bvh, query_aabb);
79
+
80
+ for (let i = 0; i < nA; i++) {
81
+ const pieceA = piecesA[cand_a[i]];
82
+
83
+ // Candidate pieces of B near this piece of A.
84
+ const ab = pieceA.aabb;
85
+ aabb3_transform_oriented(
86
+ world_aabb, 0,
87
+ ab[0], ab[1], ab[2], ab[3], ab[4], ab[5],
88
+ trA.position.x, trA.position.y, trA.position.z,
89
+ trA.rotation.x, trA.rotation.y, trA.rotation.z, trA.rotation.w
90
+ );
91
+ aabb_world_to_local(
92
+ query_aabb, 0, world_aabb,
93
+ trB.position.x, trB.position.y, trB.position.z,
94
+ trB.rotation.x, trB.rotation.y, trB.rotation.z, trB.rotation.w
95
+ );
96
+ cand_b = ensure(cand_b, piecesB.length);
97
+ const nB = bvh_query_user_data_overlaps_aabb(cand_b, 0, decompB.bvh, query_aabb);
98
+
99
+ for (let j = 0; j < nB; j++) {
100
+ const pieceB = piecesB[cand_b[j]];
101
+ if (!convex_convex_manifold(piece_out, pieceA, trA.position, trA.rotation, pieceB, trB.position, trB.rotation)) {
102
+ continue;
103
+ }
104
+ const cc = piece_out[3] | 0;
105
+ const nx = piece_out[0], ny = piece_out[1], nz = piece_out[2];
106
+ for (let k = 0; k < cc; k++) {
107
+ const base = 4 + k * 7;
108
+ count = append(count,
109
+ piece_out[base], piece_out[base + 1], piece_out[base + 2],
110
+ piece_out[base + 3], piece_out[base + 4], piece_out[base + 5],
111
+ nx, ny, nz, piece_out[base + 6], 0);
112
+ }
113
+ if (count >= MAX_EMIT) return count;
114
+ }
115
+ }
116
+ return count;
117
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"AA8zCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qDAVW,YAAY,GAAC,MAAM,EAAE,iCAGrB;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,QAC5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,iCAErC;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,QAC5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,GACnC,MAAM,CAyClB;AAED;;;;;;;;;;;GAWG;AACH,uFALW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,CAAC,QA4KlE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,uEAJW,MAAM,UACN,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,UACjD,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,QAkH3D"}
1
+ {"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"AAk1CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qDAVW,YAAY,GAAC,MAAM,EAAE,iCAGrB;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,QAC5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,iCAErC;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,QAC5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,GACnC,MAAM,CAuClB;AAED;;;;;;;;;;;GAWG;AACH,uFALW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,CAAC,QAiKlE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,uEAJW,MAAM,UACN,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,UACjD,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,QA8G3D"}