@woosh/meep-engine 2.144.0 → 2.146.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 (60) hide show
  1. package/package.json +1 -1
  2. package/src/core/bvh2/bvh3/BVH.d.ts.map +1 -1
  3. package/src/core/bvh2/bvh3/BVH.js +158 -4
  4. package/src/core/geom/3d/shape/CylinderShape3D.d.ts +56 -0
  5. package/src/core/geom/3d/shape/CylinderShape3D.d.ts.map +1 -0
  6. package/src/core/geom/3d/shape/CylinderShape3D.js +223 -0
  7. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +33 -3
  8. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
  9. package/src/core/geom/3d/shape/HeightMapShape3D.js +486 -451
  10. package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
  11. package/src/core/geom/3d/shape/json/shape_to_type.js +3 -0
  12. package/src/core/geom/3d/shape/json/type_adapters.d.ts +15 -0
  13. package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
  14. package/src/core/geom/3d/shape/json/type_adapters.js +16 -0
  15. package/src/engine/control/first-person/DESIGN_COLLISION.md +365 -302
  16. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +1 -3
  17. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  18. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +12 -2
  19. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  20. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +7 -2
  21. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +13 -0
  22. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  23. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +16 -2
  24. package/src/engine/control/first-person/TODO.md +13 -11
  25. package/src/engine/control/first-person/abilities/WallJump.d.ts.map +1 -1
  26. package/src/engine/control/first-person/abilities/WallJump.js +11 -3
  27. package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
  28. package/src/engine/control/first-person/abilities/WallRun.js +30 -35
  29. package/src/engine/control/first-person/collision/KinematicMover.d.ts +35 -5
  30. package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
  31. package/src/engine/control/first-person/collision/KinematicMover.js +634 -424
  32. package/src/engine/control/first-person/prototype_first_person_controller.js +1003 -901
  33. package/src/engine/physics/PLAN.md +943 -767
  34. package/src/engine/physics/body/BodyStorage.d.ts +9 -0
  35. package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
  36. package/src/engine/physics/body/BodyStorage.js +23 -0
  37. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  38. package/src/engine/physics/broadphase/generate_pairs.js +7 -0
  39. package/src/engine/physics/ccd/linear_sweep.d.ts +97 -0
  40. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -0
  41. package/src/engine/physics/ccd/linear_sweep.js +238 -0
  42. package/src/engine/physics/ecs/PhysicsSystem.d.ts +18 -3
  43. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  44. package/src/engine/physics/ecs/PhysicsSystem.js +59 -8
  45. package/src/engine/physics/ecs/RigidBodyFlags.d.ts +6 -0
  46. package/src/engine/physics/ecs/RigidBodyFlags.d.ts.map +1 -1
  47. package/src/engine/physics/ecs/RigidBodyFlags.js +6 -0
  48. package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -811
  49. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
  50. package/src/engine/physics/narrowphase/compute_penetration.js +325 -323
  51. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +27 -8
  52. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -1
  53. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +235 -204
  54. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  55. package/src/engine/physics/narrowphase/narrowphase_step.js +70 -13
  56. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  57. package/src/engine/physics/queries/overlap_shape.js +185 -183
  58. package/src/engine/simulation/Ticker.d.ts +14 -0
  59. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  60. package/src/engine/simulation/Ticker.js +136 -1
@@ -1,811 +1,811 @@
1
- import { line3_closest_points_segment_segment } from "../../../core/geom/3d/line/line3_closest_points_segment_segment.js";
2
- import { quat3_to_matrix3 } from "../../../core/geom/3d/quaternion/quat3_to_matrix3.js";
3
-
4
- /**
5
- * Multi-point manifold construction for an oriented box vs. a triangle.
6
- *
7
- * Algorithm (Parry's `contact_manifolds_cuboid_triangle.rs` as the
8
- * blueprint):
9
- *
10
- * 1. SAT over 13 candidate axes — 3 box face normals + 1 triangle face
11
- * normal + 9 edge-edge cross products. The triangle's projection
12
- * onto an axis is NOT symmetric around its centroid (unlike the
13
- * box's), so the per-axis MTV is computed via the asymmetric
14
- * `min(push_pos, push_neg)` form rather than the box-box-style
15
- * `(rA + rB) - dist`.
16
- *
17
- * 2. Branch by winning-axis source:
18
- *
19
- * a. Box face axis (sources 0..2) — reference face = box face along
20
- * winning axis, incident polygon = triangle. Project the triangle
21
- * into the reference face's (u, v) basis, clip against the
22
- * rectangle |u| ≤ half_u, |v| ≤ half_v via Sutherland-Hodgman
23
- * with 4 axis-aligned passes. For each surviving (u, v) point,
24
- * recover its world position on the triangle plane.
25
- *
26
- * b. Triangle face axis (source 3) — reference = triangle, incident
27
- * = box face most antiparallel to the contact normal. Project
28
- * the box face's 4 corners into a 2D basis on the triangle
29
- * plane, clip against the triangle's 3 edges (3 general
30
- * half-plane passes). Recover world positions on the box face
31
- * plane.
32
- *
33
- * c. Edge-cross axis (sources 4..12) — single contact at the
34
- * closest pair of points on the relevant box edge and triangle
35
- * edge (via {@link line3_closest_points_segment_segment}).
36
- *
37
- * 3. Reduce surviving contacts to at most {@link MAX_CONTACTS} by
38
- * deepest-first then perimeter expansion.
39
- *
40
- * Contact convention: `out[0..2]` is the world normal pointing from the
41
- * triangle's surface toward the box's centre (the direction the box
42
- * should be pushed to separate from the triangle). The caller in
43
- * `narrowphase_step.js` swaps to the "B → A" convention as needed.
44
- *
45
- * Output layout (mirrors {@link box_box_manifold}):
46
- * out[0..2] : world normal (triangle → box)
47
- * out[3] : contact count
48
- * out[4 + k*7 + 0..2] : world contact on triangle surface
49
- * out[4 + k*7 + 3..5] : world contact on box surface
50
- * out[4 + k*7 + 6] : penetration depth (positive)
51
- *
52
- * @author Alex Goldring
53
- * @copyright Company Named Limited (c) 2026
54
- */
55
-
56
- const MAX_CONTACTS = 4;
57
- const CONTACT_STRIDE = 7;
58
- const PARALLEL_EPS_SQR = 1e-8;
59
-
60
- /**
61
- * Length of `out` required by {@link box_triangle_contact}.
62
- * @type {number}
63
- */
64
- export const BOX_TRIANGLE_OUT_LENGTH = 4 + MAX_CONTACTS * CONTACT_STRIDE;
65
-
66
- // --- scratch storage (allocation-free across calls) ----------------------------
67
-
68
- const scratch_axes = new Float64Array(9);
69
-
70
- // Triangle projected to the box face's uv basis (start with 3 points;
71
- // after axis-aligned clipping can grow to at most 7 points).
72
- const tri_uv_in = new Float64Array(8 * 2);
73
- const tri_uv_out = new Float64Array(8 * 2);
74
-
75
- // Box face corners (for the triangle-axis-winner branch).
76
- const box_face_corners_uv_in = new Float64Array(8 * 2);
77
- const box_face_corners_uv_out = new Float64Array(8 * 2);
78
-
79
- // Surviving contact candidates: stride 7 (3 world-tri + 3 world-box + 1 depth).
80
- const candidates = new Float64Array(8 * 7);
81
-
82
- // Closest-points scratch for the edge-cross-winner branch.
83
- const closest_pair_st = new Float64Array(2);
84
-
85
- // --- helpers -----------------------------------------------------------------
86
-
87
- /**
88
- * Half-extent of the box projected onto a unit world-axis `(ux, uy, uz)`,
89
- * given the box's world-space axes and half-extents.
90
- */
91
- function projected_box_half_extent(axes, hx, hy, hz, ux, uy, uz) {
92
- const px = ux * axes[0] + uy * axes[1] + uz * axes[2];
93
- const py = ux * axes[3] + uy * axes[4] + uz * axes[5];
94
- const pz = ux * axes[6] + uy * axes[7] + uz * axes[8];
95
- return (px < 0 ? -px : px) * hx
96
- + (py < 0 ? -py : py) * hy
97
- + (pz < 0 ? -pz : pz) * hz;
98
- }
99
-
100
- /**
101
- * Sutherland-Hodgman clip of `points_in` against the axis-aligned
102
- * half-plane `coord_idx ≤ bound` (or `coord_idx ≥ bound` when
103
- * `keep_below` is false). 2D stride 2; result to `points_out`.
104
- * Returns surviving vertex count.
105
- */
106
- function clip_against_axis_uv(points_in, point_count, points_out, coord_idx, bound, keep_below) {
107
- let out_count = 0;
108
- for (let i = 0; i < point_count; i++) {
109
- const j = (i + 1) % point_count;
110
- const ax = points_in[i * 2], ay = points_in[i * 2 + 1];
111
- const bx = points_in[j * 2], by = points_in[j * 2 + 1];
112
- const av = coord_idx === 0 ? ax : ay;
113
- const bv = coord_idx === 0 ? bx : by;
114
- const a_inside = keep_below ? (av <= bound) : (av >= bound);
115
- const b_inside = keep_below ? (bv <= bound) : (bv >= bound);
116
-
117
- if (a_inside) {
118
- points_out[out_count * 2] = ax;
119
- points_out[out_count * 2 + 1] = ay;
120
- out_count++;
121
- }
122
- if (a_inside !== b_inside) {
123
- const denom = bv - av;
124
- const t = denom !== 0 ? (bound - av) / denom : 0;
125
- points_out[out_count * 2] = ax + (bx - ax) * t;
126
- points_out[out_count * 2 + 1] = ay + (by - ay) * t;
127
- out_count++;
128
- }
129
- }
130
- return out_count;
131
- }
132
-
133
- /**
134
- * Sutherland-Hodgman clip of `points_in` against the general half-plane
135
- * `(p - (line_ox, line_oy)) · (line_nx, line_ny) ≤ 0`. 2D stride 2;
136
- * result to `points_out`. Returns surviving vertex count.
137
- */
138
- function clip_against_half_plane_uv(points_in, point_count, points_out, line_ox, line_oy, line_nx, line_ny) {
139
- let out_count = 0;
140
- for (let i = 0; i < point_count; i++) {
141
- const j = (i + 1) % point_count;
142
- const ax = points_in[i * 2], ay = points_in[i * 2 + 1];
143
- const bx = points_in[j * 2], by = points_in[j * 2 + 1];
144
- const a_val = (ax - line_ox) * line_nx + (ay - line_oy) * line_ny;
145
- const b_val = (bx - line_ox) * line_nx + (by - line_oy) * line_ny;
146
- const a_inside = a_val <= 0;
147
- const b_inside = b_val <= 0;
148
-
149
- if (a_inside) {
150
- points_out[out_count * 2] = ax;
151
- points_out[out_count * 2 + 1] = ay;
152
- out_count++;
153
- }
154
- if (a_inside !== b_inside) {
155
- const denom = a_val - b_val;
156
- const t = denom !== 0 ? a_val / denom : 0;
157
- points_out[out_count * 2] = ax + (bx - ax) * t;
158
- points_out[out_count * 2 + 1] = ay + (by - ay) * t;
159
- out_count++;
160
- }
161
- }
162
- return out_count;
163
- }
164
-
165
- /**
166
- * In-place reduction of `candidates` (stride 7: x, y, z on triangle,
167
- * x, y, z on box, depth) to at most {@link MAX_CONTACTS} entries.
168
- * Strategy: keep the deepest, then iteratively pick the candidate
169
- * whose minimum distance to the already-kept set (in 3D space) is
170
- * largest — approximates max-perimeter / max-area selection.
171
- */
172
- function reduce_contacts(n) {
173
- if (n <= MAX_CONTACTS) return n;
174
-
175
- function swap(i, j) {
176
- if (i === j) return;
177
- const oi = i * 7;
178
- const oj = j * 7;
179
- for (let k = 0; k < 7; k++) {
180
- const t = candidates[oi + k];
181
- candidates[oi + k] = candidates[oj + k];
182
- candidates[oj + k] = t;
183
- }
184
- }
185
-
186
- // Move deepest to slot 0.
187
- let deepest_idx = 0;
188
- let deepest_val = candidates[6];
189
- for (let i = 1; i < n; i++) {
190
- const d = candidates[i * 7 + 6];
191
- if (d > deepest_val) { deepest_val = d; deepest_idx = i; }
192
- }
193
- swap(0, deepest_idx);
194
-
195
- for (let k = 1; k < MAX_CONTACTS; k++) {
196
- let best_score = -1;
197
- let best_i = -1;
198
- for (let i = k; i < n; i++) {
199
- let min_d2 = Infinity;
200
- for (let j = 0; j < k; j++) {
201
- const dx = candidates[i * 7] - candidates[j * 7];
202
- const dy = candidates[i * 7 + 1] - candidates[j * 7 + 1];
203
- const dz = candidates[i * 7 + 2] - candidates[j * 7 + 2];
204
- const d2 = dx * dx + dy * dy + dz * dz;
205
- if (d2 < min_d2) min_d2 = d2;
206
- }
207
- if (min_d2 > best_score) { best_score = min_d2; best_i = i; }
208
- }
209
- swap(k, best_i);
210
- }
211
- return MAX_CONTACTS;
212
- }
213
-
214
- // --- main --------------------------------------------------------------------
215
-
216
- /**
217
- * @param {number[]|Float64Array} out length >= {@link BOX_TRIANGLE_OUT_LENGTH}
218
- * @param {number} bcx box centre x (world)
219
- * @param {number} bcy
220
- * @param {number} bcz
221
- * @param {number} bqx box quaternion x
222
- * @param {number} bqy
223
- * @param {number} bqz
224
- * @param {number} bqw
225
- * @param {number} bhx box half-extent x (body frame)
226
- * @param {number} bhy
227
- * @param {number} bhz
228
- * @param {number} ax triangle vertex A x (world)
229
- * @param {number} ay
230
- * @param {number} az
231
- * @param {number} bx triangle vertex B x (world)
232
- * @param {number} by
233
- * @param {number} bz
234
- * @param {number} cx triangle vertex C x (world)
235
- * @param {number} cy
236
- * @param {number} cz
237
- * @returns {boolean} true if box and triangle overlap
238
- */
239
- export function box_triangle_contact(
240
- out,
241
- bcx, bcy, bcz, bqx, bqy, bqz, bqw, bhx, bhy, bhz,
242
- ax, ay, az, bx, by, bz, cx, cy, cz
243
- ) {
244
- quat3_to_matrix3(scratch_axes, 0, bqx, bqy, bqz, bqw);
245
- const axes = scratch_axes;
246
-
247
- // Triangle edges in world.
248
- const ab_x = bx - ax, ab_y = by - ay, ab_z = bz - az;
249
- const bc_x = cx - bx, bc_y = cy - by, bc_z = cz - bz;
250
- const ca_x = ax - cx, ca_y = ay - cy, ca_z = az - cz;
251
-
252
- // Triangle face normal in world via (B-A) × (C-A).
253
- const e2x = cx - ax, e2y = cy - ay, e2z = cz - az;
254
- const tnx_raw = ab_y * e2z - ab_z * e2y;
255
- const tny_raw = ab_z * e2x - ab_x * e2z;
256
- const tnz_raw = ab_x * e2y - ab_y * e2x;
257
- const tn_mag_sqr = tnx_raw * tnx_raw + tny_raw * tny_raw + tnz_raw * tnz_raw;
258
- if (tn_mag_sqr <= PARALLEL_EPS_SQR) {
259
- // Degenerate triangle (zero area). No sensible contact.
260
- out[3] = 0;
261
- return false;
262
- }
263
- const tn_inv = 1 / Math.sqrt(tn_mag_sqr);
264
- const tnx = tnx_raw * tn_inv;
265
- const tny = tny_raw * tn_inv;
266
- const tnz = tnz_raw * tn_inv;
267
-
268
- // Triangle centroid for sign-canonicalisation of the contact normal.
269
- const tcx = (ax + bx + cx) / 3;
270
- const tcy = (ay + by + cy) / 3;
271
- const tcz = (az + bz + cz) / 3;
272
- // Vector from triangle centroid toward box centre — used to flip the
273
- // SAT axis so the stored normal consistently points triangle → box.
274
- const d_t2b_x = bcx - tcx;
275
- const d_t2b_y = bcy - tcy;
276
- const d_t2b_z = bcz - tcz;
277
-
278
- // SAT state.
279
- let best_overlap = Infinity;
280
- let best_nx = 0, best_ny = 0, best_nz = 0; // points triangle → box
281
- let best_source = -1;
282
- // Box-edge index (0..2) and triangle-edge index (0..2) for edge-cross sources.
283
- let best_box_edge_idx = -1;
284
- let best_tri_edge_idx = -1;
285
-
286
- /**
287
- * Test a candidate separating axis `(lx, ly, lz)` (need not be unit-length).
288
- * Returns true if a separating axis is found (overlap < 0); otherwise
289
- * updates `best_*` if this axis has the smallest overlap so far.
290
- *
291
- * Source:
292
- * 0..2 : box face normal (axis index 0..2)
293
- * 3 : triangle face normal
294
- * 4..12 : edge-cross (box_edge_idx * 3 + tri_edge_idx, offset +4)
295
- */
296
- function test_axis(lx, ly, lz, source, box_e_idx, tri_e_idx) {
297
- const len_sqr = lx * lx + ly * ly + lz * lz;
298
- if (len_sqr <= PARALLEL_EPS_SQR) return false;
299
- const inv_len = 1 / Math.sqrt(len_sqr);
300
- const ux = lx * inv_len, uy = ly * inv_len, uz = lz * inv_len;
301
-
302
- // Box projection.
303
- const box_centre_proj = ux * bcx + uy * bcy + uz * bcz;
304
- const r_box = projected_box_half_extent(axes, bhx, bhy, bhz, ux, uy, uz);
305
- const bmin = box_centre_proj - r_box;
306
- const bmax = box_centre_proj + r_box;
307
-
308
- // Triangle projection — NOT symmetric around centroid, so we take
309
- // the per-vertex extreme values.
310
- const da = ux * ax + uy * ay + uz * az;
311
- const db = ux * bx + uy * by + uz * bz;
312
- const dc = ux * cx + uy * cy + uz * cz;
313
- const tmin = da < db ? (da < dc ? da : dc) : (db < dc ? db : dc);
314
- const tmax = da > db ? (da > dc ? da : dc) : (db > dc ? db : dc);
315
-
316
- // Separation test.
317
- if (bmax < tmin || tmax < bmin) return true;
318
-
319
- // Compute MTV magnitude as the smaller of the two "push directions".
320
- // push_pos: how far we'd push the triangle in +u to escape.
321
- // push_neg: how far in -u.
322
- // For asymmetric intervals these are not equal in general.
323
- const push_pos = bmax - tmin;
324
- const push_neg = tmax - bmin;
325
- const overlap = push_pos < push_neg ? push_pos : push_neg;
326
-
327
- if (overlap < best_overlap) {
328
- best_overlap = overlap;
329
- // Canonical normal direction: triangle → box. Use the
330
- // box-centre-vs-triangle-centroid sign as the tiebreaker so
331
- // the SAT axis is canonically oriented before we negate.
332
- const center_dot = d_t2b_x * ux + d_t2b_y * uy + d_t2b_z * uz;
333
- const sign = center_dot >= 0 ? 1 : -1;
334
- best_nx = ux * sign;
335
- best_ny = uy * sign;
336
- best_nz = uz * sign;
337
- best_source = source;
338
- best_box_edge_idx = box_e_idx;
339
- best_tri_edge_idx = tri_e_idx;
340
- }
341
- return false;
342
- }
343
-
344
- // 1. Box face normals (3 axes).
345
- if (test_axis(axes[0], axes[1], axes[2], 0, -1, -1)) { out[3] = 0; return false; }
346
- if (test_axis(axes[3], axes[4], axes[5], 1, -1, -1)) { out[3] = 0; return false; }
347
- if (test_axis(axes[6], axes[7], axes[8], 2, -1, -1)) { out[3] = 0; return false; }
348
-
349
- // 2. Triangle face normal (1 axis).
350
- if (test_axis(tnx, tny, tnz, 3, -1, -1)) { out[3] = 0; return false; }
351
-
352
- // 3. Edge-edge cross products (9 axes).
353
- for (let i = 0; i < 3; i++) {
354
- const aix = axes[i * 3], aiy = axes[i * 3 + 1], aiz = axes[i * 3 + 2];
355
- for (let j = 0; j < 3; j++) {
356
- let ex, ey, ez;
357
- if (j === 0) { ex = ab_x; ey = ab_y; ez = ab_z; }
358
- else if (j === 1) { ex = bc_x; ey = bc_y; ez = bc_z; }
359
- else { ex = ca_x; ey = ca_y; ez = ca_z; }
360
- const cx_ax = aiy * ez - aiz * ey;
361
- const cy_ax = aiz * ex - aix * ez;
362
- const cz_ax = aix * ey - aiy * ex;
363
- if (test_axis(cx_ax, cy_ax, cz_ax, 4 + i * 3 + j, i, j)) { out[3] = 0; return false; }
364
- }
365
- }
366
-
367
- // We have overlap. Output the contact normal (triangle → box).
368
- const nx = best_nx, ny = best_ny, nz = best_nz;
369
- out[0] = nx; out[1] = ny; out[2] = nz;
370
-
371
- // --- Contact manifold construction ---
372
-
373
- if (best_source < 3) {
374
- // Box face axis winner: reference = box face, incident = triangle.
375
- return emit_box_face_manifold(out, best_source, axes, bcx, bcy, bcz, bhx, bhy, bhz,
376
- ax, ay, az, bx, by, bz, cx, cy, cz, tnx, tny, tnz, nx, ny, nz);
377
- } else if (best_source === 3) {
378
- // Triangle face axis winner: reference = triangle, incident = box face.
379
- return emit_triangle_face_manifold(out, axes, bcx, bcy, bcz, bhx, bhy, bhz,
380
- ax, ay, az, bx, by, bz, cx, cy, cz, tnx, tny, tnz, nx, ny, nz);
381
- } else {
382
- // Edge-cross winner: single contact at closest pair on the two edges.
383
- return emit_edge_cross_contact(out, axes, bcx, bcy, bcz, bhx, bhy, bhz,
384
- ax, ay, az, bx, by, bz, cx, cy, cz,
385
- best_box_edge_idx, best_tri_edge_idx,
386
- nx, ny, nz, best_overlap);
387
- }
388
- }
389
-
390
- // --- Manifold builders ------------------------------------------------------
391
-
392
- /**
393
- * Box-face SAT winner: clip the triangle against the box's reference face
394
- * rectangle, then for each surviving (u, v) recover the world contact on
395
- * the triangle plane.
396
- */
397
- function emit_box_face_manifold(
398
- out, ref_axis_idx, axes,
399
- bcx, bcy, bcz, bhx, bhy, bhz,
400
- ax, ay, az, bx, by, bz, cx, cy, cz,
401
- tnx, tny, tnz,
402
- nx, ny, nz
403
- ) {
404
- // The contact normal `n` points triangle → box. The box's outward
405
- // face normal at the reference face points box → triangle = -n.
406
- const face_out_nx = -nx;
407
- const face_out_ny = -ny;
408
- const face_out_nz = -nz;
409
-
410
- // Sign of the reference axis aligned with the outward normal.
411
- const ref_axis_x = axes[ref_axis_idx * 3];
412
- const ref_axis_y = axes[ref_axis_idx * 3 + 1];
413
- const ref_axis_z = axes[ref_axis_idx * 3 + 2];
414
- const ref_axis_dot = ref_axis_x * face_out_nx + ref_axis_y * face_out_ny + ref_axis_z * face_out_nz;
415
- const ref_axis_sign = ref_axis_dot >= 0 ? 1 : -1;
416
- const ref_h_along = ref_axis_idx === 0 ? bhx : (ref_axis_idx === 1 ? bhy : bhz);
417
-
418
- // Reference face origin (centre of the face on the +/- side of the box).
419
- const ref_face_ox = bcx + ref_axis_x * ref_axis_sign * ref_h_along;
420
- const ref_face_oy = bcy + ref_axis_y * ref_axis_sign * ref_h_along;
421
- const ref_face_oz = bcz + ref_axis_z * ref_axis_sign * ref_h_along;
422
-
423
- // Reference face's two tangent axes (u, v) and their half-extents.
424
- let u_axis_idx, v_axis_idx;
425
- if (ref_axis_idx === 0) { u_axis_idx = 1; v_axis_idx = 2; }
426
- else if (ref_axis_idx === 1) { u_axis_idx = 2; v_axis_idx = 0; }
427
- else { u_axis_idx = 0; v_axis_idx = 1; }
428
- const ux = axes[u_axis_idx * 3], uy = axes[u_axis_idx * 3 + 1], uz = axes[u_axis_idx * 3 + 2];
429
- const vx = axes[v_axis_idx * 3], vy = axes[v_axis_idx * 3 + 1], vz = axes[v_axis_idx * 3 + 2];
430
- const half_u = u_axis_idx === 0 ? bhx : (u_axis_idx === 1 ? bhy : bhz);
431
- const half_v = v_axis_idx === 0 ? bhx : (v_axis_idx === 1 ? bhy : bhz);
432
-
433
- // Project the triangle's 3 vertices into the (u, v) basis on the
434
- // reference plane.
435
- const da_x = ax - ref_face_ox, da_y = ay - ref_face_oy, da_z = az - ref_face_oz;
436
- const db_x = bx - ref_face_ox, db_y = by - ref_face_oy, db_z = bz - ref_face_oz;
437
- const dc_x = cx - ref_face_ox, dc_y = cy - ref_face_oy, dc_z = cz - ref_face_oz;
438
- tri_uv_in[0] = da_x * ux + da_y * uy + da_z * uz;
439
- tri_uv_in[1] = da_x * vx + da_y * vy + da_z * vz;
440
- tri_uv_in[2] = db_x * ux + db_y * uy + db_z * uz;
441
- tri_uv_in[3] = db_x * vx + db_y * vy + db_z * vz;
442
- tri_uv_in[4] = dc_x * ux + dc_y * uy + dc_z * uz;
443
- tri_uv_in[5] = dc_x * vx + dc_y * vy + dc_z * vz;
444
-
445
- // Clip against rectangle |u| ≤ half_u, |v| ≤ half_v via 4 passes.
446
- let n = 3;
447
- n = clip_against_axis_uv(tri_uv_in, n, tri_uv_out, 0, half_u, true);
448
- if (n === 0) { out[3] = 0; return false; }
449
- n = clip_against_axis_uv(tri_uv_out, n, tri_uv_in, 0, -half_u, false);
450
- if (n === 0) { out[3] = 0; return false; }
451
- n = clip_against_axis_uv(tri_uv_in, n, tri_uv_out, 1, half_v, true);
452
- if (n === 0) { out[3] = 0; return false; }
453
- n = clip_against_axis_uv(tri_uv_out, n, tri_uv_in, 1, -half_v, false);
454
- if (n === 0) { out[3] = 0; return false; }
455
-
456
- // The clipped polygon is in tri_uv_in. For each surviving (u, v):
457
- // X = ref_face_origin + u * u_axis + v * v_axis (point on ref plane)
458
- // Find t such that (X + t * face_out_n) is on the triangle plane:
459
- // (X + t * face_out_n - A) · tn = 0
460
- // t = ((A - X) · tn) / (face_out_n · tn)
461
- // World point on triangle plane = X + t * face_out_n.
462
- // Depth = -t (positive when triangle penetrates the box).
463
- const denom = face_out_nx * tnx + face_out_ny * tny + face_out_nz * tnz;
464
- if (Math.abs(denom) < PARALLEL_EPS_SQR) {
465
- // Triangle plane parallel to ref outward — degenerate. No contacts.
466
- out[3] = 0;
467
- return false;
468
- }
469
- const clipped_uv = tri_uv_in;
470
-
471
- let cand_count = 0;
472
- for (let i = 0; i < n; i++) {
473
- const u = clipped_uv[i * 2];
474
- const v = clipped_uv[i * 2 + 1];
475
- const xpx = ref_face_ox + u * ux + v * vx;
476
- const xpy = ref_face_oy + u * uy + v * vy;
477
- const xpz = ref_face_oz + u * uz + v * vz;
478
-
479
- const t = ((ax - xpx) * tnx + (ay - xpy) * tny + (az - xpz) * tnz) / denom;
480
- const world_tri_x = xpx + face_out_nx * t;
481
- const world_tri_y = xpy + face_out_ny * t;
482
- const world_tri_z = xpz + face_out_nz * t;
483
- const depth = -t;
484
- if (depth <= 0) continue;
485
-
486
- const off = cand_count * 7;
487
- candidates[off] = world_tri_x;
488
- candidates[off + 1] = world_tri_y;
489
- candidates[off + 2] = world_tri_z;
490
- // Box-side contact = same (u, v) on ref plane.
491
- candidates[off + 3] = xpx;
492
- candidates[off + 4] = xpy;
493
- candidates[off + 5] = xpz;
494
- candidates[off + 6] = depth;
495
- cand_count++;
496
- }
497
-
498
- if (cand_count === 0) { out[3] = 0; return false; }
499
- const kept = reduce_contacts(cand_count);
500
- out[3] = kept;
501
- for (let k = 0; k < kept; k++) {
502
- const src = k * 7;
503
- const base = 4 + k * CONTACT_STRIDE;
504
- out[base] = candidates[src];
505
- out[base + 1] = candidates[src + 1];
506
- out[base + 2] = candidates[src + 2];
507
- out[base + 3] = candidates[src + 3];
508
- out[base + 4] = candidates[src + 4];
509
- out[base + 5] = candidates[src + 5];
510
- out[base + 6] = candidates[src + 6];
511
- }
512
- return true;
513
- }
514
-
515
- /**
516
- * Triangle-face SAT winner: reference = triangle, incident = the box face
517
- * most antiparallel to the contact normal. Clip the box face quad against
518
- * the triangle's 3 edges, then recover world contacts on the box face plane.
519
- */
520
- function emit_triangle_face_manifold(
521
- out, axes,
522
- bcx, bcy, bcz, bhx, bhy, bhz,
523
- ax, ay, az, bx, by, bz, cx, cy, cz,
524
- tnx, tny, tnz,
525
- nx, ny, nz
526
- ) {
527
- // Triangle's outward face normal points box → triangle = -n.
528
- // (Triangle is the reference, so its outward normal is the direction
529
- // the box should be pushed to separate — opposite of triangle → box.)
530
- const ref_out_x = -nx;
531
- const ref_out_y = -ny;
532
- const ref_out_z = -nz;
533
- // But we also need to ensure ref_out aligns with the triangle's CCW
534
- // face normal (tn). If tn opposes ref_out, the triangle is "facing
535
- // away" from the box — would happen on a thin-shell mesh; in that
536
- // case our SAT axis came from the FRONT face direction and ref_out
537
- // is still correct geometrically. Either way, we use the
538
- // sign-canonicalised contact normal as the source of truth.
539
-
540
- // Pick the incident box face: the one whose outward axis is most
541
- // antiparallel to ref_out.
542
- let inc_axis_idx = 0;
543
- let inc_axis_sign = 1;
544
- let max_anti = -Infinity;
545
- for (let i = 0; i < 3; i++) {
546
- const iax = axes[i * 3], iay = axes[i * 3 + 1], iaz = axes[i * 3 + 2];
547
- const d_pos = iax * ref_out_x + iay * ref_out_y + iaz * ref_out_z;
548
- if (-d_pos > max_anti) { max_anti = -d_pos; inc_axis_idx = i; inc_axis_sign = -1; }
549
- if ( d_pos > max_anti) { max_anti = d_pos; inc_axis_idx = i; inc_axis_sign = 1; }
550
- }
551
- const inc_outward_sign = -inc_axis_sign;
552
- const inc_h_along = inc_axis_idx === 0 ? bhx : (inc_axis_idx === 1 ? bhy : bhz);
553
- const inc_face_cx = bcx + axes[inc_axis_idx * 3] * inc_outward_sign * inc_h_along;
554
- const inc_face_cy = bcy + axes[inc_axis_idx * 3 + 1] * inc_outward_sign * inc_h_along;
555
- const inc_face_cz = bcz + axes[inc_axis_idx * 3 + 2] * inc_outward_sign * inc_h_along;
556
-
557
- // Box face's two tangent axes in world.
558
- let i_u_idx, i_v_idx;
559
- if (inc_axis_idx === 0) { i_u_idx = 1; i_v_idx = 2; }
560
- else if (inc_axis_idx === 1) { i_u_idx = 2; i_v_idx = 0; }
561
- else { i_u_idx = 0; i_v_idx = 1; }
562
- const iux = axes[i_u_idx * 3], iuy = axes[i_u_idx * 3 + 1], iuz = axes[i_u_idx * 3 + 2];
563
- const ivx = axes[i_v_idx * 3], ivy = axes[i_v_idx * 3 + 1], ivz = axes[i_v_idx * 3 + 2];
564
- const i_half_u = i_u_idx === 0 ? bhx : (i_u_idx === 1 ? bhy : bhz);
565
- const i_half_v = i_v_idx === 0 ? bhx : (i_v_idx === 1 ? bhy : bhz);
566
-
567
- // Build a 2D basis on the triangle plane. Use AB direction as `u`,
568
- // and `n_uv = tn × u` as `v` so the basis is orthonormal.
569
- const ab_x_w = bx - ax, ab_y_w = by - ay, ab_z_w = bz - az;
570
- const ab_len_sqr = ab_x_w * ab_x_w + ab_y_w * ab_y_w + ab_z_w * ab_z_w;
571
- if (ab_len_sqr <= PARALLEL_EPS_SQR) { out[3] = 0; return false; }
572
- const ab_inv = 1 / Math.sqrt(ab_len_sqr);
573
- const tux = ab_x_w * ab_inv, tuy = ab_y_w * ab_inv, tuz = ab_z_w * ab_inv;
574
- // v = tn × u (using the geometric face normal as the "up").
575
- const tvx = tny * tuz - tnz * tuy;
576
- const tvy = tnz * tux - tnx * tuz;
577
- const tvz = tnx * tuy - tny * tux;
578
-
579
- // Project triangle vertices to (u, v). A is the origin of the
580
- // 2D basis — A_uv = (0, 0); B is along the u axis by construction
581
- // so B_uv = (|AB|, 0); C is computed normally.
582
- const ac_x_w = cx - ax, ac_y_w = cy - ay, ac_z_w = cz - az;
583
- const b_u = ab_x_w * tux + ab_y_w * tuy + ab_z_w * tuz; // = |AB|
584
- const c_u = ac_x_w * tux + ac_y_w * tuy + ac_z_w * tuz;
585
- const c_v = ac_x_w * tvx + ac_y_w * tvy + ac_z_w * tvz;
586
-
587
- // Triangle's 3 edges as 2D half-planes in (u, v). For each edge, the
588
- // outward normal must point AWAY from the opposite vertex.
589
- // Edge AB: from A=(0,0) to B=(b_u, 0). Direction = (1, 0). Outward
590
- // normal perpendicular = (0, 1) or (0, -1). Choose sign so it
591
- // points away from C (= sign(-c_v) gives 1 if c_v < 0).
592
- const ab_n_v = c_v >= 0 ? -1 : 1;
593
- // Edge BC: from B=(b_u, 0) to C=(c_u, c_v). Direction = (c_u - b_u, c_v).
594
- // Outward perpendicular: rotate by ±90° (depending on triangle winding).
595
- // Outward direction must point away from A=(0,0).
596
- const bc_dir_u = c_u - b_u, bc_dir_v = c_v - 0;
597
- // Two perpendiculars: (-bc_dir_v, bc_dir_u) and (bc_dir_v, -bc_dir_u).
598
- // Pick the one with positive dot vs (A - midpoint of BC) negated.
599
- const bc_mid_u = (b_u + c_u) * 0.5;
600
- const bc_mid_v = c_v * 0.5;
601
- const to_a_u_from_bc = 0 - bc_mid_u;
602
- const to_a_v_from_bc = 0 - bc_mid_v;
603
- // We want outward = -direction_to_A.
604
- let bc_n_u = -bc_dir_v, bc_n_v = bc_dir_u;
605
- if (bc_n_u * to_a_u_from_bc + bc_n_v * to_a_v_from_bc > 0) {
606
- bc_n_u = -bc_n_u; bc_n_v = -bc_n_v;
607
- }
608
- // Normalise so the clip threshold is "distance ≤ 0" in 2D.
609
- const bc_n_len = Math.sqrt(bc_n_u * bc_n_u + bc_n_v * bc_n_v);
610
- if (bc_n_len > 0) { bc_n_u /= bc_n_len; bc_n_v /= bc_n_len; }
611
- // Edge CA: from C to A. Direction = (-c_u, -c_v). Outward must point
612
- // away from B.
613
- const ca_dir_u = 0 - c_u, ca_dir_v = 0 - c_v;
614
- const ca_mid_u = c_u * 0.5;
615
- const ca_mid_v = c_v * 0.5;
616
- const to_b_u_from_ca = b_u - ca_mid_u;
617
- const to_b_v_from_ca = 0 - ca_mid_v;
618
- let ca_n_u = -ca_dir_v, ca_n_v = ca_dir_u;
619
- if (ca_n_u * to_b_u_from_ca + ca_n_v * to_b_v_from_ca > 0) {
620
- ca_n_u = -ca_n_u; ca_n_v = -ca_n_v;
621
- }
622
- const ca_n_len = Math.sqrt(ca_n_u * ca_n_u + ca_n_v * ca_n_v);
623
- if (ca_n_len > 0) { ca_n_u /= ca_n_len; ca_n_v /= ca_n_len; }
624
-
625
- // Build the box-face polygon (4 corners) and project to (u, v) on
626
- // the triangle plane.
627
- const signs_u = [ 1, 1, -1, -1];
628
- const signs_v = [-1, 1, 1, -1];
629
- for (let i = 0; i < 4; i++) {
630
- const su = signs_u[i] * i_half_u;
631
- const sv = signs_v[i] * i_half_v;
632
- const wx = inc_face_cx + iux * su + ivx * sv;
633
- const wy = inc_face_cy + iuy * su + ivy * sv;
634
- const wz = inc_face_cz + iuz * su + ivz * sv;
635
- const dx = wx - ax, dy = wy - ay, dz = wz - az;
636
- box_face_corners_uv_in[i * 2] = dx * tux + dy * tuy + dz * tuz;
637
- box_face_corners_uv_in[i * 2 + 1] = dx * tvx + dy * tvy + dz * tvz;
638
- }
639
-
640
- // Clip the quad against the 3 triangle-edge half-planes.
641
- let np = 4;
642
- // Edge AB: half-plane (p.v - 0) * ab_n_v <= 0 → coord_idx = 1, bound = 0, keep_below if ab_n_v=+1.
643
- np = clip_against_half_plane_uv(box_face_corners_uv_in, np, box_face_corners_uv_out, 0, 0, 0, ab_n_v);
644
- if (np === 0) { out[3] = 0; return false; }
645
- np = clip_against_half_plane_uv(box_face_corners_uv_out, np, box_face_corners_uv_in, b_u, 0, bc_n_u, bc_n_v);
646
- if (np === 0) { out[3] = 0; return false; }
647
- np = clip_against_half_plane_uv(box_face_corners_uv_in, np, box_face_corners_uv_out, c_u, c_v, ca_n_u, ca_n_v);
648
- if (np === 0) { out[3] = 0; return false; }
649
-
650
- // Survivors are in box_face_corners_uv_out. For each:
651
- // X = A + u * tu + v * tv (point on triangle plane)
652
- // Find t such that X + t * ref_out is on box face plane:
653
- // (X + t * ref_out - inc_face_c) · inc_out = 0
654
- // Where inc_out = inc_axes[inc_axis_idx] * inc_outward_sign.
655
- const inc_out_x = axes[inc_axis_idx * 3] * inc_outward_sign;
656
- const inc_out_y = axes[inc_axis_idx * 3 + 1] * inc_outward_sign;
657
- const inc_out_z = axes[inc_axis_idx * 3 + 2] * inc_outward_sign;
658
- const denom = ref_out_x * inc_out_x + ref_out_y * inc_out_y + ref_out_z * inc_out_z;
659
- if (Math.abs(denom) < PARALLEL_EPS_SQR) { out[3] = 0; return false; }
660
- const clipped_uv = box_face_corners_uv_out;
661
-
662
- let cand_count = 0;
663
- for (let i = 0; i < np; i++) {
664
- const u = clipped_uv[i * 2];
665
- const v = clipped_uv[i * 2 + 1];
666
- const xpx = ax + u * tux + v * tvx;
667
- const xpy = ay + u * tuy + v * tvy;
668
- const xpz = az + u * tuz + v * tvz;
669
-
670
- const num_x = inc_face_cx - xpx;
671
- const num_y = inc_face_cy - xpy;
672
- const num_z = inc_face_cz - xpz;
673
- const t = (num_x * inc_out_x + num_y * inc_out_y + num_z * inc_out_z) / denom;
674
- const world_box_x = xpx + ref_out_x * t;
675
- const world_box_y = xpy + ref_out_y * t;
676
- const world_box_z = xpz + ref_out_z * t;
677
- // Depth: how far the box face has penetrated past the triangle
678
- // plane, measured along ref_out. depth = -t.
679
- const depth = -t;
680
- if (depth <= 0) continue;
681
-
682
- const off = cand_count * 7;
683
- // Triangle-side contact: the clipped (u, v) point on the
684
- // triangle plane.
685
- candidates[off] = xpx;
686
- candidates[off + 1] = xpy;
687
- candidates[off + 2] = xpz;
688
- candidates[off + 3] = world_box_x;
689
- candidates[off + 4] = world_box_y;
690
- candidates[off + 5] = world_box_z;
691
- candidates[off + 6] = depth;
692
- cand_count++;
693
- }
694
-
695
- if (cand_count === 0) { out[3] = 0; return false; }
696
- const kept = reduce_contacts(cand_count);
697
- out[3] = kept;
698
- for (let k = 0; k < kept; k++) {
699
- const src = k * 7;
700
- const base = 4 + k * CONTACT_STRIDE;
701
- out[base] = candidates[src];
702
- out[base + 1] = candidates[src + 1];
703
- out[base + 2] = candidates[src + 2];
704
- out[base + 3] = candidates[src + 3];
705
- out[base + 4] = candidates[src + 4];
706
- out[base + 5] = candidates[src + 5];
707
- out[base + 6] = candidates[src + 6];
708
- }
709
- return true;
710
- }
711
-
712
- /**
713
- * Edge-cross SAT winner: emit a single contact at the closest pair of
714
- * points on (a parallel-translated) box edge × the triangle edge.
715
- *
716
- * The box has 12 edges, but along the winning box-axis direction `i`
717
- * there are 4 parallel edges (the four lines on the box surface
718
- * parallel to box-axis i). The "right" edge for the contact is the
719
- * one whose endpoints span the triangle edge — practically, pick the
720
- * box edge most aligned with the SAT separation: the one whose centre
721
- * (when translated perpendicular to the SAT axis) is closest to the
722
- * triangle edge.
723
- *
724
- * Procedure:
725
- * 1. Construct the box edge as the line through box centre +
726
- * (the +/- of the other two box half-extents pushing to the
727
- * "near" corner determined by the SAT-axis sign).
728
- * 2. Construct the triangle edge as the segment.
729
- * 3. Closest-pair via `line3_closest_points_segment_segment`.
730
- * 4. Contact = midpoint, depth = best_overlap.
731
- */
732
- function emit_edge_cross_contact(
733
- out, axes,
734
- bcx, bcy, bcz, bhx, bhy, bhz,
735
- ax, ay, az, bx, by, bz, cx, cy, cz,
736
- box_edge_idx, tri_edge_idx,
737
- nx, ny, nz,
738
- overlap
739
- ) {
740
- // Box edge direction: axis index `box_edge_idx`.
741
- const edx = axes[box_edge_idx * 3];
742
- const edy = axes[box_edge_idx * 3 + 1];
743
- const edz = axes[box_edge_idx * 3 + 2];
744
- const edge_half = box_edge_idx === 0 ? bhx : (box_edge_idx === 1 ? bhy : bhz);
745
-
746
- // The OTHER two box axes determine which of the four parallel edges
747
- // we pick. Use the sign of the contact normal projected onto each
748
- // perpendicular axis to choose the edge closest to the contact.
749
- let u_idx, v_idx;
750
- if (box_edge_idx === 0) { u_idx = 1; v_idx = 2; }
751
- else if (box_edge_idx === 1) { u_idx = 2; v_idx = 0; }
752
- else { u_idx = 0; v_idx = 1; }
753
- const u_ax_x = axes[u_idx * 3], u_ax_y = axes[u_idx * 3 + 1], u_ax_z = axes[u_idx * 3 + 2];
754
- const v_ax_x = axes[v_idx * 3], v_ax_y = axes[v_idx * 3 + 1], v_ax_z = axes[v_idx * 3 + 2];
755
- const half_u = u_idx === 0 ? bhx : (u_idx === 1 ? bhy : bhz);
756
- const half_v = v_idx === 0 ? bhx : (v_idx === 1 ? bhy : bhz);
757
-
758
- // The contact normal `n` points triangle → box. The box edge
759
- // closest to the triangle is the one whose corner (in u, v) is in
760
- // the -n direction. Decompose -n into the u and v components.
761
- const minus_n_dot_u = -(nx * u_ax_x + ny * u_ax_y + nz * u_ax_z);
762
- const minus_n_dot_v = -(nx * v_ax_x + ny * v_ax_y + nz * v_ax_z);
763
- const u_sign = minus_n_dot_u >= 0 ? 1 : -1;
764
- const v_sign = minus_n_dot_v >= 0 ? 1 : -1;
765
-
766
- // Box edge endpoints in world: from corner_lo to corner_hi along
767
- // the chosen box axis.
768
- const corner_cx = bcx + u_ax_x * u_sign * half_u + v_ax_x * v_sign * half_v;
769
- const corner_cy = bcy + u_ax_y * u_sign * half_u + v_ax_y * v_sign * half_v;
770
- const corner_cz = bcz + u_ax_z * u_sign * half_u + v_ax_z * v_sign * half_v;
771
- const box_p1x = corner_cx - edx * edge_half, box_p1y = corner_cy - edy * edge_half, box_p1z = corner_cz - edz * edge_half;
772
- const box_p2x = corner_cx + edx * edge_half, box_p2y = corner_cy + edy * edge_half, box_p2z = corner_cz + edz * edge_half;
773
-
774
- // Triangle edge endpoints.
775
- let tri_p1x, tri_p1y, tri_p1z, tri_p2x, tri_p2y, tri_p2z;
776
- if (tri_edge_idx === 0) {
777
- tri_p1x = ax; tri_p1y = ay; tri_p1z = az;
778
- tri_p2x = bx; tri_p2y = by; tri_p2z = bz;
779
- } else if (tri_edge_idx === 1) {
780
- tri_p1x = bx; tri_p1y = by; tri_p1z = bz;
781
- tri_p2x = cx; tri_p2y = cy; tri_p2z = cz;
782
- } else {
783
- tri_p1x = cx; tri_p1y = cy; tri_p1z = cz;
784
- tri_p2x = ax; tri_p2y = ay; tri_p2z = az;
785
- }
786
-
787
- // Closest points on the two segments.
788
- line3_closest_points_segment_segment(
789
- closest_pair_st,
790
- box_p1x, box_p1y, box_p1z, box_p2x, box_p2y, box_p2z,
791
- tri_p1x, tri_p1y, tri_p1z, tri_p2x, tri_p2y, tri_p2z
792
- );
793
- const s = closest_pair_st[0], t = closest_pair_st[1];
794
- const closest_box_x = box_p1x + s * (box_p2x - box_p1x);
795
- const closest_box_y = box_p1y + s * (box_p2y - box_p1y);
796
- const closest_box_z = box_p1z + s * (box_p2z - box_p1z);
797
- const closest_tri_x = tri_p1x + t * (tri_p2x - tri_p1x);
798
- const closest_tri_y = tri_p1y + t * (tri_p2y - tri_p1y);
799
- const closest_tri_z = tri_p1z + t * (tri_p2z - tri_p1z);
800
-
801
- out[3] = 1;
802
- const base = 4;
803
- out[base] = closest_tri_x;
804
- out[base + 1] = closest_tri_y;
805
- out[base + 2] = closest_tri_z;
806
- out[base + 3] = closest_box_x;
807
- out[base + 4] = closest_box_y;
808
- out[base + 5] = closest_box_z;
809
- out[base + 6] = overlap;
810
- return true;
811
- }
1
+ import { line3_closest_points_segment_segment } from "../../../core/geom/3d/line/line3_closest_points_segment_segment.js";
2
+ import { quat3_to_matrix3 } from "../../../core/geom/3d/quaternion/quat3_to_matrix3.js";
3
+
4
+ /**
5
+ * Multi-point manifold construction for an oriented box vs. a triangle.
6
+ *
7
+ * Algorithm (Parry's `contact_manifolds_cuboid_triangle.rs` as the
8
+ * blueprint):
9
+ *
10
+ * 1. SAT over 13 candidate axes — 3 box face normals + 1 triangle face
11
+ * normal + 9 edge-edge cross products. The triangle's projection
12
+ * onto an axis is NOT symmetric around its centroid (unlike the
13
+ * box's), so the per-axis MTV is computed via the asymmetric
14
+ * `min(push_pos, push_neg)` form rather than the box-box-style
15
+ * `(rA + rB) - dist`.
16
+ *
17
+ * 2. Branch by winning-axis source:
18
+ *
19
+ * a. Box face axis (sources 0..2) — reference face = box face along
20
+ * winning axis, incident polygon = triangle. Project the triangle
21
+ * into the reference face's (u, v) basis, clip against the
22
+ * rectangle |u| ≤ half_u, |v| ≤ half_v via Sutherland-Hodgman
23
+ * with 4 axis-aligned passes. For each surviving (u, v) point,
24
+ * recover its world position on the triangle plane.
25
+ *
26
+ * b. Triangle face axis (source 3) — reference = triangle, incident
27
+ * = box face most antiparallel to the contact normal. Project
28
+ * the box face's 4 corners into a 2D basis on the triangle
29
+ * plane, clip against the triangle's 3 edges (3 general
30
+ * half-plane passes). Recover world positions on the box face
31
+ * plane.
32
+ *
33
+ * c. Edge-cross axis (sources 4..12) — single contact at the
34
+ * closest pair of points on the relevant box edge and triangle
35
+ * edge (via {@link line3_closest_points_segment_segment}).
36
+ *
37
+ * 3. Reduce surviving contacts to at most {@link MAX_CONTACTS} by
38
+ * deepest-first then perimeter expansion.
39
+ *
40
+ * Contact convention: `out[0..2]` is the world normal pointing from the
41
+ * triangle's surface toward the box's centre (the direction the box
42
+ * should be pushed to separate from the triangle). The caller in
43
+ * `narrowphase_step.js` swaps to the "B → A" convention as needed.
44
+ *
45
+ * Output layout (mirrors {@link box_box_manifold}):
46
+ * out[0..2] : world normal (triangle → box)
47
+ * out[3] : contact count
48
+ * out[4 + k*7 + 0..2] : world contact on triangle surface
49
+ * out[4 + k*7 + 3..5] : world contact on box surface
50
+ * out[4 + k*7 + 6] : penetration depth (positive)
51
+ *
52
+ * @author Alex Goldring
53
+ * @copyright Company Named Limited (c) 2026
54
+ */
55
+
56
+ const MAX_CONTACTS = 4;
57
+ const CONTACT_STRIDE = 7;
58
+ const PARALLEL_EPS_SQR = 1e-8;
59
+
60
+ /**
61
+ * Length of `out` required by {@link box_triangle_contact}.
62
+ * @type {number}
63
+ */
64
+ export const BOX_TRIANGLE_OUT_LENGTH = 4 + MAX_CONTACTS * CONTACT_STRIDE;
65
+
66
+ // --- scratch storage (allocation-free across calls) ----------------------------
67
+
68
+ const scratch_axes = new Float64Array(9);
69
+
70
+ // Triangle projected to the box face's uv basis (start with 3 points;
71
+ // after axis-aligned clipping can grow to at most 7 points).
72
+ const tri_uv_in = new Float64Array(8 * 2);
73
+ const tri_uv_out = new Float64Array(8 * 2);
74
+
75
+ // Box face corners (for the triangle-axis-winner branch).
76
+ const box_face_corners_uv_in = new Float64Array(8 * 2);
77
+ const box_face_corners_uv_out = new Float64Array(8 * 2);
78
+
79
+ // Surviving contact candidates: stride 7 (3 world-tri + 3 world-box + 1 depth).
80
+ const candidates = new Float64Array(8 * 7);
81
+
82
+ // Closest-points scratch for the edge-cross-winner branch.
83
+ const closest_pair_st = new Float64Array(2);
84
+
85
+ // --- helpers -----------------------------------------------------------------
86
+
87
+ /**
88
+ * Half-extent of the box projected onto a unit world-axis `(ux, uy, uz)`,
89
+ * given the box's world-space axes and half-extents.
90
+ */
91
+ function projected_box_half_extent(axes, hx, hy, hz, ux, uy, uz) {
92
+ const px = ux * axes[0] + uy * axes[1] + uz * axes[2];
93
+ const py = ux * axes[3] + uy * axes[4] + uz * axes[5];
94
+ const pz = ux * axes[6] + uy * axes[7] + uz * axes[8];
95
+ return (px < 0 ? -px : px) * hx
96
+ + (py < 0 ? -py : py) * hy
97
+ + (pz < 0 ? -pz : pz) * hz;
98
+ }
99
+
100
+ /**
101
+ * Sutherland-Hodgman clip of `points_in` against the axis-aligned
102
+ * half-plane `coord_idx ≤ bound` (or `coord_idx ≥ bound` when
103
+ * `keep_below` is false). 2D stride 2; result to `points_out`.
104
+ * Returns surviving vertex count.
105
+ */
106
+ function clip_against_axis_uv(points_in, point_count, points_out, coord_idx, bound, keep_below) {
107
+ let out_count = 0;
108
+ for (let i = 0; i < point_count; i++) {
109
+ const j = (i + 1) % point_count;
110
+ const ax = points_in[i * 2], ay = points_in[i * 2 + 1];
111
+ const bx = points_in[j * 2], by = points_in[j * 2 + 1];
112
+ const av = coord_idx === 0 ? ax : ay;
113
+ const bv = coord_idx === 0 ? bx : by;
114
+ const a_inside = keep_below ? (av <= bound) : (av >= bound);
115
+ const b_inside = keep_below ? (bv <= bound) : (bv >= bound);
116
+
117
+ if (a_inside) {
118
+ points_out[out_count * 2] = ax;
119
+ points_out[out_count * 2 + 1] = ay;
120
+ out_count++;
121
+ }
122
+ if (a_inside !== b_inside) {
123
+ const denom = bv - av;
124
+ const t = denom !== 0 ? (bound - av) / denom : 0;
125
+ points_out[out_count * 2] = ax + (bx - ax) * t;
126
+ points_out[out_count * 2 + 1] = ay + (by - ay) * t;
127
+ out_count++;
128
+ }
129
+ }
130
+ return out_count;
131
+ }
132
+
133
+ /**
134
+ * Sutherland-Hodgman clip of `points_in` against the general half-plane
135
+ * `(p - (line_ox, line_oy)) · (line_nx, line_ny) ≤ 0`. 2D stride 2;
136
+ * result to `points_out`. Returns surviving vertex count.
137
+ */
138
+ function clip_against_half_plane_uv(points_in, point_count, points_out, line_ox, line_oy, line_nx, line_ny) {
139
+ let out_count = 0;
140
+ for (let i = 0; i < point_count; i++) {
141
+ const j = (i + 1) % point_count;
142
+ const ax = points_in[i * 2], ay = points_in[i * 2 + 1];
143
+ const bx = points_in[j * 2], by = points_in[j * 2 + 1];
144
+ const a_val = (ax - line_ox) * line_nx + (ay - line_oy) * line_ny;
145
+ const b_val = (bx - line_ox) * line_nx + (by - line_oy) * line_ny;
146
+ const a_inside = a_val <= 0;
147
+ const b_inside = b_val <= 0;
148
+
149
+ if (a_inside) {
150
+ points_out[out_count * 2] = ax;
151
+ points_out[out_count * 2 + 1] = ay;
152
+ out_count++;
153
+ }
154
+ if (a_inside !== b_inside) {
155
+ const denom = a_val - b_val;
156
+ const t = denom !== 0 ? a_val / denom : 0;
157
+ points_out[out_count * 2] = ax + (bx - ax) * t;
158
+ points_out[out_count * 2 + 1] = ay + (by - ay) * t;
159
+ out_count++;
160
+ }
161
+ }
162
+ return out_count;
163
+ }
164
+
165
+ /**
166
+ * In-place reduction of `candidates` (stride 7: x, y, z on triangle,
167
+ * x, y, z on box, depth) to at most {@link MAX_CONTACTS} entries.
168
+ * Strategy: keep the deepest, then iteratively pick the candidate
169
+ * whose minimum distance to the already-kept set (in 3D space) is
170
+ * largest — approximates max-perimeter / max-area selection.
171
+ */
172
+ function reduce_contacts(n) {
173
+ if (n <= MAX_CONTACTS) return n;
174
+
175
+ function swap(i, j) {
176
+ if (i === j) return;
177
+ const oi = i * 7;
178
+ const oj = j * 7;
179
+ for (let k = 0; k < 7; k++) {
180
+ const t = candidates[oi + k];
181
+ candidates[oi + k] = candidates[oj + k];
182
+ candidates[oj + k] = t;
183
+ }
184
+ }
185
+
186
+ // Move deepest to slot 0.
187
+ let deepest_idx = 0;
188
+ let deepest_val = candidates[6];
189
+ for (let i = 1; i < n; i++) {
190
+ const d = candidates[i * 7 + 6];
191
+ if (d > deepest_val) { deepest_val = d; deepest_idx = i; }
192
+ }
193
+ swap(0, deepest_idx);
194
+
195
+ for (let k = 1; k < MAX_CONTACTS; k++) {
196
+ let best_score = -1;
197
+ let best_i = -1;
198
+ for (let i = k; i < n; i++) {
199
+ let min_d2 = Infinity;
200
+ for (let j = 0; j < k; j++) {
201
+ const dx = candidates[i * 7] - candidates[j * 7];
202
+ const dy = candidates[i * 7 + 1] - candidates[j * 7 + 1];
203
+ const dz = candidates[i * 7 + 2] - candidates[j * 7 + 2];
204
+ const d2 = dx * dx + dy * dy + dz * dz;
205
+ if (d2 < min_d2) min_d2 = d2;
206
+ }
207
+ if (min_d2 > best_score) { best_score = min_d2; best_i = i; }
208
+ }
209
+ swap(k, best_i);
210
+ }
211
+ return MAX_CONTACTS;
212
+ }
213
+
214
+ // --- main --------------------------------------------------------------------
215
+
216
+ /**
217
+ * @param {number[]|Float64Array} out length >= {@link BOX_TRIANGLE_OUT_LENGTH}
218
+ * @param {number} bcx box centre x (world)
219
+ * @param {number} bcy
220
+ * @param {number} bcz
221
+ * @param {number} bqx box quaternion x
222
+ * @param {number} bqy
223
+ * @param {number} bqz
224
+ * @param {number} bqw
225
+ * @param {number} bhx box half-extent x (body frame)
226
+ * @param {number} bhy
227
+ * @param {number} bhz
228
+ * @param {number} ax triangle vertex A x (world)
229
+ * @param {number} ay
230
+ * @param {number} az
231
+ * @param {number} bx triangle vertex B x (world)
232
+ * @param {number} by
233
+ * @param {number} bz
234
+ * @param {number} cx triangle vertex C x (world)
235
+ * @param {number} cy
236
+ * @param {number} cz
237
+ * @returns {boolean} true if box and triangle overlap
238
+ */
239
+ export function box_triangle_contact(
240
+ out,
241
+ bcx, bcy, bcz, bqx, bqy, bqz, bqw, bhx, bhy, bhz,
242
+ ax, ay, az, bx, by, bz, cx, cy, cz
243
+ ) {
244
+ quat3_to_matrix3(scratch_axes, 0, bqx, bqy, bqz, bqw);
245
+ const axes = scratch_axes;
246
+
247
+ // Triangle edges in world.
248
+ const ab_x = bx - ax, ab_y = by - ay, ab_z = bz - az;
249
+ const bc_x = cx - bx, bc_y = cy - by, bc_z = cz - bz;
250
+ const ca_x = ax - cx, ca_y = ay - cy, ca_z = az - cz;
251
+
252
+ // Triangle face normal in world via (B-A) × (C-A).
253
+ const e2x = cx - ax, e2y = cy - ay, e2z = cz - az;
254
+ const tnx_raw = ab_y * e2z - ab_z * e2y;
255
+ const tny_raw = ab_z * e2x - ab_x * e2z;
256
+ const tnz_raw = ab_x * e2y - ab_y * e2x;
257
+ const tn_mag_sqr = tnx_raw * tnx_raw + tny_raw * tny_raw + tnz_raw * tnz_raw;
258
+ if (tn_mag_sqr <= PARALLEL_EPS_SQR) {
259
+ // Degenerate triangle (zero area). No sensible contact.
260
+ out[3] = 0;
261
+ return false;
262
+ }
263
+ const tn_inv = 1 / Math.sqrt(tn_mag_sqr);
264
+ const tnx = tnx_raw * tn_inv;
265
+ const tny = tny_raw * tn_inv;
266
+ const tnz = tnz_raw * tn_inv;
267
+
268
+ // Triangle centroid for sign-canonicalisation of the contact normal.
269
+ const tcx = (ax + bx + cx) / 3;
270
+ const tcy = (ay + by + cy) / 3;
271
+ const tcz = (az + bz + cz) / 3;
272
+ // Vector from triangle centroid toward box centre — used to flip the
273
+ // SAT axis so the stored normal consistently points triangle → box.
274
+ const d_t2b_x = bcx - tcx;
275
+ const d_t2b_y = bcy - tcy;
276
+ const d_t2b_z = bcz - tcz;
277
+
278
+ // SAT state.
279
+ let best_overlap = Infinity;
280
+ let best_nx = 0, best_ny = 0, best_nz = 0; // points triangle → box
281
+ let best_source = -1;
282
+ // Box-edge index (0..2) and triangle-edge index (0..2) for edge-cross sources.
283
+ let best_box_edge_idx = -1;
284
+ let best_tri_edge_idx = -1;
285
+
286
+ /**
287
+ * Test a candidate separating axis `(lx, ly, lz)` (need not be unit-length).
288
+ * Returns true if a separating axis is found (overlap < 0); otherwise
289
+ * updates `best_*` if this axis has the smallest overlap so far.
290
+ *
291
+ * Source:
292
+ * 0..2 : box face normal (axis index 0..2)
293
+ * 3 : triangle face normal
294
+ * 4..12 : edge-cross (box_edge_idx * 3 + tri_edge_idx, offset +4)
295
+ */
296
+ function test_axis(lx, ly, lz, source, box_e_idx, tri_e_idx) {
297
+ const len_sqr = lx * lx + ly * ly + lz * lz;
298
+ if (len_sqr <= PARALLEL_EPS_SQR) return false;
299
+ const inv_len = 1 / Math.sqrt(len_sqr);
300
+ const ux = lx * inv_len, uy = ly * inv_len, uz = lz * inv_len;
301
+
302
+ // Box projection.
303
+ const box_centre_proj = ux * bcx + uy * bcy + uz * bcz;
304
+ const r_box = projected_box_half_extent(axes, bhx, bhy, bhz, ux, uy, uz);
305
+ const bmin = box_centre_proj - r_box;
306
+ const bmax = box_centre_proj + r_box;
307
+
308
+ // Triangle projection — NOT symmetric around centroid, so we take
309
+ // the per-vertex extreme values.
310
+ const da = ux * ax + uy * ay + uz * az;
311
+ const db = ux * bx + uy * by + uz * bz;
312
+ const dc = ux * cx + uy * cy + uz * cz;
313
+ const tmin = da < db ? (da < dc ? da : dc) : (db < dc ? db : dc);
314
+ const tmax = da > db ? (da > dc ? da : dc) : (db > dc ? db : dc);
315
+
316
+ // Separation test.
317
+ if (bmax < tmin || tmax < bmin) return true;
318
+
319
+ // Compute MTV magnitude as the smaller of the two "push directions".
320
+ // push_pos: how far we'd push the triangle in +u to escape.
321
+ // push_neg: how far in -u.
322
+ // For asymmetric intervals these are not equal in general.
323
+ const push_pos = bmax - tmin;
324
+ const push_neg = tmax - bmin;
325
+ const overlap = push_pos < push_neg ? push_pos : push_neg;
326
+
327
+ if (overlap < best_overlap) {
328
+ best_overlap = overlap;
329
+ // Canonical normal direction: triangle → box. Use the
330
+ // box-centre-vs-triangle-centroid sign as the tiebreaker so
331
+ // the SAT axis is canonically oriented before we negate.
332
+ const center_dot = d_t2b_x * ux + d_t2b_y * uy + d_t2b_z * uz;
333
+ const sign = center_dot >= 0 ? 1 : -1;
334
+ best_nx = ux * sign;
335
+ best_ny = uy * sign;
336
+ best_nz = uz * sign;
337
+ best_source = source;
338
+ best_box_edge_idx = box_e_idx;
339
+ best_tri_edge_idx = tri_e_idx;
340
+ }
341
+ return false;
342
+ }
343
+
344
+ // 1. Box face normals (3 axes).
345
+ if (test_axis(axes[0], axes[1], axes[2], 0, -1, -1)) { out[3] = 0; return false; }
346
+ if (test_axis(axes[3], axes[4], axes[5], 1, -1, -1)) { out[3] = 0; return false; }
347
+ if (test_axis(axes[6], axes[7], axes[8], 2, -1, -1)) { out[3] = 0; return false; }
348
+
349
+ // 2. Triangle face normal (1 axis).
350
+ if (test_axis(tnx, tny, tnz, 3, -1, -1)) { out[3] = 0; return false; }
351
+
352
+ // 3. Edge-edge cross products (9 axes).
353
+ for (let i = 0; i < 3; i++) {
354
+ const aix = axes[i * 3], aiy = axes[i * 3 + 1], aiz = axes[i * 3 + 2];
355
+ for (let j = 0; j < 3; j++) {
356
+ let ex, ey, ez;
357
+ if (j === 0) { ex = ab_x; ey = ab_y; ez = ab_z; }
358
+ else if (j === 1) { ex = bc_x; ey = bc_y; ez = bc_z; }
359
+ else { ex = ca_x; ey = ca_y; ez = ca_z; }
360
+ const cx_ax = aiy * ez - aiz * ey;
361
+ const cy_ax = aiz * ex - aix * ez;
362
+ const cz_ax = aix * ey - aiy * ex;
363
+ if (test_axis(cx_ax, cy_ax, cz_ax, 4 + i * 3 + j, i, j)) { out[3] = 0; return false; }
364
+ }
365
+ }
366
+
367
+ // We have overlap. Output the contact normal (triangle → box).
368
+ const nx = best_nx, ny = best_ny, nz = best_nz;
369
+ out[0] = nx; out[1] = ny; out[2] = nz;
370
+
371
+ // --- Contact manifold construction ---
372
+
373
+ if (best_source < 3) {
374
+ // Box face axis winner: reference = box face, incident = triangle.
375
+ return emit_box_face_manifold(out, best_source, axes, bcx, bcy, bcz, bhx, bhy, bhz,
376
+ ax, ay, az, bx, by, bz, cx, cy, cz, tnx, tny, tnz, nx, ny, nz);
377
+ } else if (best_source === 3) {
378
+ // Triangle face axis winner: reference = triangle, incident = box face.
379
+ return emit_triangle_face_manifold(out, axes, bcx, bcy, bcz, bhx, bhy, bhz,
380
+ ax, ay, az, bx, by, bz, cx, cy, cz, tnx, tny, tnz, nx, ny, nz);
381
+ } else {
382
+ // Edge-cross winner: single contact at closest pair on the two edges.
383
+ return emit_edge_cross_contact(out, axes, bcx, bcy, bcz, bhx, bhy, bhz,
384
+ ax, ay, az, bx, by, bz, cx, cy, cz,
385
+ best_box_edge_idx, best_tri_edge_idx,
386
+ nx, ny, nz, best_overlap);
387
+ }
388
+ }
389
+
390
+ // --- Manifold builders ------------------------------------------------------
391
+
392
+ /**
393
+ * Box-face SAT winner: clip the triangle against the box's reference face
394
+ * rectangle, then for each surviving (u, v) recover the world contact on
395
+ * the triangle plane.
396
+ */
397
+ function emit_box_face_manifold(
398
+ out, ref_axis_idx, axes,
399
+ bcx, bcy, bcz, bhx, bhy, bhz,
400
+ ax, ay, az, bx, by, bz, cx, cy, cz,
401
+ tnx, tny, tnz,
402
+ nx, ny, nz
403
+ ) {
404
+ // The contact normal `n` points triangle → box. The box's outward
405
+ // face normal at the reference face points box → triangle = -n.
406
+ const face_out_nx = -nx;
407
+ const face_out_ny = -ny;
408
+ const face_out_nz = -nz;
409
+
410
+ // Sign of the reference axis aligned with the outward normal.
411
+ const ref_axis_x = axes[ref_axis_idx * 3];
412
+ const ref_axis_y = axes[ref_axis_idx * 3 + 1];
413
+ const ref_axis_z = axes[ref_axis_idx * 3 + 2];
414
+ const ref_axis_dot = ref_axis_x * face_out_nx + ref_axis_y * face_out_ny + ref_axis_z * face_out_nz;
415
+ const ref_axis_sign = ref_axis_dot >= 0 ? 1 : -1;
416
+ const ref_h_along = ref_axis_idx === 0 ? bhx : (ref_axis_idx === 1 ? bhy : bhz);
417
+
418
+ // Reference face origin (centre of the face on the +/- side of the box).
419
+ const ref_face_ox = bcx + ref_axis_x * ref_axis_sign * ref_h_along;
420
+ const ref_face_oy = bcy + ref_axis_y * ref_axis_sign * ref_h_along;
421
+ const ref_face_oz = bcz + ref_axis_z * ref_axis_sign * ref_h_along;
422
+
423
+ // Reference face's two tangent axes (u, v) and their half-extents.
424
+ let u_axis_idx, v_axis_idx;
425
+ if (ref_axis_idx === 0) { u_axis_idx = 1; v_axis_idx = 2; }
426
+ else if (ref_axis_idx === 1) { u_axis_idx = 2; v_axis_idx = 0; }
427
+ else { u_axis_idx = 0; v_axis_idx = 1; }
428
+ const ux = axes[u_axis_idx * 3], uy = axes[u_axis_idx * 3 + 1], uz = axes[u_axis_idx * 3 + 2];
429
+ const vx = axes[v_axis_idx * 3], vy = axes[v_axis_idx * 3 + 1], vz = axes[v_axis_idx * 3 + 2];
430
+ const half_u = u_axis_idx === 0 ? bhx : (u_axis_idx === 1 ? bhy : bhz);
431
+ const half_v = v_axis_idx === 0 ? bhx : (v_axis_idx === 1 ? bhy : bhz);
432
+
433
+ // Project the triangle's 3 vertices into the (u, v) basis on the
434
+ // reference plane.
435
+ const da_x = ax - ref_face_ox, da_y = ay - ref_face_oy, da_z = az - ref_face_oz;
436
+ const db_x = bx - ref_face_ox, db_y = by - ref_face_oy, db_z = bz - ref_face_oz;
437
+ const dc_x = cx - ref_face_ox, dc_y = cy - ref_face_oy, dc_z = cz - ref_face_oz;
438
+ tri_uv_in[0] = da_x * ux + da_y * uy + da_z * uz;
439
+ tri_uv_in[1] = da_x * vx + da_y * vy + da_z * vz;
440
+ tri_uv_in[2] = db_x * ux + db_y * uy + db_z * uz;
441
+ tri_uv_in[3] = db_x * vx + db_y * vy + db_z * vz;
442
+ tri_uv_in[4] = dc_x * ux + dc_y * uy + dc_z * uz;
443
+ tri_uv_in[5] = dc_x * vx + dc_y * vy + dc_z * vz;
444
+
445
+ // Clip against rectangle |u| ≤ half_u, |v| ≤ half_v via 4 passes.
446
+ let n = 3;
447
+ n = clip_against_axis_uv(tri_uv_in, n, tri_uv_out, 0, half_u, true);
448
+ if (n === 0) { out[3] = 0; return false; }
449
+ n = clip_against_axis_uv(tri_uv_out, n, tri_uv_in, 0, -half_u, false);
450
+ if (n === 0) { out[3] = 0; return false; }
451
+ n = clip_against_axis_uv(tri_uv_in, n, tri_uv_out, 1, half_v, true);
452
+ if (n === 0) { out[3] = 0; return false; }
453
+ n = clip_against_axis_uv(tri_uv_out, n, tri_uv_in, 1, -half_v, false);
454
+ if (n === 0) { out[3] = 0; return false; }
455
+
456
+ // The clipped polygon is in tri_uv_in. For each surviving (u, v):
457
+ // X = ref_face_origin + u * u_axis + v * v_axis (point on ref plane)
458
+ // Find t such that (X + t * face_out_n) is on the triangle plane:
459
+ // (X + t * face_out_n - A) · tn = 0
460
+ // t = ((A - X) · tn) / (face_out_n · tn)
461
+ // World point on triangle plane = X + t * face_out_n.
462
+ // Depth = -t (positive when triangle penetrates the box).
463
+ const denom = face_out_nx * tnx + face_out_ny * tny + face_out_nz * tnz;
464
+ if (Math.abs(denom) < PARALLEL_EPS_SQR) {
465
+ // Triangle plane parallel to ref outward — degenerate. No contacts.
466
+ out[3] = 0;
467
+ return false;
468
+ }
469
+ const clipped_uv = tri_uv_in;
470
+
471
+ let cand_count = 0;
472
+ for (let i = 0; i < n; i++) {
473
+ const u = clipped_uv[i * 2];
474
+ const v = clipped_uv[i * 2 + 1];
475
+ const xpx = ref_face_ox + u * ux + v * vx;
476
+ const xpy = ref_face_oy + u * uy + v * vy;
477
+ const xpz = ref_face_oz + u * uz + v * vz;
478
+
479
+ const t = ((ax - xpx) * tnx + (ay - xpy) * tny + (az - xpz) * tnz) / denom;
480
+ const world_tri_x = xpx + face_out_nx * t;
481
+ const world_tri_y = xpy + face_out_ny * t;
482
+ const world_tri_z = xpz + face_out_nz * t;
483
+ const depth = -t;
484
+ if (depth <= 0) continue;
485
+
486
+ const off = cand_count * 7;
487
+ candidates[off] = world_tri_x;
488
+ candidates[off + 1] = world_tri_y;
489
+ candidates[off + 2] = world_tri_z;
490
+ // Box-side contact = same (u, v) on ref plane.
491
+ candidates[off + 3] = xpx;
492
+ candidates[off + 4] = xpy;
493
+ candidates[off + 5] = xpz;
494
+ candidates[off + 6] = depth;
495
+ cand_count++;
496
+ }
497
+
498
+ if (cand_count === 0) { out[3] = 0; return false; }
499
+ const kept = reduce_contacts(cand_count);
500
+ out[3] = kept;
501
+ for (let k = 0; k < kept; k++) {
502
+ const src = k * 7;
503
+ const base = 4 + k * CONTACT_STRIDE;
504
+ out[base] = candidates[src];
505
+ out[base + 1] = candidates[src + 1];
506
+ out[base + 2] = candidates[src + 2];
507
+ out[base + 3] = candidates[src + 3];
508
+ out[base + 4] = candidates[src + 4];
509
+ out[base + 5] = candidates[src + 5];
510
+ out[base + 6] = candidates[src + 6];
511
+ }
512
+ return true;
513
+ }
514
+
515
+ /**
516
+ * Triangle-face SAT winner: reference = triangle, incident = the box face
517
+ * most antiparallel to the contact normal. Clip the box face quad against
518
+ * the triangle's 3 edges, then recover world contacts on the box face plane.
519
+ */
520
+ function emit_triangle_face_manifold(
521
+ out, axes,
522
+ bcx, bcy, bcz, bhx, bhy, bhz,
523
+ ax, ay, az, bx, by, bz, cx, cy, cz,
524
+ tnx, tny, tnz,
525
+ nx, ny, nz
526
+ ) {
527
+ // Triangle's outward face normal points box → triangle = -n.
528
+ // (Triangle is the reference, so its outward normal is the direction
529
+ // the box should be pushed to separate — opposite of triangle → box.)
530
+ const ref_out_x = -nx;
531
+ const ref_out_y = -ny;
532
+ const ref_out_z = -nz;
533
+ // But we also need to ensure ref_out aligns with the triangle's CCW
534
+ // face normal (tn). If tn opposes ref_out, the triangle is "facing
535
+ // away" from the box — would happen on a thin-shell mesh; in that
536
+ // case our SAT axis came from the FRONT face direction and ref_out
537
+ // is still correct geometrically. Either way, we use the
538
+ // sign-canonicalised contact normal as the source of truth.
539
+
540
+ // Pick the incident box face: the one whose outward axis is most
541
+ // antiparallel to ref_out.
542
+ let inc_axis_idx = 0;
543
+ let inc_axis_sign = 1;
544
+ let max_anti = -Infinity;
545
+ for (let i = 0; i < 3; i++) {
546
+ const iax = axes[i * 3], iay = axes[i * 3 + 1], iaz = axes[i * 3 + 2];
547
+ const d_pos = iax * ref_out_x + iay * ref_out_y + iaz * ref_out_z;
548
+ if (-d_pos > max_anti) { max_anti = -d_pos; inc_axis_idx = i; inc_axis_sign = -1; }
549
+ if ( d_pos > max_anti) { max_anti = d_pos; inc_axis_idx = i; inc_axis_sign = 1; }
550
+ }
551
+ const inc_outward_sign = -inc_axis_sign;
552
+ const inc_h_along = inc_axis_idx === 0 ? bhx : (inc_axis_idx === 1 ? bhy : bhz);
553
+ const inc_face_cx = bcx + axes[inc_axis_idx * 3] * inc_outward_sign * inc_h_along;
554
+ const inc_face_cy = bcy + axes[inc_axis_idx * 3 + 1] * inc_outward_sign * inc_h_along;
555
+ const inc_face_cz = bcz + axes[inc_axis_idx * 3 + 2] * inc_outward_sign * inc_h_along;
556
+
557
+ // Box face's two tangent axes in world.
558
+ let i_u_idx, i_v_idx;
559
+ if (inc_axis_idx === 0) { i_u_idx = 1; i_v_idx = 2; }
560
+ else if (inc_axis_idx === 1) { i_u_idx = 2; i_v_idx = 0; }
561
+ else { i_u_idx = 0; i_v_idx = 1; }
562
+ const iux = axes[i_u_idx * 3], iuy = axes[i_u_idx * 3 + 1], iuz = axes[i_u_idx * 3 + 2];
563
+ const ivx = axes[i_v_idx * 3], ivy = axes[i_v_idx * 3 + 1], ivz = axes[i_v_idx * 3 + 2];
564
+ const i_half_u = i_u_idx === 0 ? bhx : (i_u_idx === 1 ? bhy : bhz);
565
+ const i_half_v = i_v_idx === 0 ? bhx : (i_v_idx === 1 ? bhy : bhz);
566
+
567
+ // Build a 2D basis on the triangle plane. Use AB direction as `u`,
568
+ // and `n_uv = tn × u` as `v` so the basis is orthonormal.
569
+ const ab_x_w = bx - ax, ab_y_w = by - ay, ab_z_w = bz - az;
570
+ const ab_len_sqr = ab_x_w * ab_x_w + ab_y_w * ab_y_w + ab_z_w * ab_z_w;
571
+ if (ab_len_sqr <= PARALLEL_EPS_SQR) { out[3] = 0; return false; }
572
+ const ab_inv = 1 / Math.sqrt(ab_len_sqr);
573
+ const tux = ab_x_w * ab_inv, tuy = ab_y_w * ab_inv, tuz = ab_z_w * ab_inv;
574
+ // v = tn × u (using the geometric face normal as the "up").
575
+ const tvx = tny * tuz - tnz * tuy;
576
+ const tvy = tnz * tux - tnx * tuz;
577
+ const tvz = tnx * tuy - tny * tux;
578
+
579
+ // Project triangle vertices to (u, v). A is the origin of the
580
+ // 2D basis — A_uv = (0, 0); B is along the u axis by construction
581
+ // so B_uv = (|AB|, 0); C is computed normally.
582
+ const ac_x_w = cx - ax, ac_y_w = cy - ay, ac_z_w = cz - az;
583
+ const b_u = ab_x_w * tux + ab_y_w * tuy + ab_z_w * tuz; // = |AB|
584
+ const c_u = ac_x_w * tux + ac_y_w * tuy + ac_z_w * tuz;
585
+ const c_v = ac_x_w * tvx + ac_y_w * tvy + ac_z_w * tvz;
586
+
587
+ // Triangle's 3 edges as 2D half-planes in (u, v). For each edge, the
588
+ // outward normal must point AWAY from the opposite vertex.
589
+ // Edge AB: from A=(0,0) to B=(b_u, 0). Direction = (1, 0). Outward
590
+ // normal perpendicular = (0, 1) or (0, -1). Choose sign so it
591
+ // points away from C (= sign(-c_v) gives 1 if c_v < 0).
592
+ const ab_n_v = c_v >= 0 ? -1 : 1;
593
+ // Edge BC: from B=(b_u, 0) to C=(c_u, c_v). Direction = (c_u - b_u, c_v).
594
+ // Outward perpendicular: rotate by ±90° (depending on triangle winding).
595
+ // Outward direction must point away from A=(0,0).
596
+ const bc_dir_u = c_u - b_u, bc_dir_v = c_v - 0;
597
+ // Two perpendiculars: (-bc_dir_v, bc_dir_u) and (bc_dir_v, -bc_dir_u).
598
+ // Pick the one with positive dot vs (A - midpoint of BC) negated.
599
+ const bc_mid_u = (b_u + c_u) * 0.5;
600
+ const bc_mid_v = c_v * 0.5;
601
+ const to_a_u_from_bc = 0 - bc_mid_u;
602
+ const to_a_v_from_bc = 0 - bc_mid_v;
603
+ // We want outward = -direction_to_A.
604
+ let bc_n_u = -bc_dir_v, bc_n_v = bc_dir_u;
605
+ if (bc_n_u * to_a_u_from_bc + bc_n_v * to_a_v_from_bc > 0) {
606
+ bc_n_u = -bc_n_u; bc_n_v = -bc_n_v;
607
+ }
608
+ // Normalise so the clip threshold is "distance ≤ 0" in 2D.
609
+ const bc_n_len = Math.sqrt(bc_n_u * bc_n_u + bc_n_v * bc_n_v);
610
+ if (bc_n_len > 0) { bc_n_u /= bc_n_len; bc_n_v /= bc_n_len; }
611
+ // Edge CA: from C to A. Direction = (-c_u, -c_v). Outward must point
612
+ // away from B.
613
+ const ca_dir_u = 0 - c_u, ca_dir_v = 0 - c_v;
614
+ const ca_mid_u = c_u * 0.5;
615
+ const ca_mid_v = c_v * 0.5;
616
+ const to_b_u_from_ca = b_u - ca_mid_u;
617
+ const to_b_v_from_ca = 0 - ca_mid_v;
618
+ let ca_n_u = -ca_dir_v, ca_n_v = ca_dir_u;
619
+ if (ca_n_u * to_b_u_from_ca + ca_n_v * to_b_v_from_ca > 0) {
620
+ ca_n_u = -ca_n_u; ca_n_v = -ca_n_v;
621
+ }
622
+ const ca_n_len = Math.sqrt(ca_n_u * ca_n_u + ca_n_v * ca_n_v);
623
+ if (ca_n_len > 0) { ca_n_u /= ca_n_len; ca_n_v /= ca_n_len; }
624
+
625
+ // Build the box-face polygon (4 corners) and project to (u, v) on
626
+ // the triangle plane.
627
+ const signs_u = [ 1, 1, -1, -1];
628
+ const signs_v = [-1, 1, 1, -1];
629
+ for (let i = 0; i < 4; i++) {
630
+ const su = signs_u[i] * i_half_u;
631
+ const sv = signs_v[i] * i_half_v;
632
+ const wx = inc_face_cx + iux * su + ivx * sv;
633
+ const wy = inc_face_cy + iuy * su + ivy * sv;
634
+ const wz = inc_face_cz + iuz * su + ivz * sv;
635
+ const dx = wx - ax, dy = wy - ay, dz = wz - az;
636
+ box_face_corners_uv_in[i * 2] = dx * tux + dy * tuy + dz * tuz;
637
+ box_face_corners_uv_in[i * 2 + 1] = dx * tvx + dy * tvy + dz * tvz;
638
+ }
639
+
640
+ // Clip the quad against the 3 triangle-edge half-planes.
641
+ let np = 4;
642
+ // Edge AB: half-plane (p.v - 0) * ab_n_v <= 0 → coord_idx = 1, bound = 0, keep_below if ab_n_v=+1.
643
+ np = clip_against_half_plane_uv(box_face_corners_uv_in, np, box_face_corners_uv_out, 0, 0, 0, ab_n_v);
644
+ if (np === 0) { out[3] = 0; return false; }
645
+ np = clip_against_half_plane_uv(box_face_corners_uv_out, np, box_face_corners_uv_in, b_u, 0, bc_n_u, bc_n_v);
646
+ if (np === 0) { out[3] = 0; return false; }
647
+ np = clip_against_half_plane_uv(box_face_corners_uv_in, np, box_face_corners_uv_out, c_u, c_v, ca_n_u, ca_n_v);
648
+ if (np === 0) { out[3] = 0; return false; }
649
+
650
+ // Survivors are in box_face_corners_uv_out. For each:
651
+ // X = A + u * tu + v * tv (point on triangle plane)
652
+ // Find t such that X + t * ref_out is on box face plane:
653
+ // (X + t * ref_out - inc_face_c) · inc_out = 0
654
+ // Where inc_out = inc_axes[inc_axis_idx] * inc_outward_sign.
655
+ const inc_out_x = axes[inc_axis_idx * 3] * inc_outward_sign;
656
+ const inc_out_y = axes[inc_axis_idx * 3 + 1] * inc_outward_sign;
657
+ const inc_out_z = axes[inc_axis_idx * 3 + 2] * inc_outward_sign;
658
+ const denom = ref_out_x * inc_out_x + ref_out_y * inc_out_y + ref_out_z * inc_out_z;
659
+ if (Math.abs(denom) < PARALLEL_EPS_SQR) { out[3] = 0; return false; }
660
+ const clipped_uv = box_face_corners_uv_out;
661
+
662
+ let cand_count = 0;
663
+ for (let i = 0; i < np; i++) {
664
+ const u = clipped_uv[i * 2];
665
+ const v = clipped_uv[i * 2 + 1];
666
+ const xpx = ax + u * tux + v * tvx;
667
+ const xpy = ay + u * tuy + v * tvy;
668
+ const xpz = az + u * tuz + v * tvz;
669
+
670
+ const num_x = inc_face_cx - xpx;
671
+ const num_y = inc_face_cy - xpy;
672
+ const num_z = inc_face_cz - xpz;
673
+ const t = (num_x * inc_out_x + num_y * inc_out_y + num_z * inc_out_z) / denom;
674
+ const world_box_x = xpx + ref_out_x * t;
675
+ const world_box_y = xpy + ref_out_y * t;
676
+ const world_box_z = xpz + ref_out_z * t;
677
+ // Depth: how far the box face has penetrated past the triangle
678
+ // plane, measured along ref_out. depth = -t.
679
+ const depth = -t;
680
+ if (depth <= 0) continue;
681
+
682
+ const off = cand_count * 7;
683
+ // Triangle-side contact: the clipped (u, v) point on the
684
+ // triangle plane.
685
+ candidates[off] = xpx;
686
+ candidates[off + 1] = xpy;
687
+ candidates[off + 2] = xpz;
688
+ candidates[off + 3] = world_box_x;
689
+ candidates[off + 4] = world_box_y;
690
+ candidates[off + 5] = world_box_z;
691
+ candidates[off + 6] = depth;
692
+ cand_count++;
693
+ }
694
+
695
+ if (cand_count === 0) { out[3] = 0; return false; }
696
+ const kept = reduce_contacts(cand_count);
697
+ out[3] = kept;
698
+ for (let k = 0; k < kept; k++) {
699
+ const src = k * 7;
700
+ const base = 4 + k * CONTACT_STRIDE;
701
+ out[base] = candidates[src];
702
+ out[base + 1] = candidates[src + 1];
703
+ out[base + 2] = candidates[src + 2];
704
+ out[base + 3] = candidates[src + 3];
705
+ out[base + 4] = candidates[src + 4];
706
+ out[base + 5] = candidates[src + 5];
707
+ out[base + 6] = candidates[src + 6];
708
+ }
709
+ return true;
710
+ }
711
+
712
+ /**
713
+ * Edge-cross SAT winner: emit a single contact at the closest pair of
714
+ * points on (a parallel-translated) box edge × the triangle edge.
715
+ *
716
+ * The box has 12 edges, but along the winning box-axis direction `i`
717
+ * there are 4 parallel edges (the four lines on the box surface
718
+ * parallel to box-axis i). The "right" edge for the contact is the
719
+ * one whose endpoints span the triangle edge — practically, pick the
720
+ * box edge most aligned with the SAT separation: the one whose centre
721
+ * (when translated perpendicular to the SAT axis) is closest to the
722
+ * triangle edge.
723
+ *
724
+ * Procedure:
725
+ * 1. Construct the box edge as the line through box centre +
726
+ * (the +/- of the other two box half-extents pushing to the
727
+ * "near" corner determined by the SAT-axis sign).
728
+ * 2. Construct the triangle edge as the segment.
729
+ * 3. Closest-pair via `line3_closest_points_segment_segment`.
730
+ * 4. Contact = midpoint, depth = best_overlap.
731
+ */
732
+ function emit_edge_cross_contact(
733
+ out, axes,
734
+ bcx, bcy, bcz, bhx, bhy, bhz,
735
+ ax, ay, az, bx, by, bz, cx, cy, cz,
736
+ box_edge_idx, tri_edge_idx,
737
+ nx, ny, nz,
738
+ overlap
739
+ ) {
740
+ // Box edge direction: axis index `box_edge_idx`.
741
+ const edx = axes[box_edge_idx * 3];
742
+ const edy = axes[box_edge_idx * 3 + 1];
743
+ const edz = axes[box_edge_idx * 3 + 2];
744
+ const edge_half = box_edge_idx === 0 ? bhx : (box_edge_idx === 1 ? bhy : bhz);
745
+
746
+ // The OTHER two box axes determine which of the four parallel edges
747
+ // we pick. Use the sign of the contact normal projected onto each
748
+ // perpendicular axis to choose the edge closest to the contact.
749
+ let u_idx, v_idx;
750
+ if (box_edge_idx === 0) { u_idx = 1; v_idx = 2; }
751
+ else if (box_edge_idx === 1) { u_idx = 2; v_idx = 0; }
752
+ else { u_idx = 0; v_idx = 1; }
753
+ const u_ax_x = axes[u_idx * 3], u_ax_y = axes[u_idx * 3 + 1], u_ax_z = axes[u_idx * 3 + 2];
754
+ const v_ax_x = axes[v_idx * 3], v_ax_y = axes[v_idx * 3 + 1], v_ax_z = axes[v_idx * 3 + 2];
755
+ const half_u = u_idx === 0 ? bhx : (u_idx === 1 ? bhy : bhz);
756
+ const half_v = v_idx === 0 ? bhx : (v_idx === 1 ? bhy : bhz);
757
+
758
+ // The contact normal `n` points triangle → box. The box edge
759
+ // closest to the triangle is the one whose corner (in u, v) is in
760
+ // the -n direction. Decompose -n into the u and v components.
761
+ const minus_n_dot_u = -(nx * u_ax_x + ny * u_ax_y + nz * u_ax_z);
762
+ const minus_n_dot_v = -(nx * v_ax_x + ny * v_ax_y + nz * v_ax_z);
763
+ const u_sign = minus_n_dot_u >= 0 ? 1 : -1;
764
+ const v_sign = minus_n_dot_v >= 0 ? 1 : -1;
765
+
766
+ // Box edge endpoints in world: from corner_lo to corner_hi along
767
+ // the chosen box axis.
768
+ const corner_cx = bcx + u_ax_x * u_sign * half_u + v_ax_x * v_sign * half_v;
769
+ const corner_cy = bcy + u_ax_y * u_sign * half_u + v_ax_y * v_sign * half_v;
770
+ const corner_cz = bcz + u_ax_z * u_sign * half_u + v_ax_z * v_sign * half_v;
771
+ const box_p1x = corner_cx - edx * edge_half, box_p1y = corner_cy - edy * edge_half, box_p1z = corner_cz - edz * edge_half;
772
+ const box_p2x = corner_cx + edx * edge_half, box_p2y = corner_cy + edy * edge_half, box_p2z = corner_cz + edz * edge_half;
773
+
774
+ // Triangle edge endpoints.
775
+ let tri_p1x, tri_p1y, tri_p1z, tri_p2x, tri_p2y, tri_p2z;
776
+ if (tri_edge_idx === 0) {
777
+ tri_p1x = ax; tri_p1y = ay; tri_p1z = az;
778
+ tri_p2x = bx; tri_p2y = by; tri_p2z = bz;
779
+ } else if (tri_edge_idx === 1) {
780
+ tri_p1x = bx; tri_p1y = by; tri_p1z = bz;
781
+ tri_p2x = cx; tri_p2y = cy; tri_p2z = cz;
782
+ } else {
783
+ tri_p1x = cx; tri_p1y = cy; tri_p1z = cz;
784
+ tri_p2x = ax; tri_p2y = ay; tri_p2z = az;
785
+ }
786
+
787
+ // Closest points on the two segments.
788
+ line3_closest_points_segment_segment(
789
+ closest_pair_st,
790
+ box_p1x, box_p1y, box_p1z, box_p2x, box_p2y, box_p2z,
791
+ tri_p1x, tri_p1y, tri_p1z, tri_p2x, tri_p2y, tri_p2z
792
+ );
793
+ const s = closest_pair_st[0], t = closest_pair_st[1];
794
+ const closest_box_x = box_p1x + s * (box_p2x - box_p1x);
795
+ const closest_box_y = box_p1y + s * (box_p2y - box_p1y);
796
+ const closest_box_z = box_p1z + s * (box_p2z - box_p1z);
797
+ const closest_tri_x = tri_p1x + t * (tri_p2x - tri_p1x);
798
+ const closest_tri_y = tri_p1y + t * (tri_p2y - tri_p1y);
799
+ const closest_tri_z = tri_p1z + t * (tri_p2z - tri_p1z);
800
+
801
+ out[3] = 1;
802
+ const base = 4;
803
+ out[base] = closest_tri_x;
804
+ out[base + 1] = closest_tri_y;
805
+ out[base + 2] = closest_tri_z;
806
+ out[base + 3] = closest_box_x;
807
+ out[base + 4] = closest_box_y;
808
+ out[base + 5] = closest_box_z;
809
+ out[base + 6] = overlap;
810
+ return true;
811
+ }