@woosh/meep-engine 2.145.0 → 2.147.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/geom/3d/shape/HeightMapShape3D.d.ts +33 -3
  3. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
  4. package/src/core/geom/3d/shape/HeightMapShape3D.js +486 -451
  5. package/src/engine/control/first-person/DESIGN_COLLISION.md +365 -352
  6. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +1 -14
  7. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  8. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +20 -8
  9. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  10. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +552 -546
  11. package/src/engine/control/first-person/TODO.md +13 -11
  12. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts +8 -3
  13. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts.map +1 -1
  14. package/src/engine/control/first-person/abilities/LedgeGrab.js +213 -199
  15. package/src/engine/control/first-person/abilities/Mantle.d.ts.map +1 -1
  16. package/src/engine/control/first-person/abilities/Mantle.js +195 -188
  17. package/src/engine/control/first-person/abilities/WallJump.d.ts.map +1 -1
  18. package/src/engine/control/first-person/abilities/WallJump.js +11 -3
  19. package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
  20. package/src/engine/control/first-person/abilities/WallRun.js +183 -163
  21. package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
  22. package/src/engine/control/first-person/collision/KinematicMover.js +634 -592
  23. package/src/engine/control/first-person/prototype_first_person_controller.js +1003 -901
  24. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts +9 -0
  25. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts.map +1 -1
  26. package/src/engine/control/first-person/sensors/FirstPersonSensors.js +87 -77
  27. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts +8 -0
  28. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts.map +1 -1
  29. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js +229 -196
  30. package/src/engine/ecs/EntityManager.d.ts +34 -11
  31. package/src/engine/ecs/EntityManager.d.ts.map +1 -1
  32. package/src/engine/ecs/EntityManager.js +71 -42
  33. package/src/engine/interpolation/BinaryInterpolationAdapter.d.ts.map +1 -0
  34. package/src/engine/interpolation/Interpoland.d.ts +48 -0
  35. package/src/engine/interpolation/Interpoland.d.ts.map +1 -0
  36. package/src/engine/interpolation/Interpoland.js +49 -0
  37. package/src/engine/interpolation/Interpolated.d.ts +101 -0
  38. package/src/engine/interpolation/Interpolated.d.ts.map +1 -0
  39. package/src/engine/interpolation/Interpolated.js +149 -0
  40. package/src/engine/{network/sim → interpolation}/InterpolationLog.d.ts +1 -1
  41. package/src/engine/interpolation/InterpolationLog.d.ts.map +1 -0
  42. package/src/engine/{network/sim → interpolation}/InterpolationLog.js +2 -2
  43. package/src/engine/interpolation/InterpolationSystem.d.ts +116 -0
  44. package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -0
  45. package/src/engine/interpolation/InterpolationSystem.js +233 -0
  46. package/src/engine/interpolation/PoseInterpolationAdapter.d.ts +17 -0
  47. package/src/engine/interpolation/PoseInterpolationAdapter.d.ts.map +1 -0
  48. package/src/engine/interpolation/PoseInterpolationAdapter.js +61 -0
  49. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +35 -0
  50. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -0
  51. package/src/engine/interpolation/TransformPoseSerializationAdapter.js +57 -0
  52. package/src/engine/interpolation/pose_interpoland.d.ts +18 -0
  53. package/src/engine/interpolation/pose_interpoland.d.ts.map +1 -0
  54. package/src/engine/interpolation/pose_interpoland.js +27 -0
  55. package/src/engine/network/NetworkSession.d.ts +2 -2
  56. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  57. package/src/engine/network/NetworkSession.js +2 -2
  58. package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts +1 -1
  59. package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts.map +1 -1
  60. package/src/engine/network/adapters/QuaternionInterpolationAdapter.js +1 -1
  61. package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts +1 -1
  62. package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts.map +1 -1
  63. package/src/engine/network/adapters/TransformInterpolationAdapter.js +1 -1
  64. package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts +1 -1
  65. package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts.map +1 -1
  66. package/src/engine/network/adapters/Vector3InterpolationAdapter.js +1 -1
  67. package/src/engine/physics/INTEPOLATION_SYSTEM_PLAN.md +287 -0
  68. package/src/engine/physics/PLAN.md +944 -809
  69. package/src/engine/physics/body/BodyStorage.d.ts +9 -0
  70. package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
  71. package/src/engine/physics/body/BodyStorage.js +23 -0
  72. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  73. package/src/engine/physics/broadphase/generate_pairs.js +7 -0
  74. package/src/engine/physics/ccd/linear_sweep.d.ts +97 -0
  75. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -0
  76. package/src/engine/physics/ccd/linear_sweep.js +238 -0
  77. package/src/engine/physics/ecs/PhysicsSystem.d.ts +82 -3
  78. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  79. package/src/engine/physics/ecs/PhysicsSystem.js +227 -8
  80. package/src/engine/physics/ecs/RigidBodyFlags.d.ts +6 -0
  81. package/src/engine/physics/ecs/RigidBodyFlags.d.ts.map +1 -1
  82. package/src/engine/physics/ecs/RigidBodyFlags.js +6 -0
  83. package/src/engine/physics/narrowphase/box_triangle_contact.js +814 -811
  84. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
  85. package/src/engine/physics/narrowphase/compute_penetration.js +325 -323
  86. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +27 -8
  87. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -1
  88. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +235 -204
  89. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  90. package/src/engine/physics/narrowphase/narrowphase_step.js +97 -13
  91. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  92. package/src/engine/physics/queries/overlap_shape.js +185 -183
  93. package/src/engine/simulation/Ticker.d.ts +14 -0
  94. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  95. package/src/engine/simulation/Ticker.js +136 -1
  96. package/src/engine/network/sim/BinaryInterpolationAdapter.d.ts.map +0 -1
  97. package/src/engine/network/sim/InterpolationLog.d.ts.map +0 -1
  98. /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.d.ts +0 -0
  99. /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.js +0 -0
@@ -133,6 +133,18 @@ const prev_claimed = new Uint8Array(MAX_CONTACTS_PER_MANIFOLD);
133
133
  */
134
134
  const cand_to_prev = new Int32Array(MAX_KEPT);
135
135
 
136
+ /**
137
+ * Per-candidate "already claimed" flags for {@link redetect_pair_geometry}'s
138
+ * 1:1 existing-contact → fresh-candidate matching. Sized to the candidate
139
+ * buffer capacity (64) so it covers any per-pair candidate count. Without it,
140
+ * several existing contacts that share one triangle's `feature_id` (the
141
+ * box/capsule-triangle paths emit multiple contacts per triangle) would all
142
+ * match the same first candidate, collapsing the manifold to duplicate witness
143
+ * points.
144
+ * @type {Uint8Array}
145
+ */
146
+ const redetect_claimed = new Uint8Array(64);
147
+
136
148
  /**
137
149
  * Per body-pair scratch buffer for candidate contacts produced by the
138
150
  * cross-product of A's colliders × B's colliders. Sized generously for
@@ -148,6 +160,13 @@ const candidates = new Float64Array(64 * CANDIDATE_STRIDE);
148
160
  * the convex-side body, so a single concave-vs-convex pair typically
149
161
  * yields tens of triangles, not thousands. 1024 is the safety cap.
150
162
  *
163
+ * For a heightmap the per-cell triangle count scales O(N²) with the
164
+ * shape's {@link HeightMapShape3D#tessellation} (a sub-cell quad is 2
165
+ * triangles, and there are N×N sub-cells per sampler cell). The bounded
166
+ * query AABB keeps the cell count small, so a typical pair stays well
167
+ * inside 1024 at moderate tessellation; the silent-drop backstop below
168
+ * covers any overflow at extreme values.
169
+ *
151
170
  * If an enumerator's output would exceed this, the extra triangles are
152
171
  * silently dropped by the enumerator's bounds-check on the output
153
172
  * array — the worst case is a missed contact on a far edge of the
@@ -1606,26 +1625,91 @@ export function redetect_pair_geometry(manifolds, slot, list_a, list_b) {
1606
1625
 
1607
1626
  const data = manifolds.data_buffer;
1608
1627
  const slot_off = manifolds.slot_data_offset(slot);
1628
+
1629
+ // Match each existing contact to a DISTINCT fresh candidate. feature_id
1630
+ // identifies the TRIANGLE, not the contact point — the box/capsule-triangle
1631
+ // paths emit several contacts for one triangle, all sharing that triangle's
1632
+ // single fid (a flat box-on-heightmap cell yields fids like [6,6,6,7]). A
1633
+ // plain first-match-by-fid therefore maps every same-fid existing contact
1634
+ // onto the SAME candidate, collapsing the manifold to duplicate witness
1635
+ // points → a degenerate support polygon the solver can't damp (the
1636
+ // box-on-heightmap rattle). So: gate by fid, disambiguate same-fid
1637
+ // candidates by NEAREST previous witness (world-A) position, and claim each
1638
+ // candidate so no two existing contacts take the same one. For the common
1639
+ // unique-fid case (sphere/mesh: one contact per triangle) this picks that
1640
+ // single candidate exactly once — identical to the old behaviour.
1641
+ for (let k = 0; k < cc; k++) redetect_claimed[k] = 0;
1642
+
1609
1643
  for (let j = 0; j < count; j++) {
1610
1644
  const off = slot_off + j * CONTACT_STRIDE;
1611
1645
  const fid = data[off + 13];
1612
1646
  if (fid === 0) continue; // no feature info to match on
1647
+
1648
+ // Previous witness (world-A) of this existing contact — the anchor we
1649
+ // disambiguate same-fid candidates against.
1650
+ const pax = data[off];
1651
+ const pay = data[off + 1];
1652
+ const paz = data[off + 2];
1653
+
1654
+ let best_k = -1;
1655
+ let best_d2 = Infinity;
1613
1656
  for (let k = 0; k < cc; k++) {
1657
+ if (redetect_claimed[k] === 1) continue;
1614
1658
  const co = k * CANDIDATE_STRIDE;
1615
- if (candidates[co + 10] === fid) {
1616
- // Overwrite geometry only: witnesses, normal, depth.
1617
- data[off] = candidates[co];
1618
- data[off + 1] = candidates[co + 1];
1619
- data[off + 2] = candidates[co + 2];
1620
- data[off + 3] = candidates[co + 3];
1621
- data[off + 4] = candidates[co + 4];
1622
- data[off + 5] = candidates[co + 5];
1623
- data[off + 6] = candidates[co + 6];
1624
- data[off + 7] = candidates[co + 7];
1625
- data[off + 8] = candidates[co + 8];
1626
- data[off + 9] = candidates[co + 9];
1627
- break;
1659
+ if (candidates[co + 10] !== fid) continue;
1660
+ const dx = candidates[co] - pax;
1661
+ const dy = candidates[co + 1] - pay;
1662
+ const dz = candidates[co + 2] - paz;
1663
+ const d2 = dx * dx + dy * dy + dz * dz;
1664
+ if (d2 < best_d2) {
1665
+ best_d2 = d2;
1666
+ best_k = k;
1628
1667
  }
1629
1668
  }
1669
+
1670
+ // Position fallback (mirrors narrowphase_step's match-and-merge). A
1671
+ // single triangle's clipped contact count is NOT stable across sub-mm
1672
+ // pose changes — a box straddling a cell seam can have one triangle
1673
+ // yield 4 points one substep and 3 the next — so an existing contact can
1674
+ // outnumber this substep's same-fid candidates. Freezing its stale
1675
+ // witness then duplicates a sibling contact's point, leaving a
1676
+ // degenerate (sub-dimensional) manifold. Instead, claim the nearest
1677
+ // unclaimed candidate of ANY fid: every contact keeps a DISTINCT live
1678
+ // witness. The fid label is left intact and re-resolved by the next
1679
+ // once-per-step narrowphase. (At a fixed pose the same-fid match always
1680
+ // succeeds, so this never fires for the depth-equality guards or the
1681
+ // one-contact-per-triangle sphere/mesh paths.)
1682
+ if (best_k === -1) {
1683
+ for (let k = 0; k < cc; k++) {
1684
+ if (redetect_claimed[k] === 1) continue;
1685
+ const co = k * CANDIDATE_STRIDE;
1686
+ const dx = candidates[co] - pax;
1687
+ const dy = candidates[co + 1] - pay;
1688
+ const dz = candidates[co + 2] - paz;
1689
+ const d2 = dx * dx + dy * dy + dz * dz;
1690
+ if (d2 < best_d2) {
1691
+ best_d2 = d2;
1692
+ best_k = k;
1693
+ }
1694
+ }
1695
+ }
1696
+
1697
+ if (best_k === -1) continue; // no unclaimed candidate at all this substep — keep frozen geometry
1698
+
1699
+ redetect_claimed[best_k] = 1;
1700
+ const co = best_k * CANDIDATE_STRIDE;
1701
+ // Overwrite geometry only: witnesses, normal, depth. (Count, feature
1702
+ // ids and accumulated impulses are intentionally left untouched — see
1703
+ // the function contract.)
1704
+ data[off] = candidates[co];
1705
+ data[off + 1] = candidates[co + 1];
1706
+ data[off + 2] = candidates[co + 2];
1707
+ data[off + 3] = candidates[co + 3];
1708
+ data[off + 4] = candidates[co + 4];
1709
+ data[off + 5] = candidates[co + 5];
1710
+ data[off + 6] = candidates[co + 6];
1711
+ data[off + 7] = candidates[co + 7];
1712
+ data[off + 8] = candidates[co + 8];
1713
+ data[off + 9] = candidates[co + 9];
1630
1714
  }
1631
1715
  }
@@ -1 +1 @@
1
- {"version":3,"file":"overlap_shape.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/queries/overlap_shape.js"],"names":[],"mappings":"AA4CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,uFAZW;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,YAE5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,UAErC,WAAW,GAAC,MAAM,EAAE,iBAEpB,MAAM,oBACE,MAAM,yBAAsB,OAAO,GAEzC,MAAM,CAqGlB"}
1
+ {"version":3,"file":"overlap_shape.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/queries/overlap_shape.js"],"names":[],"mappings":"AA8CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,uFAZW;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,YAE5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,UAErC,WAAW,GAAC,MAAM,EAAE,iBAEpB,MAAM,oBACE,MAAM,yBAAsB,OAAO,GAEzC,MAAM,CAqGlB"}
@@ -1,183 +1,185 @@
1
- import { bvh_query_user_data_overlaps_aabb } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js";
2
- import { returnTrue } from "../../../core/function/returnTrue.js";
3
- import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
4
- import { Triangle3D } from "../../../core/geom/3d/shape/Triangle3D.js";
5
- import { body_id_index } from "../body/BodyStorage.js";
6
- import { gjk } from "../gjk/gjk.js";
7
- import { aabb_world_to_local } from "../narrowphase/decomposition/aabb_world_to_local.js";
8
- import { decompose_to_triangles } from "../narrowphase/decomposition/decompose_to_triangles.js";
9
- import { TRIANGLE_FLOAT_STRIDE } from "../narrowphase/decomposition/triangle_buffer_layout.js";
10
- import { PosedShape } from "../narrowphase/PosedShape.js";
11
-
12
- /**
13
- * Scratch state — module-scoped to avoid per-query allocation. Safe
14
- * because PhysicsSystem queries run on the main thread, sequentially.
15
- */
16
- const local_aabb = new Float64Array(6);
17
- const world_aabb = new Float64Array(6);
18
- const concave_query_aabb = new Float64Array(6);
19
- const simplex_buf = new Float64Array(12);
20
-
21
- const query_posed = new PosedShape();
22
- const candidate_posed = new PosedShape();
23
- const triangle_shape = new Triangle3D();
24
-
25
- /**
26
- * Maximum triangles a concave candidate can emit per overlap pair.
27
- * Same rationale as the narrowphase's `MAX_TRIANGLES_PER_PAIR`: the
28
- * broadphase has already bounded the query AABB to the query shape's
29
- * envelope, so a single candidate typically yields tens of triangles.
30
- * Excess triangles are dropped by the enumerator's bounds check —
31
- * worst case is a missed overlap on a far edge of the candidate's
32
- * geometry, recovered next query.
33
- * @type {number}
34
- */
35
- const MAX_TRIANGLES_PER_PAIR = 1024;
36
-
37
- const triangle_buffer = new Float64Array(MAX_TRIANGLES_PER_PAIR * TRIANGLE_FLOAT_STRIDE);
38
-
39
- /**
40
- * Broadphase candidate buffer. Grows by doubling on overflow.
41
- * @type {Uint32Array}
42
- */
43
- let scratch_candidates = new Uint32Array(64);
44
-
45
- /**
46
- * Test what bodies overlap a convex shape placed at a given pose. Each
47
- * overlapping body's `body_id` is written to `output` starting at
48
- * `output_offset`; the function returns the number of body ids written.
49
- *
50
- * Use case: speculative physics queries for kinematic / character
51
- * controllers. An external system can ask "would my body collide with
52
- * anything if I moved it here?" without committing a tick of
53
- * simulation. The output is a flat list of body ids so the caller can
54
- * decide what to do per hit (skip, push, slide, etc.).
55
- *
56
- * The pipeline mirrors the narrowphase pair test:
57
- * 1. Build the query shape's world AABB.
58
- * 2. Pull candidates from both broadphase trees that overlap that AABB.
59
- * 3. For each candidate, run GJK in world frame. Convex candidates
60
- * go through one GJK call; concave candidates (heightmap / mesh)
61
- * go through the per-triangle decomposition path.
62
- * 4. Apply the optional `filter` callback (same signature as in
63
- * raycast / shapeCast) before the GJK test — early-out on bodies
64
- * the caller already wants to skip (themselves, allies, etc.).
65
- *
66
- * The query shape must be convex (`is_convex === true`). Concave shapes
67
- * are typically static terrain and not used as kinematic query
68
- * probes; rejecting them avoids the M×N triangle-pair cost.
69
- *
70
- * @param {PhysicsSystem} system
71
- * @param {AbstractShape3D} shape query shape, convex; expressed in
72
- * its own local frame
73
- * @param {{x:number,y:number,z:number}} position world position of the
74
- * query shape
75
- * @param {{x:number,y:number,z:number,w:number}} rotation world rotation
76
- * of the query shape (unit quaternion)
77
- * @param {Uint32Array|number[]} output buffer to write body_ids into.
78
- * Caller is responsible for sizing it; ids past its end are dropped.
79
- * @param {number} output_offset float-index in output to start writing at
80
- * @param {(entity:number, collider:Collider)=>boolean} [filter]
81
- * defaults to {@link returnTrue} (accept every candidate)
82
- * @returns {number} number of overlapping bodies written
83
- * @throws {Error} if `shape.is_convex === false`
84
- */
85
- export function overlap_shape(system, shape, position, rotation, output, output_offset, filter = returnTrue) {
86
- if (shape.is_convex === false) {
87
- throw new Error(`overlap_shape: query shape must be convex; received \`${shape.constructor.name}\` (is_convex=false)`);
88
- }
89
-
90
- // ── 1. Query shape's world AABB ─────────────────────────────────
91
- shape.compute_bounding_box(local_aabb);
92
- aabb3_transform_oriented(
93
- world_aabb, 0,
94
- local_aabb[0], local_aabb[1], local_aabb[2],
95
- local_aabb[3], local_aabb[4], local_aabb[5],
96
- position.x, position.y, position.z,
97
- rotation.x, rotation.y, rotation.z, rotation.w
98
- );
99
-
100
- // ── 2. Gather broadphase candidates ─────────────────────────────
101
- const n_static = bvh_query_user_data_overlaps_aabb(
102
- scratch_candidates, 0, system.staticBvh, world_aabb
103
- );
104
- const n_dynamic = bvh_query_user_data_overlaps_aabb(
105
- scratch_candidates, n_static, system.dynamicBvh, world_aabb
106
- );
107
- const n_total = n_static + n_dynamic;
108
- if (n_total === 0) return 0;
109
-
110
- // ── 3. Set up query PosedShape (constant across candidates) ─────
111
- query_posed.setup(shape, position, rotation);
112
-
113
- // ── 4. Per-candidate narrowphase ────────────────────────────────
114
- const output_capacity = output.length - output_offset;
115
- let count = 0;
116
- let cursor = output_offset;
117
-
118
- for (let i = 0; i < n_total; i++) {
119
- if (count >= output_capacity) break;
120
-
121
- const body_id = scratch_candidates[i];
122
- const body_idx = body_id_index(body_id);
123
-
124
- const entity = system.entityOf(body_id);
125
- if (entity < 0) continue;
126
-
127
- const collider = system.__primary_collider(body_idx);
128
- if (collider === null) continue;
129
- if (!filter(entity, collider)) continue;
130
-
131
- const candidate_tr = system.__transforms[body_idx];
132
-
133
- let overlaps = false;
134
-
135
- if (collider.shape.is_convex !== false) {
136
- candidate_posed.setup(collider.shape, candidate_tr.position, candidate_tr.rotation);
137
- overlaps = gjk(simplex_buf, query_posed, candidate_posed);
138
- } else {
139
- // Concave candidate: project the query's world AABB into
140
- // the candidate's body-local frame, decompose to triangles,
141
- // run per-triangle GJK until one overlap is found.
142
- aabb_world_to_local(
143
- concave_query_aabb, 0,
144
- world_aabb,
145
- candidate_tr.position.x, candidate_tr.position.y, candidate_tr.position.z,
146
- candidate_tr.rotation.x, candidate_tr.rotation.y, candidate_tr.rotation.z, candidate_tr.rotation.w
147
- );
148
-
149
- const tri_count = decompose_to_triangles(
150
- triangle_buffer, 0, collider.shape,
151
- concave_query_aabb[0], concave_query_aabb[1], concave_query_aabb[2],
152
- concave_query_aabb[3], concave_query_aabb[4], concave_query_aabb[5]
153
- );
154
-
155
- // Re-pose candidate as the concave body, rebinding the
156
- // flyweight triangle per iteration.
157
- candidate_posed.shape = triangle_shape;
158
- candidate_posed.px = candidate_tr.position.x;
159
- candidate_posed.py = candidate_tr.position.y;
160
- candidate_posed.pz = candidate_tr.position.z;
161
- candidate_posed.qx = candidate_tr.rotation.x;
162
- candidate_posed.qy = candidate_tr.rotation.y;
163
- candidate_posed.qz = candidate_tr.rotation.z;
164
- candidate_posed.qw = candidate_tr.rotation.w;
165
-
166
- for (let t = 0; t < tri_count; t++) {
167
- triangle_shape.bind(triangle_buffer, t * TRIANGLE_FLOAT_STRIDE);
168
- if (gjk(simplex_buf, query_posed, candidate_posed)) {
169
- overlaps = true;
170
- break;
171
- }
172
- }
173
- }
174
-
175
- if (overlaps) {
176
- output[cursor] = body_id;
177
- cursor++;
178
- count++;
179
- }
180
- }
181
-
182
- return count;
183
- }
1
+ import { bvh_query_user_data_overlaps_aabb } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js";
2
+ import { returnTrue } from "../../../core/function/returnTrue.js";
3
+ import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
4
+ import { Triangle3D } from "../../../core/geom/3d/shape/Triangle3D.js";
5
+ import { body_id_index } from "../body/BodyStorage.js";
6
+ import { gjk } from "../gjk/gjk.js";
7
+ import { aabb_world_to_local } from "../narrowphase/decomposition/aabb_world_to_local.js";
8
+ import { decompose_to_triangles } from "../narrowphase/decomposition/decompose_to_triangles.js";
9
+ import { TRIANGLE_FLOAT_STRIDE } from "../narrowphase/decomposition/triangle_buffer_layout.js";
10
+ import { PosedShape } from "../narrowphase/PosedShape.js";
11
+
12
+ /**
13
+ * Scratch state — module-scoped to avoid per-query allocation. Safe
14
+ * because PhysicsSystem queries run on the main thread, sequentially.
15
+ */
16
+ const local_aabb = new Float64Array(6);
17
+ const world_aabb = new Float64Array(6);
18
+ const concave_query_aabb = new Float64Array(6);
19
+ const simplex_buf = new Float64Array(12);
20
+
21
+ const query_posed = new PosedShape();
22
+ const candidate_posed = new PosedShape();
23
+ const triangle_shape = new Triangle3D();
24
+
25
+ /**
26
+ * Maximum triangles a concave candidate can emit per overlap pair.
27
+ * Same rationale as the narrowphase's `MAX_TRIANGLES_PER_PAIR`: the
28
+ * broadphase has already bounded the query AABB to the query shape's
29
+ * envelope, so a single candidate typically yields tens of triangles
30
+ * (a heightmap's count scales O(N²) with its `tessellation`, still
31
+ * inside the buffer for a bounded query at moderate tessellation).
32
+ * Excess triangles are dropped by the enumerator's bounds check —
33
+ * worst case is a missed overlap on a far edge of the candidate's
34
+ * geometry, recovered next query.
35
+ * @type {number}
36
+ */
37
+ const MAX_TRIANGLES_PER_PAIR = 1024;
38
+
39
+ const triangle_buffer = new Float64Array(MAX_TRIANGLES_PER_PAIR * TRIANGLE_FLOAT_STRIDE);
40
+
41
+ /**
42
+ * Broadphase candidate buffer. Grows by doubling on overflow.
43
+ * @type {Uint32Array}
44
+ */
45
+ let scratch_candidates = new Uint32Array(64);
46
+
47
+ /**
48
+ * Test what bodies overlap a convex shape placed at a given pose. Each
49
+ * overlapping body's `body_id` is written to `output` starting at
50
+ * `output_offset`; the function returns the number of body ids written.
51
+ *
52
+ * Use case: speculative physics queries for kinematic / character
53
+ * controllers. An external system can ask "would my body collide with
54
+ * anything if I moved it here?" without committing a tick of
55
+ * simulation. The output is a flat list of body ids so the caller can
56
+ * decide what to do per hit (skip, push, slide, etc.).
57
+ *
58
+ * The pipeline mirrors the narrowphase pair test:
59
+ * 1. Build the query shape's world AABB.
60
+ * 2. Pull candidates from both broadphase trees that overlap that AABB.
61
+ * 3. For each candidate, run GJK in world frame. Convex candidates
62
+ * go through one GJK call; concave candidates (heightmap / mesh)
63
+ * go through the per-triangle decomposition path.
64
+ * 4. Apply the optional `filter` callback (same signature as in
65
+ * raycast / shapeCast) before the GJK test — early-out on bodies
66
+ * the caller already wants to skip (themselves, allies, etc.).
67
+ *
68
+ * The query shape must be convex (`is_convex === true`). Concave shapes
69
+ * are typically static terrain and not used as kinematic query
70
+ * probes; rejecting them avoids the M×N triangle-pair cost.
71
+ *
72
+ * @param {PhysicsSystem} system
73
+ * @param {AbstractShape3D} shape query shape, convex; expressed in
74
+ * its own local frame
75
+ * @param {{x:number,y:number,z:number}} position world position of the
76
+ * query shape
77
+ * @param {{x:number,y:number,z:number,w:number}} rotation world rotation
78
+ * of the query shape (unit quaternion)
79
+ * @param {Uint32Array|number[]} output buffer to write body_ids into.
80
+ * Caller is responsible for sizing it; ids past its end are dropped.
81
+ * @param {number} output_offset float-index in output to start writing at
82
+ * @param {(entity:number, collider:Collider)=>boolean} [filter]
83
+ * defaults to {@link returnTrue} (accept every candidate)
84
+ * @returns {number} number of overlapping bodies written
85
+ * @throws {Error} if `shape.is_convex === false`
86
+ */
87
+ export function overlap_shape(system, shape, position, rotation, output, output_offset, filter = returnTrue) {
88
+ if (shape.is_convex === false) {
89
+ throw new Error(`overlap_shape: query shape must be convex; received \`${shape.constructor.name}\` (is_convex=false)`);
90
+ }
91
+
92
+ // ── 1. Query shape's world AABB ─────────────────────────────────
93
+ shape.compute_bounding_box(local_aabb);
94
+ aabb3_transform_oriented(
95
+ world_aabb, 0,
96
+ local_aabb[0], local_aabb[1], local_aabb[2],
97
+ local_aabb[3], local_aabb[4], local_aabb[5],
98
+ position.x, position.y, position.z,
99
+ rotation.x, rotation.y, rotation.z, rotation.w
100
+ );
101
+
102
+ // ── 2. Gather broadphase candidates ─────────────────────────────
103
+ const n_static = bvh_query_user_data_overlaps_aabb(
104
+ scratch_candidates, 0, system.staticBvh, world_aabb
105
+ );
106
+ const n_dynamic = bvh_query_user_data_overlaps_aabb(
107
+ scratch_candidates, n_static, system.dynamicBvh, world_aabb
108
+ );
109
+ const n_total = n_static + n_dynamic;
110
+ if (n_total === 0) return 0;
111
+
112
+ // ── 3. Set up query PosedShape (constant across candidates) ─────
113
+ query_posed.setup(shape, position, rotation);
114
+
115
+ // ── 4. Per-candidate narrowphase ────────────────────────────────
116
+ const output_capacity = output.length - output_offset;
117
+ let count = 0;
118
+ let cursor = output_offset;
119
+
120
+ for (let i = 0; i < n_total; i++) {
121
+ if (count >= output_capacity) break;
122
+
123
+ const body_id = scratch_candidates[i];
124
+ const body_idx = body_id_index(body_id);
125
+
126
+ const entity = system.entityOf(body_id);
127
+ if (entity < 0) continue;
128
+
129
+ const collider = system.__primary_collider(body_idx);
130
+ if (collider === null) continue;
131
+ if (!filter(entity, collider)) continue;
132
+
133
+ const candidate_tr = system.__transforms[body_idx];
134
+
135
+ let overlaps = false;
136
+
137
+ if (collider.shape.is_convex !== false) {
138
+ candidate_posed.setup(collider.shape, candidate_tr.position, candidate_tr.rotation);
139
+ overlaps = gjk(simplex_buf, query_posed, candidate_posed);
140
+ } else {
141
+ // Concave candidate: project the query's world AABB into
142
+ // the candidate's body-local frame, decompose to triangles,
143
+ // run per-triangle GJK until one overlap is found.
144
+ aabb_world_to_local(
145
+ concave_query_aabb, 0,
146
+ world_aabb,
147
+ candidate_tr.position.x, candidate_tr.position.y, candidate_tr.position.z,
148
+ candidate_tr.rotation.x, candidate_tr.rotation.y, candidate_tr.rotation.z, candidate_tr.rotation.w
149
+ );
150
+
151
+ const tri_count = decompose_to_triangles(
152
+ triangle_buffer, 0, collider.shape,
153
+ concave_query_aabb[0], concave_query_aabb[1], concave_query_aabb[2],
154
+ concave_query_aabb[3], concave_query_aabb[4], concave_query_aabb[5]
155
+ );
156
+
157
+ // Re-pose candidate as the concave body, rebinding the
158
+ // flyweight triangle per iteration.
159
+ candidate_posed.shape = triangle_shape;
160
+ candidate_posed.px = candidate_tr.position.x;
161
+ candidate_posed.py = candidate_tr.position.y;
162
+ candidate_posed.pz = candidate_tr.position.z;
163
+ candidate_posed.qx = candidate_tr.rotation.x;
164
+ candidate_posed.qy = candidate_tr.rotation.y;
165
+ candidate_posed.qz = candidate_tr.rotation.z;
166
+ candidate_posed.qw = candidate_tr.rotation.w;
167
+
168
+ for (let t = 0; t < tri_count; t++) {
169
+ triangle_shape.bind(triangle_buffer, t * TRIANGLE_FLOAT_STRIDE);
170
+ if (gjk(simplex_buf, query_posed, candidate_posed)) {
171
+ overlaps = true;
172
+ break;
173
+ }
174
+ }
175
+ }
176
+
177
+ if (overlaps) {
178
+ output[cursor] = body_id;
179
+ cursor++;
180
+ count++;
181
+ }
182
+ }
183
+
184
+ return count;
185
+ }
@@ -20,6 +20,20 @@ declare class Ticker {
20
20
  * @type {Clock}
21
21
  */
22
22
  readonly clock: Clock;
23
+ /**
24
+ * When `true`, ticking is suspended while the host document/tab is in the
25
+ * background (hidden, or stored in the back/forward cache) and resumes from
26
+ * the current moment once it returns to the foreground.
27
+ *
28
+ * This avoids dispatching a flood of catch-up ticks (or one huge delta) for
29
+ * the time spent suspended, which would otherwise destabilise the simulation.
30
+ *
31
+ * Has no effect in environments without document/visibility support (e.g.
32
+ * Node.js), where the relevant lifecycle events simply never fire.
33
+ *
34
+ * @type {boolean}
35
+ */
36
+ suspend_in_background: boolean;
23
37
  /**
24
38
  * Dispatches time delta in seconds since last tick
25
39
  * @readonly
@@ -1 +1 @@
1
- {"version":3,"file":"Ticker.d.ts","sourceRoot":"","sources":["../../../../src/engine/simulation/Ticker.js"],"names":[],"mappings":";AAIA;;;;;;;;;;;;;;GAcG;AACH;IACI;;;OAGG;IACH,gBAFU,KAAK,CAEK;IAiBpB;;;;OAIG;IACH,iBAFU,OAAO,MAAM,CAAC,CAEF;IAUtB;;;OAGG;IACH,uBAFW,MAAM,QAgDhB;IAED,cAOC;IAED,eAQC;IAED,aAOC;;CACJ;kBArIiB,aAAa;mBADZ,oCAAoC"}
1
+ {"version":3,"file":"Ticker.d.ts","sourceRoot":"","sources":["../../../../src/engine/simulation/Ticker.js"],"names":[],"mappings":";AAIA;;;;;;;;;;;;;;GAcG;AACH;IACI;;;OAGG;IACH,gBAFU,KAAK,CAEK;IAEpB;;;;;;;;;;;;OAYG;IACH,uBAFU,OAAO,CAEY;IAwB7B;;;;OAIG;IACH,iBAFU,OAAO,MAAM,CAAC,CAEF;IA+GtB;;;OAGG;IACH,uBAFW,MAAM,QAyDhB;IAED,cAOC;IAED,eAQC;IAED,aAUC;;CACJ;kBA5QiB,aAAa;mBADZ,oCAAoC"}