@woosh/meep-engine 2.152.0 → 2.154.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/package.json +1 -1
  2. package/src/core/color/Color.d.ts +26 -6
  3. package/src/core/color/Color.d.ts.map +1 -1
  4. package/src/core/color/Color.js +38 -6
  5. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts +112 -0
  6. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -0
  7. package/src/core/geom/3d/shape/ConvexHullShape3D.js +325 -0
  8. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +4 -0
  9. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
  10. package/src/engine/graphics/ecs/trail2d/Trail2D.js +21 -0
  11. package/src/engine/physics/PLAN.md +4 -4
  12. package/src/engine/physics/body/BodyStorage.d.ts +3 -1
  13. package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
  14. package/src/engine/physics/body/BodyStorage.js +452 -450
  15. package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -1
  16. package/src/engine/physics/body/SolverBodyState.js +6 -5
  17. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  18. package/src/engine/physics/broadphase/generate_pairs.js +9 -1
  19. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
  20. package/src/engine/physics/ccd/linear_sweep.js +237 -238
  21. package/src/engine/physics/computeInterceptPoint.d.ts.map +1 -1
  22. package/src/engine/physics/computeInterceptPoint.js +8 -3
  23. package/src/engine/physics/contact/ManifoldStore.d.ts +0 -16
  24. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  25. package/src/engine/physics/contact/ManifoldStore.js +1 -38
  26. package/src/engine/physics/ecs/BodyKind.d.ts +3 -2
  27. package/src/engine/physics/ecs/BodyKind.d.ts.map +1 -1
  28. package/src/engine/physics/ecs/BodyKind.js +25 -24
  29. package/src/engine/physics/ecs/PhysicsEvents.d.ts +4 -5
  30. package/src/engine/physics/ecs/PhysicsEvents.d.ts.map +1 -1
  31. package/src/engine/physics/ecs/PhysicsEvents.js +15 -16
  32. package/src/engine/physics/ecs/PhysicsSystem.d.ts +5 -30
  33. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  34. package/src/engine/physics/ecs/PhysicsSystem.js +13 -45
  35. package/src/engine/physics/ecs/RigidBodySerializationAdapter.d.ts.map +1 -1
  36. package/src/engine/physics/ecs/RigidBodySerializationAdapter.js +85 -81
  37. package/src/engine/physics/ecs/is_sensor.d.ts +18 -0
  38. package/src/engine/physics/ecs/is_sensor.d.ts.map +1 -0
  39. package/src/engine/physics/ecs/is_sensor.js +27 -0
  40. package/src/engine/physics/events/ContactEventBuffer.d.ts +2 -1
  41. package/src/engine/physics/events/ContactEventBuffer.d.ts.map +1 -1
  42. package/src/engine/physics/events/ContactEventBuffer.js +84 -83
  43. package/src/engine/physics/gjk/gjk.d.ts +0 -26
  44. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  45. package/src/engine/physics/gjk/gjk.js +3 -52
  46. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts +16 -0
  47. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts.map +1 -0
  48. package/src/engine/physics/gjk/gjk_epa_penetration.js +255 -0
  49. package/src/engine/physics/gjk/minkowski_support.d.ts +4 -9
  50. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -1
  51. package/src/engine/physics/gjk/minkowski_support.js +70 -75
  52. package/src/engine/physics/gjk/mpr.d.ts +1 -1
  53. package/src/engine/physics/gjk/mpr.d.ts.map +1 -1
  54. package/src/engine/physics/gjk/mpr.js +362 -344
  55. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  56. package/src/engine/physics/island/IslandBuilder.js +431 -428
  57. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  58. package/src/engine/physics/narrowphase/box_box_manifold.js +4 -81
  59. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
  60. package/src/engine/physics/narrowphase/box_triangle_contact.js +4 -39
  61. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  62. package/src/engine/physics/narrowphase/capsule_contacts.js +459 -462
  63. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts.map +1 -1
  64. package/src/engine/physics/narrowphase/clip_against_axis_uv.js +4 -1
  65. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +83 -0
  66. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -0
  67. package/src/engine/physics/narrowphase/convex_convex_manifold.js +425 -0
  68. package/src/engine/physics/narrowphase/convex_decomposition.d.ts +32 -0
  69. package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -0
  70. package/src/engine/physics/narrowphase/convex_decomposition.js +293 -0
  71. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts +41 -0
  72. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts.map +1 -0
  73. package/src/engine/physics/narrowphase/mesh_convex_hull.js +106 -0
  74. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts +8 -0
  75. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts.map +1 -0
  76. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.js +117 -0
  77. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  78. package/src/engine/physics/narrowphase/narrowphase_step.js +105 -102
  79. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts +29 -0
  80. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts.map +1 -0
  81. package/src/engine/physics/narrowphase/reduce_manifold_contacts.js +69 -0
  82. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  83. package/src/engine/physics/narrowphase/refine_ray_concave.js +152 -145
  84. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  85. package/src/engine/physics/narrowphase/sphere_box_contact.js +132 -123
  86. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  87. package/src/engine/physics/queries/overlap_shape.js +16 -17
  88. package/src/engine/physics/queries/raycast.d.ts +5 -0
  89. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  90. package/src/engine/physics/queries/raycast.js +16 -8
  91. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -1
  92. package/src/engine/physics/queries/shape_cast.js +13 -7
  93. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  94. package/src/engine/physics/solver/solve_contacts.js +8 -11
  95. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
  96. package/src/engine/physics/vehicle/RaycastVehicle.js +339 -333
  97. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +0 -13
  98. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +0 -1
  99. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +0 -399
@@ -1,83 +1,84 @@
1
- /**
2
- * Event kinds emitted by the manifold diff pass.
3
- * @readonly
4
- * @enum {number}
5
- */
6
- export const ContactEventKind = {
7
- Begin: 0,
8
- Stay: 1,
9
- End: 2,
10
- };
11
-
12
- const DEFAULT_INITIAL_CAPACITY = 32;
13
-
14
- /**
15
- * Per-event stride in the Uint32 backing array:
16
- * 0 : kind (ContactEventKind)
17
- * 1 : entityA
18
- * 2 : entityB
19
- * 3 : slot
20
- * @type {number}
21
- */
22
- const EVENT_STRIDE = 4;
23
-
24
- /**
25
- * Append-only buffer of contact events produced during one fixedUpdate, ready
26
- * to be dispatched (and then cleared) at end-of-step.
27
- *
28
- * Storage is flat Uint32Array keeping the metadata side allocation-free; the
29
- * actual contact payload (normal, depth, world point) is read on demand from
30
- * the {@link ManifoldStore} by the dispatch code.
31
- *
32
- * @author Alex Goldring
33
- * @copyright Company Named Limited (c) 2026
34
- */
35
- export class ContactEventBuffer {
36
- /**
37
- * @param {number} [initial_capacity] events
38
- */
39
- constructor(initial_capacity = DEFAULT_INITIAL_CAPACITY) {
40
- this.__capacity = Math.max(1, initial_capacity);
41
- this.__count = 0;
42
- // Int32 so entity ids may legitimately store -1 (stale body after free).
43
- this.__data = new Int32Array(this.__capacity * EVENT_STRIDE);
44
- }
45
-
46
- /**
47
- * @returns {number}
48
- */
49
- get count() {
50
- return this.__count;
51
- }
52
-
53
- clear() {
54
- this.__count = 0;
55
- }
56
-
57
- /**
58
- * @param {ContactEventKind|number} kind
59
- * @param {number} entityA
60
- * @param {number} entityB
61
- * @param {number} slot manifold slot id
62
- */
63
- push(kind, entityA, entityB, slot) {
64
- if (this.__count === this.__capacity) {
65
- const new_cap = this.__capacity << 1;
66
- const next = new Int32Array(new_cap * EVENT_STRIDE);
67
- next.set(this.__data);
68
- this.__data = next;
69
- this.__capacity = new_cap;
70
- }
71
- const off = this.__count * EVENT_STRIDE;
72
- this.__data[off] = kind;
73
- this.__data[off + 1] = entityA;
74
- this.__data[off + 2] = entityB;
75
- this.__data[off + 3] = slot;
76
- this.__count++;
77
- }
78
-
79
- kind_at(i) { return this.__data[i * EVENT_STRIDE]; }
80
- entityA_at(i) { return this.__data[i * EVENT_STRIDE + 1]; }
81
- entityB_at(i) { return this.__data[i * EVENT_STRIDE + 2]; }
82
- slot_at(i) { return this.__data[i * EVENT_STRIDE + 3]; }
83
- }
1
+ /**
2
+ * Event kinds emitted by the manifold diff pass.
3
+ * @readonly
4
+ * @enum {number}
5
+ */
6
+ export const ContactEventKind = {
7
+ Begin: 0,
8
+ Stay: 1,
9
+ End: 2,
10
+ };
11
+
12
+ const DEFAULT_INITIAL_CAPACITY = 32;
13
+
14
+ /**
15
+ * Per-event stride in the Int32 backing array:
16
+ * 0 : kind (ContactEventKind)
17
+ * 1 : entityA
18
+ * 2 : entityB
19
+ * 3 : slot
20
+ * @type {number}
21
+ */
22
+ const EVENT_STRIDE = 4;
23
+
24
+ /**
25
+ * Append-only buffer of contact events produced during one fixedUpdate, ready
26
+ * to be dispatched (and then cleared) at end-of-step.
27
+ *
28
+ * Storage is flat Int32Array (signed: entity ids may legitimately be -1 for a
29
+ * stale body after free) keeping the metadata side allocation-free; the
30
+ * actual contact payload (normal, depth, world point) is read on demand from
31
+ * the {@link ManifoldStore} by the dispatch code.
32
+ *
33
+ * @author Alex Goldring
34
+ * @copyright Company Named Limited (c) 2026
35
+ */
36
+ export class ContactEventBuffer {
37
+ /**
38
+ * @param {number} [initial_capacity] events
39
+ */
40
+ constructor(initial_capacity = DEFAULT_INITIAL_CAPACITY) {
41
+ this.__capacity = Math.max(1, initial_capacity);
42
+ this.__count = 0;
43
+ // Int32 so entity ids may legitimately store -1 (stale body after free).
44
+ this.__data = new Int32Array(this.__capacity * EVENT_STRIDE);
45
+ }
46
+
47
+ /**
48
+ * @returns {number}
49
+ */
50
+ get count() {
51
+ return this.__count;
52
+ }
53
+
54
+ clear() {
55
+ this.__count = 0;
56
+ }
57
+
58
+ /**
59
+ * @param {ContactEventKind|number} kind
60
+ * @param {number} entityA
61
+ * @param {number} entityB
62
+ * @param {number} slot manifold slot id
63
+ */
64
+ push(kind, entityA, entityB, slot) {
65
+ if (this.__count === this.__capacity) {
66
+ const new_cap = this.__capacity << 1;
67
+ const next = new Int32Array(new_cap * EVENT_STRIDE);
68
+ next.set(this.__data);
69
+ this.__data = next;
70
+ this.__capacity = new_cap;
71
+ }
72
+ const off = this.__count * EVENT_STRIDE;
73
+ this.__data[off] = kind;
74
+ this.__data[off + 1] = entityA;
75
+ this.__data[off + 2] = entityB;
76
+ this.__data[off + 3] = slot;
77
+ this.__count++;
78
+ }
79
+
80
+ kind_at(i) { return this.__data[i * EVENT_STRIDE]; }
81
+ entityA_at(i) { return this.__data[i * EVENT_STRIDE + 1]; }
82
+ entityB_at(i) { return this.__data[i * EVENT_STRIDE + 2]; }
83
+ slot_at(i) { return this.__data[i * EVENT_STRIDE + 3]; }
84
+ }
@@ -13,30 +13,4 @@
13
13
  * @returns {boolean} true if the shapes intersect
14
14
  */
15
15
  export function gjk(simplex: number[] | Float64Array, shape_a: AbstractShape3D, shape_b: AbstractShape3D): boolean;
16
- /**
17
- * Separating-axis-cached variant of {@link gjk}. The 3 floats at
18
- * `axis_buffer[axis_offset..+2]` are used as the initial search
19
- * direction (seed) AND are overwritten with the final search direction
20
- * on exit. The narrowphase keeps a per-manifold-slot cache of these
21
- * three floats so quiescent contacts converge in 1–2 iterations next
22
- * frame instead of ~6–10 from a cold `(1, 0, 0)` start (Bullet's
23
- * `m_cachedSeparatingAxis` and Jolt's `ioV` use the same pattern).
24
- *
25
- * The same primitive doubles as a separating-axis cache for the
26
- * "no-overlap" path: when GJK returns false, the final direction is
27
- * roughly the separating axis, and next frame's first iteration
28
- * detects separation immediately.
29
- *
30
- * Writeback is automatic — `gjk_core` mutates a `subarray` view of
31
- * `axis_buffer`, so on return `axis_buffer[axis_offset..+2]` holds the
32
- * final direction. No try/finally needed.
33
- *
34
- * @param {number[]|Float64Array} simplex
35
- * @param {AbstractShape3D} shape_a
36
- * @param {AbstractShape3D} shape_b
37
- * @param {Float64Array} axis_buffer
38
- * @param {number} axis_offset
39
- * @returns {boolean}
40
- */
41
- export function gjk_with_axis(simplex: number[] | Float64Array, shape_a: AbstractShape3D, shape_b: AbstractShape3D, axis_buffer: Float64Array, axis_offset: number): boolean;
42
16
  //# sourceMappingURL=gjk.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gjk.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/gjk/gjk.js"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;GAaG;AACH,6BALW,MAAM,EAAE,GAAC,YAAY,uDAGnB,OAAO,CAWnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,uCAPW,MAAM,EAAE,GAAC,YAAY,mEAGrB,YAAY,eACZ,MAAM,GACJ,OAAO,CAqBnB"}
1
+ {"version":3,"file":"gjk.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/gjk/gjk.js"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;GAaG;AACH,6BALW,MAAM,EAAE,GAAC,YAAY,uDAGnB,OAAO,CAWnB"}
@@ -40,60 +40,11 @@ export function gjk(simplex, shape_a, shape_b) {
40
40
  return gjk_core(simplex, shape_a, shape_b, scratch_dir);
41
41
  }
42
42
 
43
- /**
44
- * Separating-axis-cached variant of {@link gjk}. The 3 floats at
45
- * `axis_buffer[axis_offset..+2]` are used as the initial search
46
- * direction (seed) AND are overwritten with the final search direction
47
- * on exit. The narrowphase keeps a per-manifold-slot cache of these
48
- * three floats so quiescent contacts converge in 1–2 iterations next
49
- * frame instead of ~6–10 from a cold `(1, 0, 0)` start (Bullet's
50
- * `m_cachedSeparatingAxis` and Jolt's `ioV` use the same pattern).
51
- *
52
- * The same primitive doubles as a separating-axis cache for the
53
- * "no-overlap" path: when GJK returns false, the final direction is
54
- * roughly the separating axis, and next frame's first iteration
55
- * detects separation immediately.
56
- *
57
- * Writeback is automatic — `gjk_core` mutates a `subarray` view of
58
- * `axis_buffer`, so on return `axis_buffer[axis_offset..+2]` holds the
59
- * final direction. No try/finally needed.
60
- *
61
- * @param {number[]|Float64Array} simplex
62
- * @param {AbstractShape3D} shape_a
63
- * @param {AbstractShape3D} shape_b
64
- * @param {Float64Array} axis_buffer
65
- * @param {number} axis_offset
66
- * @returns {boolean}
67
- */
68
- export function gjk_with_axis(simplex, shape_a, shape_b, axis_buffer, axis_offset) {
69
- // Subarray view: zero-copy in V8 once the JIT realises both views
70
- // back the same buffer; the in-place writes inside gjk_core
71
- // propagate to axis_buffer automatically.
72
- const dir_view = axis_buffer.subarray(axis_offset, axis_offset + 3);
73
-
74
- // Guard against a stale or degenerate cached axis. A zero vector
75
- // (first-frame initial state, or a stuck cache) and NaN / Inf
76
- // would crash the first support-point query; the (1, 0, 0)
77
- // fallback is the cold-start default.
78
- const x = dir_view[0], y = dir_view[1], z = dir_view[2];
79
- if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)
80
- || (x === 0 && y === 0 && z === 0)) {
81
- dir_view[0] = 1;
82
- dir_view[1] = 0;
83
- dir_view[2] = 0;
84
- }
85
-
86
- return gjk_core(simplex, shape_a, shape_b, dir_view);
87
- }
88
-
89
43
  /**
90
44
  * Core GJK routine — parameterised on the search-direction buffer.
91
- * The caller seeds `dir[0..2]` with an initial direction (typically
92
- * `(1, 0, 0)` for cold starts or a cached separating axis from a
93
- * previous frame), and on return `dir` holds the FINAL search
94
- * direction the algorithm settled on. The narrowphase separating-
95
- * axis cache (P2.1) uses this to thread per-manifold axes through
96
- * `gjk_with_axis` without a try/finally writeback dance.
45
+ * The caller seeds `dir[0..2]` with an initial direction (`(1, 0, 0)`
46
+ * for the {@link gjk} cold-start wrapper), and on return `dir` holds the
47
+ * FINAL search direction the algorithm settled on.
97
48
  *
98
49
  * @param {number[]|Float64Array} simplex
99
50
  * @param {AbstractShape3D} shape_a
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Penetration depth + normal between two convex support providers at world pose.
3
+ * Writes the unit separation normal (B→A) into `out` and returns the depth
4
+ * (positive on overlap, 0 if separated — `out` untouched on 0).
5
+ *
6
+ * @param {Float64Array|number[]} out length ≥ 3
7
+ * @param {{support:Function}} A world-space support provider (e.g. PosedShape)
8
+ * @param {{support:Function}} B
9
+ * @returns {number} penetration depth, or 0 if separated
10
+ */
11
+ export function gjk_epa_penetration(out: Float64Array | number[], A: {
12
+ support: Function;
13
+ }, B: {
14
+ support: Function;
15
+ }): number;
16
+ //# sourceMappingURL=gjk_epa_penetration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gjk_epa_penetration.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/gjk/gjk_epa_penetration.js"],"names":[],"mappings":"AA+OA;;;;;;;;;GASG;AACH,yCALW,YAAY,GAAC,MAAM,EAAE,KACrB;IAAC,OAAO,WAAS;CAAC,KAClB;IAAC,OAAO,WAAS;CAAC,GAChB,MAAM,CAOlB"}
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Robust GJK + EPA penetration for convex shapes — the narrowphase's
3
+ * convex-fallback and concave-per-triangle penetration query. Replaced the
4
+ * `gjk.js` `gjk_with_axis` + `expanding_polytope_algorithm.js` pair, which
5
+ * returned a non-minimal axis for polytopes because its GJK handed EPA a
6
+ * degenerate (origin-on-face) simplex (see memory: feedback_epa_unreliable_polytopes).
7
+ *
8
+ * Structure follows Bullet's btGjkEpa2: GJK produces a rank-4 simplex that
9
+ * strictly encloses the origin; EPA builds outward-oriented faces from it and
10
+ * expands toward the closest face, rebuilding the horizon each step. Operates on
11
+ * any support provider with `support(out, off, dx, dy, dz)` in WORLD space (e.g.
12
+ * {@link PosedShape}); the penetration normal + depth come from the Minkowski
13
+ * difference's closest face to the origin. No cross-frame state → reset-and-
14
+ * resimulate determinism preserved.
15
+ *
16
+ * Even with per-call allocation (plain arrays) it measured ~2.1× FASTER than the
17
+ * replaced pair, which wasted all 64 iterations diverging on the degenerate
18
+ * simplex. KNOWN FOLLOW-UP: port the hot path to typed-array pools to remove the
19
+ * per-call allocation; the convex/concave EPA fallbacks are rarely hit today
20
+ * (closed-form solvers + SAT cover the common pairs), so it has not been urgent.
21
+ *
22
+ * @author Alex Goldring
23
+ * @copyright Company Named Limited (c) 2026
24
+ */
25
+
26
+ const GJK_MAX_ITER = 64;
27
+ const EPA_MAX_ITER = 64;
28
+ const EPA_EPS = 1e-8;
29
+
30
+ const _sA = new Float64Array(3);
31
+ const _sB = new Float64Array(3);
32
+
33
+ function sub(a, b) { return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; }
34
+ function dot(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
35
+ function cross(a, b) {
36
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
37
+ }
38
+ function neg(a) { return [-a[0], -a[1], -a[2]]; }
39
+ function lensq(a) { return a[0] * a[0] + a[1] * a[1] + a[2] * a[2]; }
40
+ // triple product (a×b)×c = b(a·c) − a(b·c)
41
+ function tripleProduct(a, b, c) {
42
+ const ac = dot(a, c), bc = dot(b, c);
43
+ return [b[0] * ac - a[0] * bc, b[1] * ac - a[1] * bc, b[2] * ac - a[2] * bc];
44
+ }
45
+
46
+ /**
47
+ * Minkowski-difference support: support_A(d) − support_B(−d), world space.
48
+ *
49
+ * The direction is normalised first: the support contract assumes a UNIT vector
50
+ * — `SphereShape3D`/`CapsuleShape3D` scale the raw direction by their radius
51
+ * with no internal renormalisation — but GJK/EPA hand us arbitrary-magnitude
52
+ * search directions (the seed `−s` and the cross/triple products from the
53
+ * simplex evolution). Polytope supports are scale-invariant (vertex argmax), so
54
+ * this only matters for curved providers, but it must hold for them. Matches the
55
+ * sibling `minkowski_support.js`, which normalises for the same reason.
56
+ */
57
+ function csoSupport(A, B, d) {
58
+ let dx = d[0], dy = d[1], dz = d[2];
59
+ const len2 = dx * dx + dy * dy + dz * dz;
60
+ if (len2 > 1e-30) {
61
+ const inv = 1 / Math.sqrt(len2);
62
+ dx *= inv; dy *= inv; dz *= inv;
63
+ }
64
+ A.support(_sA, 0, dx, dy, dz);
65
+ B.support(_sB, 0, -dx, -dy, -dz);
66
+ return [_sA[0] - _sB[0], _sA[1] - _sB[1], _sA[2] - _sB[2]];
67
+ }
68
+
69
+ /**
70
+ * GJK intersection. Returns a rank-4 simplex (array of 4 CSO points) enclosing
71
+ * the origin on overlap, or null if separated. `simplex[0]` is the most-recent
72
+ * point throughout (Muratori convention).
73
+ */
74
+ function gjk(A, B) {
75
+ let d = [1, 0, 0];
76
+ let s = csoSupport(A, B, d);
77
+ if (lensq(s) < 1e-18) { d = [0, 1, 0]; s = csoSupport(A, B, d); }
78
+ const simplex = [s];
79
+ d = neg(s);
80
+
81
+ for (let iter = 0; iter < GJK_MAX_ITER; iter++) {
82
+ if (lensq(d) < 1e-18) return null; // search direction collapsed → touching/sep
83
+ const a = csoSupport(A, B, d);
84
+ if (dot(a, d) < 0) return null; // farthest point short of origin → separated
85
+ simplex.unshift(a);
86
+ if (doSimplex(simplex, d)) return simplex.slice(0, 4);
87
+ }
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * Evolve the simplex toward the origin. Mutates `simplex` (in place) and writes
93
+ * the next search direction into `dOut`. Returns true when a tetrahedron
94
+ * encloses the origin.
95
+ */
96
+ function doSimplex(simplex, dOut) {
97
+ if (simplex.length === 2) return lineCase(simplex, dOut);
98
+ if (simplex.length === 3) return triangleCase(simplex, dOut);
99
+ return tetraCase(simplex, dOut);
100
+ }
101
+
102
+ function setDir(dOut, v) { dOut[0] = v[0]; dOut[1] = v[1]; dOut[2] = v[2]; }
103
+
104
+ function lineCase(simplex, dOut) {
105
+ const a = simplex[0], b = simplex[1];
106
+ const ab = sub(b, a), ao = neg(a);
107
+ if (dot(ab, ao) > 0) {
108
+ const perp = tripleProduct(ab, ao, ab);
109
+ if (lensq(perp) < 1e-18) {
110
+ // origin on the line — pick any perpendicular
111
+ const ref = Math.abs(ab[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
112
+ setDir(dOut, cross(ab, ref));
113
+ } else setDir(dOut, perp);
114
+ } else {
115
+ simplex.length = 1; // keep [a]
116
+ setDir(dOut, ao);
117
+ }
118
+ return false;
119
+ }
120
+
121
+ function triangleCase(simplex, dOut) {
122
+ const a = simplex[0], b = simplex[1], c = simplex[2];
123
+ const ab = sub(b, a), ac = sub(c, a), ao = neg(a);
124
+ const abc = cross(ab, ac);
125
+
126
+ if (dot(cross(abc, ac), ao) > 0) {
127
+ if (dot(ac, ao) > 0) { // outside edge ac
128
+ simplex.length = 0; simplex.push(a, c);
129
+ setDir(dOut, tripleProduct(ac, ao, ac));
130
+ return false;
131
+ }
132
+ return starCase(simplex, a, b, ao, dOut);
133
+ }
134
+ if (dot(cross(ab, abc), ao) > 0) return starCase(simplex, a, b, ao, dOut);
135
+
136
+ // origin within the triangle's prism — above or below
137
+ if (dot(abc, ao) > 0) { simplex.length = 0; simplex.push(a, b, c); setDir(dOut, abc); }
138
+ else { simplex.length = 0; simplex.push(a, c, b); setDir(dOut, neg(abc)); }
139
+ return false;
140
+ }
141
+
142
+ function starCase(simplex, a, b, ao, dOut) {
143
+ const ab = sub(b, a);
144
+ if (dot(ab, ao) > 0) {
145
+ simplex.length = 0; simplex.push(a, b);
146
+ setDir(dOut, tripleProduct(ab, ao, ab));
147
+ } else {
148
+ simplex.length = 0; simplex.push(a);
149
+ setDir(dOut, ao);
150
+ }
151
+ return false;
152
+ }
153
+
154
+ function tetraCase(simplex, dOut) {
155
+ const a = simplex[0], b = simplex[1], c = simplex[2], d = simplex[3];
156
+ const ab = sub(b, a), ac = sub(c, a), ad = sub(d, a), ao = neg(a);
157
+ const abc = cross(ab, ac);
158
+ const acd = cross(ac, ad);
159
+ const adb = cross(ad, ab);
160
+
161
+ if (dot(abc, ao) > 0) { simplex.length = 0; simplex.push(a, b, c); return triangleCase(simplex, dOut); }
162
+ if (dot(acd, ao) > 0) { simplex.length = 0; simplex.push(a, c, d); return triangleCase(simplex, dOut); }
163
+ if (dot(adb, ao) > 0) { simplex.length = 0; simplex.push(a, d, b); return triangleCase(simplex, dOut); }
164
+ return true; // origin inside the tetrahedron
165
+ }
166
+
167
+ /**
168
+ * EPA over the Minkowski difference, seeded by a rank-4 enclosing simplex.
169
+ * Writes the unit penetration normal (B→A) into `out` and returns the depth.
170
+ */
171
+ function epa(A, B, simplex, out) {
172
+ const verts = simplex.map((p) => [p[0], p[1], p[2]]);
173
+ // centroid for outward orientation of the initial faces
174
+ const cen = [(verts[0][0] + verts[1][0] + verts[2][0] + verts[3][0]) / 4,
175
+ (verts[0][1] + verts[1][1] + verts[2][1] + verts[3][1]) / 4,
176
+ (verts[0][2] + verts[1][2] + verts[2][2] + verts[3][2]) / 4];
177
+
178
+ let faces = [[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2]].map((f) => makeFace(verts, f, cen));
179
+
180
+ for (let iter = 0; iter < EPA_MAX_ITER; iter++) {
181
+ // closest face to the origin
182
+ let best = faces[0];
183
+ for (let i = 1; i < faces.length; i++) if (faces[i].dist < best.dist) best = faces[i];
184
+
185
+ const w = csoSupport(A, B, best.n);
186
+ const wdist = dot(best.n, w) - best.dist;
187
+ if (wdist < EPA_EPS) break; // converged onto the closest face
188
+
189
+ const wi = verts.length;
190
+ verts.push(w);
191
+
192
+ // remove faces visible from w; collect horizon edges (those on exactly one removed face)
193
+ const edges = [];
194
+ const kept = [];
195
+ for (const f of faces) {
196
+ if (dot(f.n, w) - f.dist > EPA_EPS) {
197
+ addEdge(edges, f.v[0], f.v[1]);
198
+ addEdge(edges, f.v[1], f.v[2]);
199
+ addEdge(edges, f.v[2], f.v[0]);
200
+ } else kept.push(f);
201
+ }
202
+ faces = kept;
203
+ for (const [i, j] of edges) faces.push(makeFace(verts, [i, j, wi], cen));
204
+ if (faces.length === 0) break; // degenerate polytope (all faces visible + horizon cancelled)
205
+ }
206
+
207
+ // recompute the closest face (faces may have changed on the last iteration)
208
+ if (faces.length === 0) {
209
+ // Degenerate: no face survived. Report no usable axis (caller treats a
210
+ // non-positive depth as a miss) rather than dereferencing faces[0] and
211
+ // throwing a TypeError out of the narrowphase step.
212
+ out[0] = 0; out[1] = 0; out[2] = 0;
213
+ return 0;
214
+ }
215
+ let best = faces[0];
216
+ for (let i = 1; i < faces.length; i++) if (faces[i].dist < best.dist) best = faces[i];
217
+ out[0] = best.n[0]; out[1] = best.n[1]; out[2] = best.n[2];
218
+ return best.dist;
219
+ }
220
+
221
+ /** Build a face with an OUTWARD unit normal (away from the polytope centroid). */
222
+ function makeFace(verts, idx, cen) {
223
+ const a = verts[idx[0]], b = verts[idx[1]], c = verts[idx[2]];
224
+ let n = cross(sub(b, a), sub(c, a));
225
+ const l = Math.sqrt(lensq(n)) || 1;
226
+ n = [n[0] / l, n[1] / l, n[2] / l];
227
+ // orient outward: normal should point away from the polytope interior (centroid)
228
+ if (dot(n, sub(a, cen)) < 0) { n = neg(n); const t = idx[1]; idx = [idx[0], idx[2], t]; }
229
+ return { v: idx, n, dist: dot(n, a) }; // perpendicular distance from origin (≥0 once outward)
230
+ }
231
+
232
+ /** Add an edge with horizon cancellation: if the reverse edge is present, drop both. */
233
+ function addEdge(edges, i, j) {
234
+ for (let k = 0; k < edges.length; k++) {
235
+ if (edges[k][0] === j && edges[k][1] === i) { edges.splice(k, 1); return; }
236
+ }
237
+ edges.push([i, j]);
238
+ }
239
+
240
+ /**
241
+ * Penetration depth + normal between two convex support providers at world pose.
242
+ * Writes the unit separation normal (B→A) into `out` and returns the depth
243
+ * (positive on overlap, 0 if separated — `out` untouched on 0).
244
+ *
245
+ * @param {Float64Array|number[]} out length ≥ 3
246
+ * @param {{support:Function}} A world-space support provider (e.g. PosedShape)
247
+ * @param {{support:Function}} B
248
+ * @returns {number} penetration depth, or 0 if separated
249
+ */
250
+ export function gjk_epa_penetration(out, A, B) {
251
+ const simplex = gjk(A, B);
252
+ if (simplex === null || simplex.length < 4) return 0;
253
+ const depth = epa(A, B, simplex, out);
254
+ return depth > 0 && Number.isFinite(depth) ? depth : 0;
255
+ }
@@ -15,15 +15,10 @@
15
15
  * and shape support implementations (sphere especially) don't bother
16
16
  * to renormalise internally.
17
17
  *
18
- * NOTE: EPA (`expanding_polytope_algorithm.js`) computes the Minkowski
19
- * support inline rather than calling this. Its convention is the
20
- * opposite sign (B−A rather than A−B), and it skips the normalisation
21
- * because the search directions it constructs from face normals are
22
- * already unit-length. The narrowphase consumer of EPA's output then
23
- * negates the result to get the contact normal in the "B → A" stored
24
- * convention. Folding EPA onto this helper would require both flipping
25
- * its sign convention and changing the narrowphase call site — not
26
- * worth the churn for one call site.
18
+ * NOTE: the robust penetration query (`gjk_epa_penetration.js`) computes
19
+ * its own Minkowski-difference support inline rather than calling this
20
+ * helper it is self-contained (its own GJK + EPA) and works on world-
21
+ * space support providers, so it does not share this body-space path.
27
22
  *
28
23
  * @param {Float64Array|number[]} result 3-vector destination
29
24
  * @param {number} result_offset
@@ -1 +1 @@
1
- {"version":3,"file":"minkowski_support.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/gjk/minkowski_support.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,0CARW,YAAY,GAAC,MAAM,EAAE,iBACrB,MAAM,6DAGN,MAAM,SACN,MAAM,SACN,MAAM,QA2BhB"}
1
+ {"version":3,"file":"minkowski_support.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/gjk/minkowski_support.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,0CARW,YAAY,GAAC,MAAM,EAAE,iBACrB,MAAM,6DAGN,MAAM,SACN,MAAM,SACN,MAAM,QA2BhB"}