@woosh/meep-engine 2.163.9 → 2.163.11

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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_capsule.d.ts +22 -0
  3. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_capsule.d.ts.map +1 -0
  4. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_capsule.js +101 -0
  5. package/src/core/collection/heap/IndexedFloatMaxHeap.d.ts.map +1 -0
  6. package/src/core/{graph/metis/native/refine → collection/heap}/IndexedFloatMaxHeap.js +1 -1
  7. package/src/core/geom/3d/capsule/capsule_intersects_aabb3_closed.d.ts +40 -0
  8. package/src/core/geom/3d/capsule/capsule_intersects_aabb3_closed.d.ts.map +1 -0
  9. package/src/core/geom/3d/capsule/capsule_intersects_aabb3_closed.js +67 -0
  10. package/src/core/geom/3d/capsule/capsule_intersects_aabb3_iterative.d.ts +45 -0
  11. package/src/core/geom/3d/capsule/capsule_intersects_aabb3_iterative.d.ts.map +1 -0
  12. package/src/core/geom/3d/capsule/capsule_intersects_aabb3_iterative.js +137 -0
  13. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +146 -0
  14. package/src/core/geom/3d/line/line3_compute_segment_nearest_point_to_aabb3_t.d.ts +44 -0
  15. package/src/core/geom/3d/line/line3_compute_segment_nearest_point_to_aabb3_t.d.ts.map +1 -0
  16. package/src/core/geom/3d/line/line3_compute_segment_nearest_point_to_aabb3_t.js +153 -0
  17. package/src/core/geom/3d/topology/struct/binary/BinaryTopology.d.ts.map +1 -1
  18. package/src/core/geom/3d/topology/struct/binary/BinaryTopology.js +18 -7
  19. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.d.ts +6 -0
  20. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.d.ts.map +1 -1
  21. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.js +139 -95
  22. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts.map +1 -1
  23. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js +2 -26
  24. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify_by_error.d.ts +19 -0
  25. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify_by_error.d.ts.map +1 -0
  26. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify_by_error.js +555 -0
  27. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_swap_vertex_slots.d.ts +13 -0
  28. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_swap_vertex_slots.d.ts.map +1 -0
  29. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_swap_vertex_slots.js +28 -0
  30. package/src/core/graph/metis/native/bisection/BisectionScratch.d.ts +1 -1
  31. package/src/core/graph/metis/native/bisection/BisectionScratch.d.ts.map +1 -1
  32. package/src/core/graph/metis/native/bisection/BisectionScratch.js +1 -1
  33. package/src/core/graph/metis/native/refine/RefinementScratch.d.ts +1 -1
  34. package/src/core/graph/metis/native/refine/RefinementScratch.d.ts.map +1 -1
  35. package/src/core/graph/metis/native/refine/RefinementScratch.js +1 -1
  36. package/src/engine/navigation/mesh/build/navmesh_build_topology.d.ts.map +1 -1
  37. package/src/engine/navigation/mesh/build/navmesh_build_topology.js +25 -0
  38. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  39. package/src/engine/physics/narrowphase/capsule_contacts.js +14 -21
  40. package/src/core/graph/metis/native/refine/IndexedFloatMaxHeap.d.ts.map +0 -1
  41. /package/src/core/{graph/metis/native/refine → collection/heap}/IndexedFloatMaxHeap.d.ts +0 -0
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Pure JavaScript game engine. Fully featured and production ready.",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "2.163.9",
9
+ "version": "2.163.11",
10
10
  "main": "build/meep.module.js",
11
11
  "module": "build/meep.module.js",
12
12
  "exports": {
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Collect the user-data of every BVH leaf whose AABB overlaps a capsule.
3
+ *
4
+ * The capsule is packed into a single array as
5
+ * `[p0_x, p0_y, p0_z, p1_x, p1_y, p1_z, r]` — the two segment endpoints
6
+ * followed by the radius (the same component order as
7
+ * {@link capsule_intersects_aabb3_closed}).
8
+ *
9
+ * Modelled on {@link bvh_query_user_data_overlaps_aabb} /
10
+ * {@link bvh_query_user_data_overlaps_sphere}: the same stack traversal, with
11
+ * the per-node predicate swapped for the exact capsule-vs-AABB overlap. Every
12
+ * child AABB is contained in its parent's, so a node the capsule misses prunes
13
+ * its whole subtree.
14
+ *
15
+ * @param {number[]|Uint32Array} result destination for matched leaf user-data
16
+ * @param {number} result_offset first index in `result` to write to
17
+ * @param {BVH} bvh
18
+ * @param {Float32Array|number[]} capsule `[p0x, p0y, p0z, p1x, p1y, p1z, r]`
19
+ * @returns {number} number of leaves written
20
+ */
21
+ export function bvh_query_user_data_overlaps_capsule(result: number[] | Uint32Array, result_offset: number, bvh: BVH, capsule: Float32Array | number[]): number;
22
+ //# sourceMappingURL=bvh_query_user_data_overlaps_capsule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bvh_query_user_data_overlaps_capsule.d.ts","sourceRoot":"","sources":["../../../../../../src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_capsule.js"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,6DANW,MAAM,EAAE,GAAC,WAAW,iBACpB,MAAM,qBAEN,YAAY,GAAC,MAAM,EAAE,GACnB,MAAM,CAyElB"}
@@ -0,0 +1,101 @@
1
+ //
2
+ import { SCRATCH_UINT32_TRAVERSAL_STACK } from "../../../collection/SCRATCH_UINT32_TRAVERSAL_STACK.js";
3
+ import { capsule_intersects_aabb3_closed } from "../../../geom/3d/capsule/capsule_intersects_aabb3_closed.js";
4
+ import { NULL_NODE } from "../BVH.js";
5
+
6
+ const stack = SCRATCH_UINT32_TRAVERSAL_STACK;
7
+
8
+ const scratch_aabb = new Float32Array(6);
9
+
10
+ /**
11
+ * Collect the user-data of every BVH leaf whose AABB overlaps a capsule.
12
+ *
13
+ * The capsule is packed into a single array as
14
+ * `[p0_x, p0_y, p0_z, p1_x, p1_y, p1_z, r]` — the two segment endpoints
15
+ * followed by the radius (the same component order as
16
+ * {@link capsule_intersects_aabb3_closed}).
17
+ *
18
+ * Modelled on {@link bvh_query_user_data_overlaps_aabb} /
19
+ * {@link bvh_query_user_data_overlaps_sphere}: the same stack traversal, with
20
+ * the per-node predicate swapped for the exact capsule-vs-AABB overlap. Every
21
+ * child AABB is contained in its parent's, so a node the capsule misses prunes
22
+ * its whole subtree.
23
+ *
24
+ * @param {number[]|Uint32Array} result destination for matched leaf user-data
25
+ * @param {number} result_offset first index in `result` to write to
26
+ * @param {BVH} bvh
27
+ * @param {Float32Array|number[]} capsule `[p0x, p0y, p0z, p1x, p1y, p1z, r]`
28
+ * @returns {number} number of leaves written
29
+ */
30
+ export function bvh_query_user_data_overlaps_capsule(
31
+ result,
32
+ result_offset,
33
+ bvh,
34
+ capsule
35
+ ) {
36
+ const root = bvh.root;
37
+
38
+ if (root === NULL_NODE) {
39
+ return 0;
40
+ }
41
+
42
+ const p0_x = capsule[0], p0_y = capsule[1], p0_z = capsule[2];
43
+ const p1_x = capsule[3], p1_y = capsule[4], p1_z = capsule[5];
44
+ const r = capsule[6];
45
+
46
+ /**
47
+ *
48
+ * @type {number}
49
+ */
50
+ const stack_top = stack.pointer++;
51
+
52
+ stack[stack_top] = root;
53
+
54
+ let result_cursor = result_offset;
55
+
56
+ while (stack.pointer > stack_top) {
57
+ stack.pointer--;
58
+
59
+ /**
60
+ *
61
+ * @type {number}
62
+ */
63
+ const node = stack[stack.pointer];
64
+
65
+ // test node against the capsule
66
+ bvh.node_get_aabb(node, scratch_aabb);
67
+
68
+ const intersection_exists = capsule_intersects_aabb3_closed(
69
+ p0_x, p0_y, p0_z,
70
+ p1_x, p1_y, p1_z,
71
+ r,
72
+ scratch_aabb[0], scratch_aabb[1], scratch_aabb[2],
73
+ scratch_aabb[3], scratch_aabb[4], scratch_aabb[5]
74
+ );
75
+
76
+ if (!intersection_exists) {
77
+ // fully outside
78
+ continue;
79
+ }
80
+
81
+ const node_is_leaf = bvh.node_is_leaf(node);
82
+
83
+ if (node_is_leaf) {
84
+ // leaf node
85
+ result[result_cursor++] = bvh.node_get_user_data(node);
86
+
87
+ } else {
88
+ // read in-order
89
+ const child1 = bvh.node_get_child1(node);
90
+ const child2 = bvh.node_get_child2(node);
91
+
92
+ // write to stack in reverse order, so that fist child ends up being visited first
93
+ stack[stack.pointer++] = child1;
94
+ stack[stack.pointer++] = child2;
95
+ }
96
+ }
97
+
98
+ // drop stack frame
99
+
100
+ return result_cursor - result_offset;
101
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IndexedFloatMaxHeap.d.ts","sourceRoot":"","sources":["../../../../../src/core/collection/heap/IndexedFloatMaxHeap.js"],"names":[],"mappings":"AA6BA;IACI;;;OAGG;IACH,yBAHW,MAAM,0BACN,MAAM,EAsBhB;IAbG,wBAA4C;IAC5C,eAAe;IAEf,2BAA4E;IAC5E,2BAAwD;IACxD,6BAA0D;IAE1D;;;OAGG;IACH,cAFU,WAAW,CAE2B;IAIpD,mBAEC;IAED;;;OAGG;IACH,cAQC;IAED,2BAEC;IAED;;;OAGG;IACH,qBAkBC;IAED;;;;OAIG;IACH,oBAsBC;IAED;;;;OAIG;IACH,kBAYC;IAED,wBAUC;IAED;;;OAGG;IACH,WAHW,MAAM,SACN,MAAM,QAehB;IAED;;OAEG;IACH,WAFa,MAAM,CAuBlB;IAED;;;OAGG;IACH,WAHW,MAAM,aACN,MAAM,QAehB;IAED;;;OAGG;IACH,WAHW,MAAM,GACJ,OAAO,CA+BnB;CACJ"}
@@ -1,4 +1,4 @@
1
- import { assert } from "../../../../assert.js";
1
+ import { assert } from "../../assert.js";
2
2
 
3
3
  /**
4
4
  * Max-heap of (uint32 id, float32 score) entries with an external id→slot index
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Test whether an oriented capsule overlaps an axis-aligned box — exact,
3
+ * closed-form (no iteration count, no convergence tolerance).
4
+ *
5
+ * A capsule is the set of points within `r` of its central segment `p0 → p1`
6
+ * (the Minkowski sum of that segment with a sphere of radius `r`), so it
7
+ * overlaps the box exactly when the segment passes within `r` of the box:
8
+ *
9
+ * overlap ⟺ distanceSqr(segment, AABB) ≤ r²
10
+ *
11
+ * The closest point on the segment to the box is located in O(1) by
12
+ * {@link line3_compute_segment_nearest_point_to_aabb3_t} (convex
13
+ * piecewise-quadratic minimiser); this then compares that closest distance to
14
+ * `r²`. {@link capsule_intersects_aabb3_iterative} is the convex-bisection
15
+ * sibling (simpler, but ~50 refinement steps). A degenerate capsule
16
+ * (`p0 === p1`) collapses to the sphere-vs-box test with no special-casing.
17
+ *
18
+ * Box bounds are assumed ordered per axis (`box_*0 ≤ box_*1`), matching the
19
+ * `AABB3` storage convention.
20
+ *
21
+ * @param {number} p0_x capsule segment start x
22
+ * @param {number} p0_y capsule segment start y
23
+ * @param {number} p0_z capsule segment start z
24
+ * @param {number} p1_x capsule segment end x
25
+ * @param {number} p1_y capsule segment end y
26
+ * @param {number} p1_z capsule segment end z
27
+ * @param {number} r capsule radius
28
+ * @param {number} box_x0 box min x
29
+ * @param {number} box_y0 box min y
30
+ * @param {number} box_z0 box min z
31
+ * @param {number} box_x1 box max x
32
+ * @param {number} box_y1 box max y
33
+ * @param {number} box_z1 box max z
34
+ * @returns {boolean} true when the capsule overlaps the box
35
+ *
36
+ * @author Alex Goldring
37
+ * @copyright Company Named Limited (c) 2026
38
+ */
39
+ export function capsule_intersects_aabb3_closed(p0_x: number, p0_y: number, p0_z: number, p1_x: number, p1_y: number, p1_z: number, r: number, box_x0: number, box_y0: number, box_z0: number, box_x1: number, box_y1: number, box_z1: number): boolean;
40
+ //# sourceMappingURL=capsule_intersects_aabb3_closed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capsule_intersects_aabb3_closed.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/capsule/capsule_intersects_aabb3_closed.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,sDAlBW,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,KACN,MAAM,UACN,MAAM,UACN,MAAM,UACN,MAAM,UACN,MAAM,UACN,MAAM,UACN,MAAM,GACJ,OAAO,CA6BnB"}
@@ -0,0 +1,67 @@
1
+ import { clamp } from "../../../math/clamp.js";
2
+ import { v3_distance_sqr } from "../../vec3/v3_distance_sqr.js";
3
+ import { line3_compute_segment_nearest_point_to_aabb3_t } from "../line/line3_compute_segment_nearest_point_to_aabb3_t.js";
4
+
5
+ /**
6
+ * Test whether an oriented capsule overlaps an axis-aligned box — exact,
7
+ * closed-form (no iteration count, no convergence tolerance).
8
+ *
9
+ * A capsule is the set of points within `r` of its central segment `p0 → p1`
10
+ * (the Minkowski sum of that segment with a sphere of radius `r`), so it
11
+ * overlaps the box exactly when the segment passes within `r` of the box:
12
+ *
13
+ * overlap ⟺ distanceSqr(segment, AABB) ≤ r²
14
+ *
15
+ * The closest point on the segment to the box is located in O(1) by
16
+ * {@link line3_compute_segment_nearest_point_to_aabb3_t} (convex
17
+ * piecewise-quadratic minimiser); this then compares that closest distance to
18
+ * `r²`. {@link capsule_intersects_aabb3_iterative} is the convex-bisection
19
+ * sibling (simpler, but ~50 refinement steps). A degenerate capsule
20
+ * (`p0 === p1`) collapses to the sphere-vs-box test with no special-casing.
21
+ *
22
+ * Box bounds are assumed ordered per axis (`box_*0 ≤ box_*1`), matching the
23
+ * `AABB3` storage convention.
24
+ *
25
+ * @param {number} p0_x capsule segment start x
26
+ * @param {number} p0_y capsule segment start y
27
+ * @param {number} p0_z capsule segment start z
28
+ * @param {number} p1_x capsule segment end x
29
+ * @param {number} p1_y capsule segment end y
30
+ * @param {number} p1_z capsule segment end z
31
+ * @param {number} r capsule radius
32
+ * @param {number} box_x0 box min x
33
+ * @param {number} box_y0 box min y
34
+ * @param {number} box_z0 box min z
35
+ * @param {number} box_x1 box max x
36
+ * @param {number} box_y1 box max y
37
+ * @param {number} box_z1 box max z
38
+ * @returns {boolean} true when the capsule overlaps the box
39
+ *
40
+ * @author Alex Goldring
41
+ * @copyright Company Named Limited (c) 2026
42
+ */
43
+ export function capsule_intersects_aabb3_closed(
44
+ p0_x, p0_y, p0_z,
45
+ p1_x, p1_y, p1_z,
46
+ r,
47
+ box_x0, box_y0, box_z0,
48
+ box_x1, box_y1, box_z1
49
+ ) {
50
+ const t = line3_compute_segment_nearest_point_to_aabb3_t(
51
+ p0_x, p0_y, p0_z,
52
+ p1_x, p1_y, p1_z,
53
+ box_x0, box_y0, box_z0,
54
+ box_x1, box_y1, box_z1
55
+ );
56
+
57
+ const p_x = p0_x + (p1_x - p0_x) * t;
58
+ const p_y = p0_y + (p1_y - p0_y) * t;
59
+ const p_z = p0_z + (p1_z - p0_z) * t;
60
+
61
+ return v3_distance_sqr(
62
+ p_x, p_y, p_z,
63
+ clamp(p_x, box_x0, box_x1),
64
+ clamp(p_y, box_y0, box_y1),
65
+ clamp(p_z, box_z0, box_z1)
66
+ ) <= r * r;
67
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Test whether an oriented capsule overlaps an axis-aligned box.
3
+ *
4
+ * A capsule is the set of points within `r` of its central segment `p0 → p1`
5
+ * (the Minkowski sum of that segment with a sphere of radius `r`). It therefore
6
+ * overlaps the box exactly when the segment passes within `r` of the box:
7
+ *
8
+ * overlap ⟺ distanceSqr(segment, AABB) ≤ r²
9
+ *
10
+ * so the whole test reduces to a segment-vs-box squared-distance compared
11
+ * against `r²` — no square root, no curved-surface math.
12
+ *
13
+ * The segment→box distance is `min_{t∈[0,1]} g(t)` with
14
+ * `g(t) = distanceSqr(P(t), box)`, `P(t) = p0 + t·d`. `g` is convex (a sum of
15
+ * per-axis convex "outside" terms composed with an affine `P`), so its
16
+ * minimiser is found from the monotone sign of
17
+ * `g'(t) ∝ (P(t) − clamp(P(t), box)) · d`: if `g'(0) ≥ 0` the minimum is the
18
+ * `p0` end, if `g'(1) ≤ 0` it is the `p1` end, otherwise it is the interior
19
+ * root, found by {@link BISECTION_ITERATIONS} bisection. A degenerate capsule
20
+ * (`p0 === p1`) has `d = 0 → g'(0) = 0 → t = 0`, i.e. the sphere-vs-box test,
21
+ * with no special-casing.
22
+ *
23
+ * Box bounds are assumed ordered per axis (`box_*0 ≤ box_*1`), matching the
24
+ * `AABB3` storage convention.
25
+ *
26
+ * @param {number} p0_x capsule segment start x
27
+ * @param {number} p0_y capsule segment start y
28
+ * @param {number} p0_z capsule segment start z
29
+ * @param {number} p1_x capsule segment end x
30
+ * @param {number} p1_y capsule segment end y
31
+ * @param {number} p1_z capsule segment end z
32
+ * @param {number} r capsule radius
33
+ * @param {number} box_x0 box min x
34
+ * @param {number} box_y0 box min y
35
+ * @param {number} box_z0 box min z
36
+ * @param {number} box_x1 box max x
37
+ * @param {number} box_y1 box max y
38
+ * @param {number} box_z1 box max z
39
+ * @returns {boolean} true when the capsule overlaps the box
40
+ *
41
+ * @author Alex Goldring
42
+ * @copyright Company Named Limited (c) 2026
43
+ */
44
+ export function capsule_intersects_aabb3_iterative(p0_x: number, p0_y: number, p0_z: number, p1_x: number, p1_y: number, p1_z: number, r: number, box_x0: number, box_y0: number, box_z0: number, box_x1: number, box_y1: number, box_z1: number): boolean;
45
+ //# sourceMappingURL=capsule_intersects_aabb3_iterative.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capsule_intersects_aabb3_iterative.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/capsule/capsule_intersects_aabb3_iterative.js"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,yDAlBW,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,KACN,MAAM,UACN,MAAM,UACN,MAAM,UACN,MAAM,UACN,MAAM,UACN,MAAM,UACN,MAAM,GACJ,OAAO,CA6EnB"}
@@ -0,0 +1,137 @@
1
+ import { clamp } from "../../../math/clamp.js";
2
+ import { v3_dot } from "../../vec3/v3_dot.js";
3
+ import { v3_distance_sqr } from "../../vec3/v3_distance_sqr.js";
4
+
5
+ /**
6
+ * Bisection steps used to locate the closest point on the capsule's central
7
+ * segment to the box. `g(t) = distanceSqr(P(t), box)` is convex on `t ∈ [0,1]`,
8
+ * so bisecting the sign of `g'(t)` halves the bracket every step at a rate that
9
+ * does NOT depend on the segment's orientation. 50 steps drives `t` to the f64
10
+ * floor on `[0,1]` (`2⁻⁵⁰ ≈ 9e-16`), so the reported distance is exact to
11
+ * floating-point — there are no false negatives even for a hair-thin radius.
12
+ *
13
+ * (Alternating projection — project onto the segment, clamp into the box,
14
+ * repeat — was tried first and is what the narrowphase `capsule_box_contact`
15
+ * uses, but it crawls when the segment is near-parallel to a box face/edge and
16
+ * a short in-box chord can be missed entirely; a fuzz at `r = 1e-7` found such
17
+ * misses, hence the switch.)
18
+ * @type {number}
19
+ */
20
+ const BISECTION_ITERATIONS = 50;
21
+
22
+ /**
23
+ * Test whether an oriented capsule overlaps an axis-aligned box.
24
+ *
25
+ * A capsule is the set of points within `r` of its central segment `p0 → p1`
26
+ * (the Minkowski sum of that segment with a sphere of radius `r`). It therefore
27
+ * overlaps the box exactly when the segment passes within `r` of the box:
28
+ *
29
+ * overlap ⟺ distanceSqr(segment, AABB) ≤ r²
30
+ *
31
+ * so the whole test reduces to a segment-vs-box squared-distance compared
32
+ * against `r²` — no square root, no curved-surface math.
33
+ *
34
+ * The segment→box distance is `min_{t∈[0,1]} g(t)` with
35
+ * `g(t) = distanceSqr(P(t), box)`, `P(t) = p0 + t·d`. `g` is convex (a sum of
36
+ * per-axis convex "outside" terms composed with an affine `P`), so its
37
+ * minimiser is found from the monotone sign of
38
+ * `g'(t) ∝ (P(t) − clamp(P(t), box)) · d`: if `g'(0) ≥ 0` the minimum is the
39
+ * `p0` end, if `g'(1) ≤ 0` it is the `p1` end, otherwise it is the interior
40
+ * root, found by {@link BISECTION_ITERATIONS} bisection. A degenerate capsule
41
+ * (`p0 === p1`) has `d = 0 → g'(0) = 0 → t = 0`, i.e. the sphere-vs-box test,
42
+ * with no special-casing.
43
+ *
44
+ * Box bounds are assumed ordered per axis (`box_*0 ≤ box_*1`), matching the
45
+ * `AABB3` storage convention.
46
+ *
47
+ * @param {number} p0_x capsule segment start x
48
+ * @param {number} p0_y capsule segment start y
49
+ * @param {number} p0_z capsule segment start z
50
+ * @param {number} p1_x capsule segment end x
51
+ * @param {number} p1_y capsule segment end y
52
+ * @param {number} p1_z capsule segment end z
53
+ * @param {number} r capsule radius
54
+ * @param {number} box_x0 box min x
55
+ * @param {number} box_y0 box min y
56
+ * @param {number} box_z0 box min z
57
+ * @param {number} box_x1 box max x
58
+ * @param {number} box_y1 box max y
59
+ * @param {number} box_z1 box max z
60
+ * @returns {boolean} true when the capsule overlaps the box
61
+ *
62
+ * @author Alex Goldring
63
+ * @copyright Company Named Limited (c) 2026
64
+ */
65
+ export function capsule_intersects_aabb3_iterative(
66
+ p0_x, p0_y, p0_z,
67
+ p1_x, p1_y, p1_z,
68
+ r,
69
+ box_x0, box_y0, box_z0,
70
+ box_x1, box_y1, box_z1
71
+ ) {
72
+ const d_x = p1_x - p0_x;
73
+ const d_y = p1_y - p0_y;
74
+ const d_z = p1_z - p0_z;
75
+
76
+ // Parameter of the closest point on the segment to the box.
77
+ let t;
78
+
79
+ // g'(0) ≥ 0 ⇒ g is non-decreasing on [0,1] ⇒ minimum at the p0 end.
80
+ // (Also the degenerate-segment path: d = 0 makes this dot 0 ⇒ t = 0.)
81
+ if (v3_dot(
82
+ p0_x - clamp(p0_x, box_x0, box_x1),
83
+ p0_y - clamp(p0_y, box_y0, box_y1),
84
+ p0_z - clamp(p0_z, box_z0, box_z1),
85
+ d_x, d_y, d_z
86
+ ) >= 0) {
87
+ t = 0;
88
+ } else if (v3_dot(
89
+ // g'(1) ≤ 0 ⇒ g is non-increasing on [0,1] ⇒ minimum at the p1 end.
90
+ p1_x - clamp(p1_x, box_x0, box_x1),
91
+ p1_y - clamp(p1_y, box_y0, box_y1),
92
+ p1_z - clamp(p1_z, box_z0, box_z1),
93
+ d_x, d_y, d_z
94
+ ) <= 0) {
95
+ t = 1;
96
+ } else {
97
+ // Interior minimum: g'(0) < 0 < g'(1), so a sign change is bracketed —
98
+ // bisect for g'(t) = 0.
99
+ let lo = 0;
100
+ let hi = 1;
101
+
102
+ for (let i = 0; i < BISECTION_ITERATIONS; i++) {
103
+ const mid = (lo + hi) * 0.5;
104
+
105
+ const p_x = p0_x + d_x * mid;
106
+ const p_y = p0_y + d_y * mid;
107
+ const p_z = p0_z + d_z * mid;
108
+
109
+ const g_prime = v3_dot(
110
+ p_x - clamp(p_x, box_x0, box_x1),
111
+ p_y - clamp(p_y, box_y0, box_y1),
112
+ p_z - clamp(p_z, box_z0, box_z1),
113
+ d_x, d_y, d_z
114
+ );
115
+
116
+ if (g_prime > 0) {
117
+ hi = mid;
118
+ } else {
119
+ lo = mid;
120
+ }
121
+ }
122
+
123
+ t = (lo + hi) * 0.5;
124
+ }
125
+
126
+ // Closest segment point and the box point nearest it; overlap ⟺ within radius.
127
+ const p_x = p0_x + d_x * t;
128
+ const p_y = p0_y + d_y * t;
129
+ const p_z = p0_z + d_z * t;
130
+
131
+ return v3_distance_sqr(
132
+ p_x, p_y, p_z,
133
+ clamp(p_x, box_x0, box_x1),
134
+ clamp(p_y, box_y0, box_y1),
135
+ clamp(p_z, box_z0, box_z1)
136
+ ) <= r * r;
137
+ }
@@ -0,0 +1,146 @@
1
+ # GJK review notes — Johnson distance subalgorithm & robustness
2
+
3
+ Notes from a 2026-06-18 research pass (capsule-vs-AABB / segment-vs-box work) on
4
+ how this engine's GJK family relates to the known GJK robustness literature —
5
+ chiefly the **Johnson distance subalgorithm** and its 2017 **signed-volumes**
6
+ replacement. See `NOTES.md` (same dir) for the raw reference links.
7
+
8
+ **TL;DR:** the specific Johnson failure mode does **not** apply here, because this
9
+ engine has **no distance-returning GJK** — nothing computes the Johnson
10
+ sub-distance, so there is nothing to replace. The finding is *forward-looking*:
11
+ it becomes relevant the day a closest-distance-between-separated-convexes query
12
+ is added (speculative contacts, conservative-advancement CCD, proximity
13
+ queries). The analogous simplex-degeneracy risk in our boolean/penetration GJK is
14
+ real but already mitigated by explicit floating-point-dust guards.
15
+
16
+ ## What this engine actually has
17
+
18
+ There are three Minkowski-difference routines here, and **none is a distance
19
+ subalgorithm**:
20
+
21
+ | File | Role | Simplex / sub-distance method |
22
+ |---|---|---|
23
+ | `gjk.js` | **Boolean** overlap test (used by `physics/queries/shape_cast.js`) | Voronoi-region simplex update via triple cross products (Kevin Moran adaptation). Returns a bool; never computes a closest point. |
24
+ | `gjk_epa_penetration.js` | **Penetration** axis+depth (narrowphase convex / mesh / concave-per-triangle) | GJK builds a rank-4 origin-enclosing simplex (Voronoi-region updates), then EPA expands faces toward the closest one (Bullet `btGjkEpa2` structure). |
25
+ | `mpr.js` | Penetration normal on **smooth** supports (sphere/capsule), where EPA stalls | Minkowski Portal Refinement — portal, not simplex sub-distance. |
26
+
27
+ The thing the literature calls the *Johnson distance subalgorithm* — given a
28
+ simplex of ≤4 points, find the closest point on it to the origin and the minimal
29
+ sub-simplex (which Voronoi region the origin projects into), via recursively
30
+ computed barycentric cofactors — is **absent**. Our GJK only ever asks "does the
31
+ simplex enclose / has the support passed the origin?", never "what is the
32
+ closest point on the simplex?". So there is no Johnson cofactor recursion and no
33
+ Backup procedure in this codebase.
34
+
35
+ ## The Johnson subalgorithm and why it fails
36
+
37
+ In a *distance* GJK (Gilbert–Johnson–Keerthi 1988), each iteration must solve the
38
+ sub-distance problem: project the origin onto the current simplex and discard the
39
+ vertices not contributing to the closest feature. Johnson's method expresses the
40
+ barycentric weights as ratios of determinants (Δ cofactors) built from dot
41
+ products of support-point *differences*.
42
+
43
+ Failure mode (the part relevant to robustness reviews): for a **near-degenerate
44
+ simplex** — points nearly coincident or nearly coplanar/collinear, which GJK
45
+ *naturally drives toward* as it converges on a near-contact configuration — those
46
+ determinants are differences of nearly-equal products and lose most of their
47
+ significant digits to **subtractive cancellation**. The algorithm then either
48
+ fails to find any valid sub-simplex (the historical "Backup procedure" brute-forces
49
+ all subsets as a fallback) or, worse, selects the wrong Voronoi region and returns
50
+ a **wrong closest point / distance**. Net effect in practice: the distance is
51
+ *under-estimated just before contact* — i.e. under-detection, the same direction
52
+ of error we found in the capsule-box alternating projection.
53
+
54
+ ## The 2017 fix — signed volumes (Montanari, Petrinic & Barbieri)
55
+
56
+ *"Improving the GJK Algorithm for Faster and More Reliable Distance Queries
57
+ Between Convex Objects"*, ACM TOG 36(3), 2017; reference impl **openGJK**
58
+ (SoftwareX 2018). Both already cited in `NOTES.md`.
59
+
60
+ It replaces Johnson's distance subalgorithm **and** the Backup procedure with a
61
+ **signed-volumes** subalgorithm:
62
+
63
+ - determines the Voronoi region by comparing **signed volumes** (determinants) of
64
+ the simplex and its sub-faces, projecting onto the lowest-dimensional one that
65
+ contains the origin's projection;
66
+ - crucially **does not multiply together potentially small quantities**, so it is
67
+ *accurate to machine precision* and, per the paper, "cannot possibly generate
68
+ systems of linearly dependent equations" — **degenerate simplices are handled
69
+ naturally** (they collapse to a lower-dimensional projection instead of
70
+ triggering a fallback);
71
+ - side benefit: ~10% faster on average, 15–30% in the contact-heavy regime.
72
+
73
+ This is the single most relevant robustness upgrade for any GJK that returns a
74
+ **distance**. It is *not* a drop-in change to a boolean or penetration GJK, which
75
+ do not run a sub-distance step.
76
+
77
+ ## Applicability to this codebase
78
+
79
+ 1. **No live bug.** Nothing here runs the Johnson subalgorithm, so there is
80
+ nothing to "fix". Do not retrofit signed-volumes onto `gjk.js` /
81
+ `gjk_epa_penetration.js` — they have no sub-distance step to replace.
82
+
83
+ 2. **The analogous risk is the Voronoi-region update**, not Johnson cofactors.
84
+ `gjk.js` and the GJK phase of `gjk_epa_penetration.js` decide regions with
85
+ triple-cross-products and dot products, which suffer the *same class* of
86
+ cancellation on degenerate simplices. This is already mitigated deliberately:
87
+ - `gjk.js` `perpendicular_to_edge()` + the exact-zero direction checks are FP-dust
88
+ guards for exactly the collapsed-simplex case (see the comment block there —
89
+ a wedge dot rounding a hair above zero while the triple product rounds to
90
+ zero). Documented as off the hot path.
91
+ - `gjk_epa_penetration.js` carries `DIR_EPS`/`EPA_EPS` floors and pool-overflow
92
+ "use current best face" degradations.
93
+ These are spot mitigations, not a machine-precision guarantee — but they cover
94
+ the inputs the narrowphase actually feeds (the convex hulls / triangles), and
95
+ the penetration path is cross-checked by MPR on smooth shapes.
96
+
97
+ 3. **Signed-volumes becomes relevant when a distance query is added.** If/when the
98
+ engine grows speculative/“soft” contacts that need a *separation distance*, a
99
+ conservative-advancement CCD that needs closest-distance per step, or general
100
+ proximity queries, implement that as a **new** signed-volumes distance GJK
101
+ (port openGJK) rather than coaxing distance out of the boolean/penetration
102
+ routines. Capsule primitives should keep using the closed-form contacts
103
+ (`capsule_contacts.js`, now exact — see below), not GJK-with-margin.
104
+
105
+ ## Related robustness notes (same review)
106
+
107
+ - **EPA precision wall on flat supports.** `Triangle3D`'s support is degenerate
108
+ along the face normal (all 3 vertices project equally), which stalls EPA and
109
+ yields noisy depths. The narrowphase already routes sphere/box/capsule-vs-
110
+ triangle through **closed-form** fast-paths in `narrowphase_step.js` for exactly
111
+ this reason, and `shape_cast.js` recovers the TOI normal with **MPR** rather
112
+ than EPA on smooth-vs-smooth. Good — keep new primitive pairs on closed forms.
113
+ - **Capsule-as-segment-support-with-margin is intentionally NOT used here.** Jolt
114
+ and PhysX-PCM collide capsules as a segment support inflated by the radius
115
+ inside GJK/EPA (with bounded corner error from the shrink-margin model). This
116
+ engine instead uses exact closed-form capsule contacts. That is the more
117
+ accurate choice for these primitive pairs and should stay.
118
+ - **Non-convergence policy.** Both `gjk.js` and `gjk_epa_penetration.js` cap at
119
+ `*_MAX_ITER = 64` and treat exhaustion as *no intersection*. For well-conditioned
120
+ convex inputs this is unreachable; it is a backstop against pathological input,
121
+ not a correctness lever.
122
+
123
+ ## Action items (low urgency — none is a known live defect)
124
+
125
+ - [ ] If a separation-distance / proximity / conservative-advancement query is
126
+ ever needed, port **openGJK signed-volumes** as a dedicated distance GJK.
127
+ Do not extend the boolean/penetration routines for it.
128
+ - [ ] (Optional, deeper) A focused robustness pass on
129
+ `gjk_epa_penetration.js`'s simplex phase — confirm the degenerate-simplex
130
+ guards hold under near-coplanar convex-hull inputs at scale. Out of scope
131
+ for this note; flagged only because it is the nearest analog to the Johnson
132
+ concern.
133
+
134
+ ## References
135
+
136
+ - Gilbert, Johnson, Keerthi (1988), *A Fast Procedure for Computing the Distance
137
+ Between Complex Objects in Three-Dimensional Space*, IEEE J. Robotics &
138
+ Automation — the original GJK + Johnson distance subalgorithm.
139
+ - Montanari, Petrinic, Barbieri (2017), *Improving the GJK Algorithm…*, ACM TOG
140
+ 36(3), DOI 10.1145/3072959.3083724; openGJK reference impl
141
+ (github.com/MattiaMontanari/openGJK). Already in `NOTES.md`.
142
+ - van den Bergen (1999), *A Fast and Robust GJK Implementation for Collision
143
+ Detection of Convex Objects* — the classic robustness treatment (error bounds,
144
+ the Backup procedure) that motivates the 2017 work.
145
+ - Ericson, *Real-Time Collision Detection* (2005), §9.5 (GJK), Ch. 9 (EPA context).
146
+ - `gjk.js` header — adapted from github.com/kevinmoran/GJK (boolean Voronoi GJK).
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Parameter `t ∈ [0,1]` of the point on the segment `p0 → p1` nearest to an
3
+ * axis-aligned box. The closest segment point is `p0 + t·(p1 − p0)`; the closest
4
+ * box point is that point clamped per-axis into the box. Sibling of
5
+ * {@link line3_compute_segment_nearest_point_to_point_t} (segment-vs-point).
6
+ *
7
+ * Exact and O(1). `g(t) = distanceSqr(P(t), box)` is convex and — for an AABB —
8
+ * piecewise-quadratic, with pieces meeting only where `P(t)` crosses one of the
9
+ * six box face planes (≤6 crossings). `g'(t) ∝ (P(t) − clamp(P(t), box)) · d` is
10
+ * therefore piecewise-linear and monotonically non-decreasing, so the minimiser
11
+ * is found in closed form by evaluating `g'` at the ≤8 candidates
12
+ * `{0, 1, face-plane crossings}`: the minimum is achieved on the interval where
13
+ * `g' = 0` (a single point when strictly convex), and this returns that
14
+ * interval's midpoint. No iteration, no tolerance, no Cramer's-Rule solve.
15
+ *
16
+ * When the minimum is non-unique (segment parallel to / inside the box) the
17
+ * midpoint is the *central* closest point — what the narrowphase
18
+ * `capsule_box_contact` needs for a stable resting manifold; a degenerate
19
+ * (`p0 === p1`) segment returns `0` (its parameter is arbitrary).
20
+ *
21
+ * The AABB specialisation of the Eberly/Ericson closest-point-segment-vs-box
22
+ * reduction. Backs {@link capsule_intersects_aabb3_closed} and the narrowphase
23
+ * `capsule_box_contact`; it replaced a 2-iteration alternating projection that
24
+ * under-detected and mis-placed contacts for segments near-parallel to a box
25
+ * face/edge (see `core/geom/3d/gjk/GJK_REVIEW_NOTES.md`).
26
+ *
27
+ * Box bounds are assumed ordered per axis (`box_*0 ≤ box_*1`).
28
+ *
29
+ * @param {number} p0_x segment start x
30
+ * @param {number} p0_y
31
+ * @param {number} p0_z
32
+ * @param {number} p1_x segment end x
33
+ * @param {number} p1_y
34
+ * @param {number} p1_z
35
+ * @param {number} box_x0 box min x
36
+ * @param {number} box_y0
37
+ * @param {number} box_z0
38
+ * @param {number} box_x1 box max x
39
+ * @param {number} box_y1
40
+ * @param {number} box_z1
41
+ * @returns {number} t in [0, 1]
42
+ */
43
+ export function line3_compute_segment_nearest_point_to_aabb3_t(p0_x: number, p0_y: number, p0_z: number, p1_x: number, p1_y: number, p1_z: number, box_x0: number, box_y0: number, box_z0: number, box_x1: number, box_y1: number, box_z1: number): number;
44
+ //# sourceMappingURL=line3_compute_segment_nearest_point_to_aabb3_t.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"line3_compute_segment_nearest_point_to_aabb3_t.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/line/line3_compute_segment_nearest_point_to_aabb3_t.js"],"names":[],"mappings":"AA2BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,qEAdW,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,UACN,MAAM,UACN,MAAM,UACN,MAAM,UACN,MAAM,UACN,MAAM,UACN,MAAM,GACJ,MAAM,CAqFlB"}