@woosh/meep-engine 2.163.10 → 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 (21) 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/geom/3d/capsule/capsule_intersects_aabb3_closed.d.ts +40 -0
  6. package/src/core/geom/3d/capsule/capsule_intersects_aabb3_closed.d.ts.map +1 -0
  7. package/src/core/geom/3d/capsule/capsule_intersects_aabb3_closed.js +67 -0
  8. package/src/core/geom/3d/capsule/capsule_intersects_aabb3_iterative.d.ts +45 -0
  9. package/src/core/geom/3d/capsule/capsule_intersects_aabb3_iterative.d.ts.map +1 -0
  10. package/src/core/geom/3d/capsule/capsule_intersects_aabb3_iterative.js +137 -0
  11. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +146 -0
  12. package/src/core/geom/3d/line/line3_compute_segment_nearest_point_to_aabb3_t.d.ts +44 -0
  13. package/src/core/geom/3d/line/line3_compute_segment_nearest_point_to_aabb3_t.d.ts.map +1 -0
  14. package/src/core/geom/3d/line/line3_compute_segment_nearest_point_to_aabb3_t.js +153 -0
  15. package/src/core/geom/3d/topology/struct/binary/BinaryTopology.d.ts.map +1 -1
  16. package/src/core/geom/3d/topology/struct/binary/BinaryTopology.js +18 -7
  17. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.d.ts +6 -0
  18. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.d.ts.map +1 -1
  19. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.js +139 -95
  20. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  21. package/src/engine/physics/narrowphase/capsule_contacts.js +14 -21
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.10",
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,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"}
@@ -0,0 +1,153 @@
1
+ import { clamp } from "../../../math/clamp.js";
2
+ import { v3_dot } from "../../vec3/v3_dot.js";
3
+
4
+ /**
5
+ * The argmin of `g(t)=distanceSqr(P(t),box)` is an INTERVAL when the minimum is
6
+ * non-unique (segment parallel to a face, or lying inside the box — the closest
7
+ * distance is then constant along a sub-segment). We track that interval as the
8
+ * monotone sign-structure of `g'(t)` is scanned and return its MIDPOINT, so the
9
+ * reported closest point is the central one rather than an arbitrary endpoint.
10
+ * Module-level (single-threaded, non-reentrant — same convention as the
11
+ * narrowphase `g_ct_*` accumulators):
12
+ * a = (max t, g) with g' ≤ 0, b = (min t, g) with g' > 0 → t_hi (rightmost min)
13
+ * c = (max t, g) with g' < 0, d = (min t, g) with g' ≥ 0 → t_lo (leftmost min)
14
+ */
15
+ let _a_t = -1, _a_g = 0;
16
+ let _b_t = 2, _b_g = 0;
17
+ let _c_t = -1, _c_g = 0;
18
+ let _d_t = 2, _d_g = 0;
19
+
20
+ /** Fold one candidate `(t, g'(t))` into the argmin-interval trackers. */
21
+ function _consider(t, g) {
22
+ if (g <= 0 && t > _a_t) { _a_t = t; _a_g = g; }
23
+ if (g > 0 && t < _b_t) { _b_t = t; _b_g = g; }
24
+ if (g < 0 && t > _c_t) { _c_t = t; _c_g = g; }
25
+ if (g >= 0 && t < _d_t) { _d_t = t; _d_g = g; }
26
+ }
27
+
28
+ /**
29
+ * Parameter `t ∈ [0,1]` of the point on the segment `p0 → p1` nearest to an
30
+ * axis-aligned box. The closest segment point is `p0 + t·(p1 − p0)`; the closest
31
+ * box point is that point clamped per-axis into the box. Sibling of
32
+ * {@link line3_compute_segment_nearest_point_to_point_t} (segment-vs-point).
33
+ *
34
+ * Exact and O(1). `g(t) = distanceSqr(P(t), box)` is convex and — for an AABB —
35
+ * piecewise-quadratic, with pieces meeting only where `P(t)` crosses one of the
36
+ * six box face planes (≤6 crossings). `g'(t) ∝ (P(t) − clamp(P(t), box)) · d` is
37
+ * therefore piecewise-linear and monotonically non-decreasing, so the minimiser
38
+ * is found in closed form by evaluating `g'` at the ≤8 candidates
39
+ * `{0, 1, face-plane crossings}`: the minimum is achieved on the interval where
40
+ * `g' = 0` (a single point when strictly convex), and this returns that
41
+ * interval's midpoint. No iteration, no tolerance, no Cramer's-Rule solve.
42
+ *
43
+ * When the minimum is non-unique (segment parallel to / inside the box) the
44
+ * midpoint is the *central* closest point — what the narrowphase
45
+ * `capsule_box_contact` needs for a stable resting manifold; a degenerate
46
+ * (`p0 === p1`) segment returns `0` (its parameter is arbitrary).
47
+ *
48
+ * The AABB specialisation of the Eberly/Ericson closest-point-segment-vs-box
49
+ * reduction. Backs {@link capsule_intersects_aabb3_closed} and the narrowphase
50
+ * `capsule_box_contact`; it replaced a 2-iteration alternating projection that
51
+ * under-detected and mis-placed contacts for segments near-parallel to a box
52
+ * face/edge (see `core/geom/3d/gjk/GJK_REVIEW_NOTES.md`).
53
+ *
54
+ * Box bounds are assumed ordered per axis (`box_*0 ≤ box_*1`).
55
+ *
56
+ * @param {number} p0_x segment start x
57
+ * @param {number} p0_y
58
+ * @param {number} p0_z
59
+ * @param {number} p1_x segment end x
60
+ * @param {number} p1_y
61
+ * @param {number} p1_z
62
+ * @param {number} box_x0 box min x
63
+ * @param {number} box_y0
64
+ * @param {number} box_z0
65
+ * @param {number} box_x1 box max x
66
+ * @param {number} box_y1
67
+ * @param {number} box_z1
68
+ * @returns {number} t in [0, 1]
69
+ */
70
+ export function line3_compute_segment_nearest_point_to_aabb3_t(
71
+ p0_x, p0_y, p0_z,
72
+ p1_x, p1_y, p1_z,
73
+ box_x0, box_y0, box_z0,
74
+ box_x1, box_y1, box_z1
75
+ ) {
76
+ const d_x = p1_x - p0_x;
77
+ const d_y = p1_y - p0_y;
78
+ const d_z = p1_z - p0_z;
79
+
80
+ if (d_x === 0 && d_y === 0 && d_z === 0) {
81
+ // Degenerate segment (a point) — its parameter is arbitrary.
82
+ return 0;
83
+ }
84
+
85
+ _a_t = -1; _a_g = 0;
86
+ _b_t = 2; _b_g = 0;
87
+ _c_t = -1; _c_g = 0;
88
+ _d_t = 2; _d_g = 0;
89
+
90
+ // Endpoint candidates.
91
+ _consider(0, v3_dot(
92
+ p0_x - clamp(p0_x, box_x0, box_x1),
93
+ p0_y - clamp(p0_y, box_y0, box_y1),
94
+ p0_z - clamp(p0_z, box_z0, box_z1),
95
+ d_x, d_y, d_z
96
+ ));
97
+ _consider(1, v3_dot(
98
+ p1_x - clamp(p1_x, box_x0, box_x1),
99
+ p1_y - clamp(p1_y, box_y0, box_y1),
100
+ p1_z - clamp(p1_z, box_z0, box_z1),
101
+ d_x, d_y, d_z
102
+ ));
103
+
104
+ // Face-plane crossing candidates (where an axis enters/leaves its slab).
105
+ for (let axis = 0; axis < 3; axis++) {
106
+ const d_a = axis === 0 ? d_x : (axis === 1 ? d_y : d_z);
107
+ if (d_a === 0) {
108
+ continue;
109
+ }
110
+ const p0_a = axis === 0 ? p0_x : (axis === 1 ? p0_y : p0_z);
111
+ const lo_a = axis === 0 ? box_x0 : (axis === 1 ? box_y0 : box_z0);
112
+ const hi_a = axis === 0 ? box_x1 : (axis === 1 ? box_y1 : box_z1);
113
+ const inv_d = 1 / d_a;
114
+
115
+ for (let side = 0; side < 2; side++) {
116
+ const tc = ((side === 0 ? lo_a : hi_a) - p0_a) * inv_d;
117
+ if (tc <= 0 || tc >= 1) {
118
+ continue;
119
+ }
120
+ const c_x = p0_x + d_x * tc;
121
+ const c_y = p0_y + d_y * tc;
122
+ const c_z = p0_z + d_z * tc;
123
+ _consider(tc, v3_dot(
124
+ c_x - clamp(c_x, box_x0, box_x1),
125
+ c_y - clamp(c_y, box_y0, box_y1),
126
+ c_z - clamp(c_z, box_z0, box_z1),
127
+ d_x, d_y, d_z
128
+ ));
129
+ }
130
+ }
131
+
132
+ // Rightmost minimiser: where g' last reaches 0 (g' linear between a and b).
133
+ let t_hi;
134
+ if (_a_t < 0) {
135
+ t_hi = 0; // g' > 0 everywhere → minimum at the p0 end
136
+ } else if (_b_t > 1) {
137
+ t_hi = 1; // g' ≤ 0 everywhere → minimum at the p1 end
138
+ } else {
139
+ t_hi = _a_t - _a_g * (_b_t - _a_t) / (_b_g - _a_g);
140
+ }
141
+
142
+ // Leftmost minimiser: where g' first reaches 0.
143
+ let t_lo;
144
+ if (_d_t > 1) {
145
+ t_lo = 1; // g' < 0 everywhere → minimum at the p1 end
146
+ } else if (_c_t < 0) {
147
+ t_lo = 0; // g' ≥ 0 everywhere → minimum at the p0 end
148
+ } else {
149
+ t_lo = _c_t - _c_g * (_d_t - _c_t) / (_d_g - _c_g);
150
+ }
151
+
152
+ return (t_lo + t_hi) * 0.5;
153
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"BinaryTopology.d.ts","sourceRoot":"","sources":["../../../../../../../../src/core/geom/3d/topology/struct/binary/BinaryTopology.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"BinaryTopology.d.ts","sourceRoot":"","sources":["../../../../../../../../src/core/geom/3d/topology/struct/binary/BinaryTopology.js"],"names":[],"mappings":"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"}
@@ -1,5 +1,4 @@
1
1
  import { assert } from "../../../../../assert.js";
2
- import { array_copy } from "../../../../../collection/array/array_copy.js";
3
2
  import { BinaryElementPool } from "./BinaryElementPool.js";
4
3
 
5
4
  /**
@@ -358,7 +357,9 @@ export class BinaryTopology {
358
357
 
359
358
  const v_address = pool.element_address(id);
360
359
  const v_offset = v_address >> 2; // get 4-byte boundary
361
- array_copy(pool.data_float32, v_offset, result, result_offset, 3);
360
+ result[result_offset] = pool.data_float32[v_offset];
361
+ result[result_offset + 1] = pool.data_float32[v_offset + 1];
362
+ result[result_offset + 2] = pool.data_float32[v_offset + 2];
362
363
  }
363
364
 
364
365
  /**
@@ -372,7 +373,9 @@ export class BinaryTopology {
372
373
 
373
374
  const v_address = pool.element_address(id);
374
375
  const v_offset = v_address >> 2; // get 4-byte boundary
375
- array_copy(value, value_offset, pool.data_float32, v_offset, 3);
376
+ pool.data_float32[v_offset] = value[value_offset];
377
+ pool.data_float32[v_offset + 1] = value[value_offset + 1];
378
+ pool.data_float32[v_offset + 2] = value[value_offset + 2];
376
379
  }
377
380
 
378
381
  /**
@@ -386,7 +389,9 @@ export class BinaryTopology {
386
389
 
387
390
  const v_address = pool.element_address(id) + 3 * FLOAT_32_SIZE;
388
391
  const v_offset = v_address >> 2; // get 4-byte boundary
389
- array_copy(pool.data_float32, v_offset, result, result_offset, 3);
392
+ result[result_offset] = pool.data_float32[v_offset];
393
+ result[result_offset + 1] = pool.data_float32[v_offset + 1];
394
+ result[result_offset + 2] = pool.data_float32[v_offset + 2];
390
395
  }
391
396
 
392
397
  /**
@@ -400,7 +405,9 @@ export class BinaryTopology {
400
405
 
401
406
  const v_address = pool.element_address(id) + 3 * FLOAT_32_SIZE;
402
407
  const v_offset = v_address >> 2; // get 4-byte boundary
403
- array_copy(value, value_offset, pool.data_float32, v_offset, 3);
408
+ pool.data_float32[v_offset] = value[value_offset];
409
+ pool.data_float32[v_offset + 1] = value[value_offset + 1];
410
+ pool.data_float32[v_offset + 2] = value[value_offset + 2];
404
411
  }
405
412
 
406
413
  /**
@@ -891,7 +898,9 @@ export class BinaryTopology {
891
898
  // Offset by the first loop pointer (UINT_32_SIZE)
892
899
  const f_address = pool.element_address(id) + UINT_32_SIZE;
893
900
  const f_offset = f_address >> 2;
894
- array_copy(pool.data_float32, f_offset, result, result_offset, 3);
901
+ result[result_offset] = pool.data_float32[f_offset];
902
+ result[result_offset + 1] = pool.data_float32[f_offset + 1];
903
+ result[result_offset + 2] = pool.data_float32[f_offset + 2];
895
904
  }
896
905
 
897
906
  /**
@@ -903,7 +912,9 @@ export class BinaryTopology {
903
912
  const pool = this.__face_pool;
904
913
  const f_address = pool.element_address(id) + UINT_32_SIZE;
905
914
  const f_offset = f_address >> 2;
906
- array_copy(value, value_offset, pool.data_float32, f_offset, 3);
915
+ pool.data_float32[f_offset] = value[value_offset];
916
+ pool.data_float32[f_offset + 1] = value[value_offset + 1];
917
+ pool.data_float32[f_offset + 2] = value[value_offset + 2];
907
918
  }
908
919
 
909
920
  /**
@@ -11,6 +11,12 @@
11
11
  * Call AFTER an initial vertex-merge/edge-fuse, then merge + fuse again so the new split vertices weld
12
12
  * onto the T-junction vertices and the resulting duplicate edges fuse.
13
13
  *
14
+ * A BVH over the vertices replaces the naive all-vertices-per-edge scan: for each boundary edge we
15
+ * query only the vertices within the tolerance of the edge segment (a capsule), which IS the on-edge
16
+ * region, so no separate per-candidate distance test is needed. The BVH is built ONCE -- every split
17
+ * adds a vertex snapped exactly onto an existing one's position, so no candidate position is ever
18
+ * missing from the original leaf set, and the tree never needs rebuilding as edges split.
19
+ *
14
20
  * @param {BinaryTopology} mesh
15
21
  * @param {number} [tolerance] max distance from a vertex to an edge for it to count as "on" the edge
16
22
  * @returns {number} number of splits performed
@@ -1 +1 @@
1
- {"version":3,"file":"bt_mesh_resolve_t_junctions.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;GAgBG;AACH,8EAHW,MAAM,GACJ,MAAM,CAuElB"}
1
+ {"version":3,"file":"bt_mesh_resolve_t_junctions.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.js"],"names":[],"mappings":"AAaA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,8EAHW,MAAM,GACJ,MAAM,CAwGlB"}
@@ -1,95 +1,139 @@
1
- import { NULL_POINTER } from "../BinaryTopology.js";
2
- import { bt_edge_split } from "./edge/bt_edge_split.js";
3
-
4
- const _a = new Float32Array(3);
5
- const _b = new Float32Array(3);
6
- const _p = new Float32Array(3);
7
- const _hit = new Float32Array(3);
8
-
9
- /**
10
- * Resolve T-junctions: where a vertex lies on the interior of a boundary edge (rather than at one of
11
- * its endpoints), split that edge at the vertex so the two pieces can later be welded/fused into a
12
- * shared edge.
13
- *
14
- * This is required for meshes assembled from independently-authored faces of differing sizes - e.g. an
15
- * L-shaped or ring floor built from rectangles, where one rectangle's corner falls in the middle of a
16
- * neighbour's edge. Without it those faces touch only at a single vertex, so edge-based neighbour
17
- * queries (island detection, erosion, path-finding) treat them as DISCONNECTED.
18
- *
19
- * Call AFTER an initial vertex-merge/edge-fuse, then merge + fuse again so the new split vertices weld
20
- * onto the T-junction vertices and the resulting duplicate edges fuse.
21
- *
22
- * @param {BinaryTopology} mesh
23
- * @param {number} [tolerance] max distance from a vertex to an edge for it to count as "on" the edge
24
- * @returns {number} number of splits performed
25
- */
26
- export function bt_mesh_resolve_t_junctions(mesh, tolerance = 1e-4) {
27
- const tol_sq = tolerance * tolerance;
28
-
29
- let total = 0;
30
- let rounds = 0;
31
- let changed = true;
32
-
33
- while (changed && rounds < 256) {
34
- changed = false;
35
- rounds++;
36
-
37
- const edge_count = mesh.edges.size;
38
-
39
- for (let e = 0; e < edge_count; e++) {
40
- if (!mesh.edges.is_allocated(e)) continue;
41
-
42
- // only boundary edges (a single radial loop) can carry a T-junction
43
- const l = mesh.edge_read_loop(e);
44
- if (l === NULL_POINTER) continue;
45
- if (mesh.loop_read_radial_next(l) !== l) continue;
46
-
47
- const v1 = mesh.edge_read_vertex1(e);
48
- const v2 = mesh.edge_read_vertex2(e);
49
-
50
- mesh.vertex_read_coordinate(_a, 0, v1);
51
- mesh.vertex_read_coordinate(_b, 0, v2);
52
-
53
- const abx = _b[0] - _a[0], aby = _b[1] - _a[1], abz = _b[2] - _a[2];
54
- const ab_sq = abx * abx + aby * aby + abz * abz;
55
- if (ab_sq <= tol_sq) continue;
56
-
57
- const t_margin = tolerance / Math.sqrt(ab_sq);
58
-
59
- let split_t = -1;
60
- let split_v = -1;
61
-
62
- const vertex_count = mesh.vertices.size;
63
- for (let v = 0; v < vertex_count; v++) {
64
- if (v === v1 || v === v2) continue;
65
- if (!mesh.vertices.is_allocated(v)) continue;
66
-
67
- mesh.vertex_read_coordinate(_p, 0, v);
68
-
69
- const t = ((_p[0] - _a[0]) * abx + (_p[1] - _a[1]) * aby + (_p[2] - _a[2]) * abz) / ab_sq;
70
- if (t <= t_margin || t >= 1 - t_margin) continue;
71
-
72
- const cx = _a[0] + t * abx - _p[0];
73
- const cy = _a[1] + t * aby - _p[1];
74
- const cz = _a[2] + t * abz - _p[2];
75
- if (cx * cx + cy * cy + cz * cz > tol_sq) continue;
76
-
77
- split_t = t;
78
- split_v = v;
79
- break;
80
- }
81
-
82
- if (split_t >= 0) {
83
- // snapshot the on-edge vertex coordinate, split, then snap the new vertex exactly onto it
84
- mesh.vertex_read_coordinate(_hit, 0, split_v);
85
- const nv = bt_edge_split(mesh, e, split_t);
86
- mesh.vertex_write_coordinate(nv, _hit, 0);
87
-
88
- total++;
89
- changed = true;
90
- }
91
- }
92
- }
93
-
94
- return total;
95
- }
1
+ import { BVH } from "../../../../../../bvh2/bvh3/BVH.js";
2
+ import {
3
+ bvh_query_user_data_overlaps_capsule
4
+ } from "../../../../../../bvh2/bvh3/query/bvh_query_user_data_overlaps_capsule.js";
5
+ import { NULL_POINTER } from "../BinaryTopology.js";
6
+ import { bt_edge_split } from "./edge/bt_edge_split.js";
7
+
8
+ const _a = new Float32Array(3);
9
+ const _b = new Float32Array(3);
10
+ const _p = new Float32Array(3);
11
+ const _hit = new Float32Array(3);
12
+ const _capsule = new Float32Array(7);
13
+
14
+ /**
15
+ * Resolve T-junctions: where a vertex lies on the interior of a boundary edge (rather than at one of
16
+ * its endpoints), split that edge at the vertex so the two pieces can later be welded/fused into a
17
+ * shared edge.
18
+ *
19
+ * This is required for meshes assembled from independently-authored faces of differing sizes - e.g. an
20
+ * L-shaped or ring floor built from rectangles, where one rectangle's corner falls in the middle of a
21
+ * neighbour's edge. Without it those faces touch only at a single vertex, so edge-based neighbour
22
+ * queries (island detection, erosion, path-finding) treat them as DISCONNECTED.
23
+ *
24
+ * Call AFTER an initial vertex-merge/edge-fuse, then merge + fuse again so the new split vertices weld
25
+ * onto the T-junction vertices and the resulting duplicate edges fuse.
26
+ *
27
+ * A BVH over the vertices replaces the naive all-vertices-per-edge scan: for each boundary edge we
28
+ * query only the vertices within the tolerance of the edge segment (a capsule), which IS the on-edge
29
+ * region, so no separate per-candidate distance test is needed. The BVH is built ONCE -- every split
30
+ * adds a vertex snapped exactly onto an existing one's position, so no candidate position is ever
31
+ * missing from the original leaf set, and the tree never needs rebuilding as edges split.
32
+ *
33
+ * @param {BinaryTopology} mesh
34
+ * @param {number} [tolerance] max distance from a vertex to an edge for it to count as "on" the edge
35
+ * @returns {number} number of splits performed
36
+ */
37
+ export function bt_mesh_resolve_t_junctions(mesh, tolerance = 1e-4) {
38
+ const tol_sq = tolerance * tolerance;
39
+
40
+ // --- Build a vertex BVH over the original vertices (point leaves; user data = vertex id) ---
41
+ const vertices = mesh.vertices;
42
+ const vertex_count = vertices.size;
43
+
44
+ const bvh = new BVH();
45
+
46
+ let leaf_count = 0;
47
+ for (let v = 0; v < vertex_count; v++) {
48
+ if (!vertices.is_allocated(v)) continue;
49
+
50
+ mesh.vertex_read_coordinate(_p, 0, v);
51
+
52
+ const node = bvh.allocate_node();
53
+ bvh.node_set_user_data(node, v);
54
+ bvh.node_set_aabb_primitive(node, _p[0], _p[1], _p[2], _p[0], _p[1], _p[2]);
55
+ bvh.insert_leaf(node);
56
+
57
+ leaf_count++;
58
+ }
59
+
60
+ if (leaf_count === 0) {
61
+ return 0;
62
+ }
63
+
64
+ // The query writes one vertex id per overlapping leaf; it can never exceed the leaf count.
65
+ const candidates = new Uint32Array(leaf_count);
66
+
67
+ let total = 0;
68
+ let rounds = 0;
69
+ let changed = true;
70
+
71
+ while (changed && rounds < 256) {
72
+ changed = false;
73
+ rounds++;
74
+
75
+ const edge_count = mesh.edges.size;
76
+
77
+ for (let e = 0; e < edge_count; e++) {
78
+ if (!mesh.edges.is_allocated(e)) continue;
79
+
80
+ // only boundary edges (a single radial loop) can carry a T-junction
81
+ const l = mesh.edge_read_loop(e);
82
+ if (l === NULL_POINTER) continue;
83
+ if (mesh.loop_read_radial_next(l) !== l) continue;
84
+
85
+ const v1 = mesh.edge_read_vertex1(e);
86
+ const v2 = mesh.edge_read_vertex2(e);
87
+
88
+ mesh.vertex_read_coordinate(_a, 0, v1);
89
+ mesh.vertex_read_coordinate(_b, 0, v2);
90
+
91
+ const abx = _b[0] - _a[0], aby = _b[1] - _a[1], abz = _b[2] - _a[2];
92
+ const ab_sq = abx * abx + aby * aby + abz * abz;
93
+ if (ab_sq <= tol_sq) continue;
94
+
95
+ const t_margin = tolerance / Math.sqrt(ab_sq);
96
+
97
+ // candidate vertices: those within `tolerance` of the edge segment (a capsule)
98
+ _capsule[0] = _a[0]; _capsule[1] = _a[1]; _capsule[2] = _a[2];
99
+ _capsule[3] = _b[0]; _capsule[4] = _b[1]; _capsule[5] = _b[2];
100
+ _capsule[6] = tolerance;
101
+
102
+ const candidate_count = bvh_query_user_data_overlaps_capsule(candidates, 0, bvh, _capsule);
103
+
104
+ let split_t = -1;
105
+ let split_v = -1;
106
+
107
+ for (let i = 0; i < candidate_count; i++) {
108
+ const v = candidates[i];
109
+
110
+ // Candidates are original vertices, which are never removed during resolution (splits
111
+ // only add), so they are always allocated -- no is_allocated check needed.
112
+ if (v === v1 || v === v2) continue;
113
+
114
+ mesh.vertex_read_coordinate(_p, 0, v);
115
+
116
+ // the capsule query already guarantees distance-to-segment <= tolerance; all that is
117
+ // left is to exclude the segment's endpoints (its hemispherical end caps)
118
+ const t = ((_p[0] - _a[0]) * abx + (_p[1] - _a[1]) * aby + (_p[2] - _a[2]) * abz) / ab_sq;
119
+ if (t <= t_margin || t >= 1 - t_margin) continue;
120
+
121
+ split_t = t;
122
+ split_v = v;
123
+ break;
124
+ }
125
+
126
+ if (split_t >= 0) {
127
+ // snapshot the on-edge vertex coordinate, split, then snap the new vertex exactly onto it
128
+ mesh.vertex_read_coordinate(_hit, 0, split_v);
129
+ const nv = bt_edge_split(mesh, e, split_t);
130
+ mesh.vertex_write_coordinate(nv, _hit, 0);
131
+
132
+ total++;
133
+ changed = true;
134
+ }
135
+ }
136
+ }
137
+
138
+ return total;
139
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"capsule_contacts.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/capsule_contacts.js"],"names":[],"mappings":"AA4BA;;;;;;;;;;;;;;;GAeG;AACH,2CAVW,MAAM,EAAE,GAAC,YAAY,MACrB,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,eACN,MAAM,QAOhB;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,4CAhBW,MAAM,EAAE,GAAC,YAAY,QACrB,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,YACN,MAAM,YACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,YACN,MAAM,GACJ,OAAO,CA6CnB;AAMD;;;;;GAKG;AACH,6CAHW,MAAM,EAAE,GAAC,YAAY,yNACnB,OAAO,CAuEnB;AA8BD;;;;;;;;;GASG;AACH,oDAHW,MAAM,EAAE,GAAC,YAAY,yNACnB,MAAM,CAoHlB;AASD;;;;;;;;;;;;;GAaG;AACH,yCAHW,MAAM,EAAE,GAAC,YAAY,4NACnB,OAAO,CAqGnB;AAyCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,gDAtBW,YAAY,QACZ,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,YACN,MAAM,YACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,GACJ,MAAM,CAsElB;AAzaD;;;;;;;;GAQG;AACH,6CAFU,MAAM,CAEiC;AAEjD;;;;GAIG;AACH,2CAFU,MAAM,CAE8B;AAsQ9C;;;;;;;;;;GAUG;AACH,yCAFU,MAAM,CAE6B;AAE7C;;;;;GAKG;AACH,uCAFU,MAAM,CAE0B"}
1
+ {"version":3,"file":"capsule_contacts.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/capsule_contacts.js"],"names":[],"mappings":"AA6BA;;;;;;;;;;;;;;;GAeG;AACH,2CAVW,MAAM,EAAE,GAAC,YAAY,MACrB,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,eACN,MAAM,QAOhB;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,4CAhBW,MAAM,EAAE,GAAC,YAAY,QACrB,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,YACN,MAAM,YACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,YACN,MAAM,GACJ,OAAO,CA6CnB;AAMD;;;;;GAKG;AACH,6CAHW,MAAM,EAAE,GAAC,YAAY,yNACnB,OAAO,CAuEnB;AA8BD;;;;;;;;;GASG;AACH,oDAHW,MAAM,EAAE,GAAC,YAAY,yNACnB,MAAM,CAoHlB;AASD;;;;;;;;;;;;;GAaG;AACH,yCAHW,MAAM,EAAE,GAAC,YAAY,4NACnB,OAAO,CA6FnB;AAyCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,gDAtBW,YAAY,QACZ,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,YACN,MAAM,YACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,GACJ,MAAM,CAsElB;AAjaD;;;;;;;;GAQG;AACH,6CAFU,MAAM,CAEiC;AAEjD;;;;GAIG;AACH,2CAFU,MAAM,CAE8B;AA8P9C;;;;;;;;;;GAUG;AACH,yCAFU,MAAM,CAE6B;AAE7C;;;;;GAKG;AACH,uCAFU,MAAM,CAE0B"}
@@ -1,5 +1,6 @@
1
1
  import { line3_closest_points_segment_segment } from "../../../core/geom/3d/line/line3_closest_points_segment_segment.js";
2
2
  import { line3_compute_segment_nearest_point_to_point_t } from "../../../core/geom/3d/line/line3_compute_segment_nearest_point_to_point_t.js";
3
+ import { line3_compute_segment_nearest_point_to_aabb3_t } from "../../../core/geom/3d/line/line3_compute_segment_nearest_point_to_aabb3_t.js";
3
4
  import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
4
5
  import { v3_quat3_apply_inverse } from "../../../core/geom/vec3/v3_quat3_apply_inverse.js";
5
6
  import { sphere_box_contact } from "./sphere_box_contact.js";
@@ -389,27 +390,19 @@ export function capsule_box_contact(
389
390
  const a0lx = scratch_a0_local[0], a0ly = scratch_a0_local[1], a0lz = scratch_a0_local[2];
390
391
  const a1lx = scratch_a1_local[0], a1ly = scratch_a1_local[1], a1lz = scratch_a1_local[2];
391
392
 
392
- // Fixed-point iteration: start with t at the closest point to the box centre.
393
- let t = line3_compute_segment_nearest_point_to_point_t(a0lx, a0ly, a0lz, a1lx, a1ly, a1lz, 0, 0, 0);
394
- let plx = a0lx + (a1lx - a0lx) * t;
395
- let ply = a0ly + (a1ly - a0ly) * t;
396
- let plz = a0lz + (a1lz - a0lz) * t;
397
-
398
- for (let iter = 0; iter < 2; iter++) {
399
- // Clamp current segment point to the box → closest box-surface point.
400
- const qlx = plx < -b_hx ? -b_hx : (plx > b_hx ? b_hx : plx);
401
- const qly = ply < -b_hy ? -b_hy : (ply > b_hy ? b_hy : ply);
402
- const qlz = plz < -b_hz ? -b_hz : (plz > b_hz ? b_hz : plz);
403
-
404
- // Re-find closest segment point to that box point.
405
- t = line3_compute_segment_nearest_point_to_point_t(
406
- a0lx, a0ly, a0lz, a1lx, a1ly, a1lz,
407
- qlx, qly, qlz
408
- );
409
- plx = a0lx + (a1lx - a0lx) * t;
410
- ply = a0ly + (a1ly - a0ly) * t;
411
- plz = a0lz + (a1lz - a0lz) * t;
412
- }
393
+ // Exact closest point on the capsule segment to the box-local AABB [−h, h].
394
+ // Replaces a 2-iteration alternating projection (project onto segment, clamp
395
+ // into box, repeat) that converged too slowly for capsules near-parallel to
396
+ // a box face/edge overestimating the segment-vs-box distance, which
397
+ // under-detected contacts (~0.85% of random configs missed) and mis-placed
398
+ // depth/normal on many more. See core/geom/3d/gjk/GJK_REVIEW_NOTES.md.
399
+ const t = line3_compute_segment_nearest_point_to_aabb3_t(
400
+ a0lx, a0ly, a0lz, a1lx, a1ly, a1lz,
401
+ -b_hx, -b_hy, -b_hz, b_hx, b_hy, b_hz
402
+ );
403
+ const plx = a0lx + (a1lx - a0lx) * t;
404
+ const ply = a0ly + (a1ly - a0ly) * t;
405
+ const plz = a0lz + (a1lz - a0lz) * t;
413
406
 
414
407
  // Final box-side point. `let` because the interior branch snaps it onto
415
408
  // the chosen face — for an inside segment point the clamp is an identity.