@woosh/meep-engine 2.146.0 → 2.148.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 (105) hide show
  1. package/package.json +1 -1
  2. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.d.ts +4 -4
  3. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.d.ts.map +1 -1
  4. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.js +48 -52
  5. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.d.ts +23 -21
  6. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.d.ts.map +1 -1
  7. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.js +41 -406
  8. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.d.ts +5 -4
  9. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.d.ts.map +1 -1
  10. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.js +400 -395
  11. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +0 -11
  12. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  13. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +8 -6
  14. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  15. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +552 -551
  16. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts +8 -3
  17. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts.map +1 -1
  18. package/src/engine/control/first-person/abilities/LedgeGrab.js +213 -199
  19. package/src/engine/control/first-person/abilities/Mantle.d.ts.map +1 -1
  20. package/src/engine/control/first-person/abilities/Mantle.js +195 -188
  21. package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
  22. package/src/engine/control/first-person/abilities/WallRun.js +183 -175
  23. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts +9 -0
  24. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts.map +1 -1
  25. package/src/engine/control/first-person/sensors/FirstPersonSensors.js +87 -77
  26. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts +8 -0
  27. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts.map +1 -1
  28. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js +229 -196
  29. package/src/engine/ecs/EntityManager.d.ts +34 -11
  30. package/src/engine/ecs/EntityManager.d.ts.map +1 -1
  31. package/src/engine/ecs/EntityManager.js +71 -42
  32. package/src/engine/interpolation/BinaryInterpolationAdapter.d.ts.map +1 -0
  33. package/src/engine/interpolation/Interpoland.d.ts +48 -0
  34. package/src/engine/interpolation/Interpoland.d.ts.map +1 -0
  35. package/src/engine/interpolation/Interpoland.js +49 -0
  36. package/src/engine/interpolation/Interpolated.d.ts +101 -0
  37. package/src/engine/interpolation/Interpolated.d.ts.map +1 -0
  38. package/src/engine/interpolation/Interpolated.js +149 -0
  39. package/src/engine/{network/sim → interpolation}/InterpolationLog.d.ts +1 -1
  40. package/src/engine/interpolation/InterpolationLog.d.ts.map +1 -0
  41. package/src/engine/{network/sim → interpolation}/InterpolationLog.js +2 -2
  42. package/src/engine/interpolation/InterpolationSystem.d.ts +116 -0
  43. package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -0
  44. package/src/engine/interpolation/InterpolationSystem.js +233 -0
  45. package/src/engine/interpolation/PoseInterpolationAdapter.d.ts +17 -0
  46. package/src/engine/interpolation/PoseInterpolationAdapter.d.ts.map +1 -0
  47. package/src/engine/interpolation/PoseInterpolationAdapter.js +61 -0
  48. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +35 -0
  49. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -0
  50. package/src/engine/interpolation/TransformPoseSerializationAdapter.js +57 -0
  51. package/src/engine/interpolation/pose_interpoland.d.ts +18 -0
  52. package/src/engine/interpolation/pose_interpoland.d.ts.map +1 -0
  53. package/src/engine/interpolation/pose_interpoland.js +27 -0
  54. package/src/engine/network/NetworkSession.d.ts +2 -2
  55. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  56. package/src/engine/network/NetworkSession.js +2 -2
  57. package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts +1 -1
  58. package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts.map +1 -1
  59. package/src/engine/network/adapters/QuaternionInterpolationAdapter.js +1 -1
  60. package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts +1 -1
  61. package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts.map +1 -1
  62. package/src/engine/network/adapters/TransformInterpolationAdapter.js +1 -1
  63. package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts +1 -1
  64. package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts.map +1 -1
  65. package/src/engine/network/adapters/Vector3InterpolationAdapter.js +1 -1
  66. package/src/engine/physics/INTEPOLATION_SYSTEM_PLAN.md +287 -0
  67. package/src/engine/physics/PLAN.md +10 -9
  68. package/src/engine/physics/body/SolverBodyState.d.ts +142 -0
  69. package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -0
  70. package/src/engine/physics/body/SolverBodyState.js +251 -0
  71. package/src/engine/physics/broadphase/generate_pairs.d.ts +2 -1
  72. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  73. package/src/engine/physics/broadphase/generate_pairs.js +5 -3
  74. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  75. package/src/engine/physics/constraint/solve_constraints.js +691 -673
  76. package/src/engine/physics/ecs/PhysicsSystem.d.ts +82 -15
  77. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  78. package/src/engine/physics/ecs/PhysicsSystem.js +387 -87
  79. package/src/engine/physics/inertia/world_inverse_inertia.d.ts +23 -0
  80. package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
  81. package/src/engine/physics/inertia/world_inverse_inertia.js +116 -77
  82. package/src/engine/physics/integration/integrate_position.d.ts +11 -1
  83. package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
  84. package/src/engine/physics/integration/integrate_position.js +97 -79
  85. package/src/engine/physics/integration/integrate_velocity.d.ts +12 -3
  86. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
  87. package/src/engine/physics/integration/integrate_velocity.js +201 -160
  88. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  89. package/src/engine/physics/narrowphase/box_box_manifold.js +750 -665
  90. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
  91. package/src/engine/physics/narrowphase/box_triangle_contact.js +19 -46
  92. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts +16 -0
  93. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts.map +1 -0
  94. package/src/engine/physics/narrowphase/clip_against_axis_uv.js +49 -0
  95. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  96. package/src/engine/physics/narrowphase/narrowphase_step.js +52 -4
  97. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  98. package/src/engine/physics/queries/raycast.js +7 -4
  99. package/src/engine/physics/solver/solve_contacts.d.ts +2 -2
  100. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  101. package/src/engine/physics/solver/solve_contacts.js +1341 -1173
  102. package/src/engine/network/sim/BinaryInterpolationAdapter.d.ts.map +0 -1
  103. package/src/engine/network/sim/InterpolationLog.d.ts.map +0 -1
  104. /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.d.ts +0 -0
  105. /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.js +0 -0
@@ -1,19 +1,26 @@
1
1
  import { assert } from "../../../core/assert.js";
2
+ import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
2
3
  import { BVH } from "../../../core/bvh2/bvh3/BVH.js";
3
4
  import Signal from "../../../core/events/signal/Signal.js";
5
+ import { returnTrue } from "../../../core/function/returnTrue.js";
6
+ import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
4
7
  import Vector3 from "../../../core/geom/Vector3.js";
5
8
  import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
6
9
  import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
7
10
  import { System } from "../../ecs/System.js";
8
11
  import { Transform } from "../../ecs/transform/Transform.js";
12
+ import { Interpolated } from "../../interpolation/Interpolated.js";
9
13
  import { body_id_index, BodyStorage } from "../body/BodyStorage.js";
10
- import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
14
+ import { SBS_STRIDE, SolverBodyState } from "../body/SolverBodyState.js";
11
15
  import { compute_fat_world_aabb } from "../broadphase/compute_fat_world_aabb.js";
12
16
  import { generate_pairs } from "../broadphase/generate_pairs.js";
13
17
  import { PairList } from "../broadphase/PairList.js";
14
- import { CONTACT_STRIDE, ManifoldStore } from "../contact/ManifoldStore.js";
18
+ import { ccd_resolve } from "../ccd/linear_sweep.js";
19
+ import { solve_joints } from "../constraint/solve_constraints.js";
20
+ import { ManifoldStore } from "../contact/ManifoldStore.js";
15
21
  import { ContactEventBuffer, ContactEventKind } from "../events/ContactEventBuffer.js";
16
22
  import { diff_manifolds } from "../events/diff_manifolds.js";
23
+ import { world_inverse_inertia_apply } from "../inertia/world_inverse_inertia.js";
17
24
  import { integrate_position } from "../integration/integrate_position.js";
18
25
  import { integrate_velocity_forces, integrate_velocity_gravity } from "../integration/integrate_velocity.js";
19
26
  import { IslandBuilder } from "../island/IslandBuilder.js";
@@ -21,32 +28,28 @@ import { narrowphase_step } from "../narrowphase/narrowphase_step.js";
21
28
  import { overlap_shape as overlap_shape_query } from "../queries/overlap_shape.js";
22
29
  import { raycast as raycast_query } from "../queries/raycast.js";
23
30
  import { shape_cast as shape_cast_query } from "../queries/shape_cast.js";
24
- import { ccd_resolve } from "../ccd/linear_sweep.js";
25
- import { returnTrue } from "../../../core/function/returnTrue.js";
26
31
  import {
32
+ apply_restitution,
27
33
  prepare_contacts,
28
- refresh_contacts,
29
34
  redetect_concave_contacts,
30
- warm_start_contacts,
31
- solve_velocity,
32
- apply_restitution,
35
+ refresh_contacts,
33
36
  solve_position,
37
+ solve_velocity,
38
+ warm_start_contacts,
34
39
  } from "../solver/solve_contacts.js";
35
- import { solve_joints } from "../constraint/solve_constraints.js";
36
- import { JOINT_WORLD, JOINT_UNALLOCATED } from "./Joint.js";
37
- import { world_inverse_inertia_apply } from "../inertia/world_inverse_inertia.js";
40
+ import { BodyKind } from "./BodyKind.js";
41
+ import { Collider, COLLIDER_UNBOUND } from "./Collider.js";
42
+ import { JOINT_UNALLOCATED, JOINT_WORLD } from "./Joint.js";
38
43
  import { PhysicsEvents } from "./PhysicsEvents.js";
44
+ import { RIGID_BODY_UNALLOCATED, RigidBody } from "./RigidBody.js";
45
+ import { RigidBodyFlags } from "./RigidBodyFlags.js";
46
+ import { SleepState } from "./SleepState.js";
39
47
 
40
48
  /**
41
49
  * Scratch for {@link applyImpulseAt}'s angular delta calculation.
42
50
  * @type {Float64Array}
43
51
  */
44
52
  const scratch_angular_delta = new Float64Array(3);
45
- import { BodyKind } from "./BodyKind.js";
46
- import { Collider, COLLIDER_UNBOUND } from "./Collider.js";
47
- import { RIGID_BODY_UNALLOCATED, RigidBody } from "./RigidBody.js";
48
- import { RigidBodyFlags } from "./RigidBodyFlags.js";
49
- import { SleepState } from "./SleepState.js";
50
53
 
51
54
  /**
52
55
  * Reusable scratch buffer for world-AABB construction so the link path is
@@ -94,6 +97,7 @@ export class PhysicsSystem extends System {
94
97
 
95
98
  /**
96
99
  * @type {BodyStorage}
100
+ * @readonly
97
101
  */
98
102
  this.storage = new BodyStorage();
99
103
 
@@ -110,6 +114,7 @@ export class PhysicsSystem extends System {
110
114
  /**
111
115
  * Persistent contact-manifold cache. One slot per active pair.
112
116
  * @type {ManifoldStore}
117
+ * @readonly
113
118
  */
114
119
  this.manifolds = new ManifoldStore();
115
120
 
@@ -137,6 +142,18 @@ export class PhysicsSystem extends System {
137
142
  */
138
143
  this.islands = new IslandBuilder();
139
144
 
145
+ /**
146
+ * Data-oriented mirror of the per-body solver hot state (velocity,
147
+ * inverse mass / inertia, orientation), packed into one ArrayBuffer
148
+ * and indexed by body slot. Gathered from the `RigidBody` /
149
+ * `Transform` components once per step (after islands are built),
150
+ * mutated in place by the TGS substep loop, and the persistent
151
+ * velocity scattered back at the end. Keeps the solver's hottest inner
152
+ * loop free of component-object dereferences.
153
+ * @type {SolverBodyState}
154
+ */
155
+ this.__solver_state = new SolverBodyState();
156
+
140
157
  /**
141
158
  * Velocity-squared threshold below which a body is eligible to start
142
159
  * accumulating sleep time. Combined linear + angular kinetic-ish
@@ -238,7 +255,11 @@ export class PhysicsSystem extends System {
238
255
  * @type {RigidBody[]}
239
256
  */
240
257
  this.__bodies = [];
241
- /** @type {Transform[]} */
258
+
259
+ /**
260
+ * @type {Transform[]}
261
+ * @readonly
262
+ */
242
263
  this.__transforms = [];
243
264
 
244
265
  /**
@@ -312,13 +333,26 @@ export class PhysicsSystem extends System {
312
333
  */
313
334
  this.__ccd_start_pos = new Float64Array(0);
314
335
 
336
+
315
337
  /**
316
- * Bound reference to {@link __pair_filter} so we hand the same
317
- * callable to {@link generate_pairs} each step without per-step
318
- * allocation.
338
+ * Optional shared interpolation log to produce per-step pose snapshots
339
+ * into (the {@link InterpolationSystem}'s log). When set — and only then —
340
+ * each fixedUpdate restores every awake {@link Interpolated} body's live
341
+ * components from the previous tick (undoing render-time interpolation so
342
+ * the sim reads authoritative state), then records the post-step state
343
+ * under the current `entityManager.fixedStepTick`. Null on a headless /
344
+ * non-rendering world, where the producer work is skipped entirely.
345
+ * @type {InterpolationLog|null}
346
+ */
347
+ this.interpolationLog = null;
348
+
349
+ /**
350
+ * Reusable decode buffer for restoring interpolated snapshots.
319
351
  * @private
352
+ * @type {BinaryBuffer}
320
353
  */
321
- this.__pair_filter_bound = (idA, idB) => this.__pair_filter(idA, idB);
354
+ this.__interp_scratch = new BinaryBuffer();
355
+ this.__interp_scratch.fromArrayBuffer(new ArrayBuffer(256));
322
356
  }
323
357
 
324
358
  /**
@@ -338,13 +372,21 @@ export class PhysicsSystem extends System {
338
372
  __pair_filter(idA, idB) {
339
373
  const idxA = body_id_index(idA);
340
374
  const idxB = body_id_index(idB);
375
+
341
376
  const rbA = this.__bodies[idxA];
342
377
  const rbB = this.__bodies[idxB];
343
- if (rbA === undefined || rbB === undefined) return false;
378
+
379
+ if (rbA === undefined || rbB === undefined) {
380
+ return false;
381
+ }
344
382
 
345
383
  // Layer/mask gate (symmetric).
346
- if (((rbA.layer & rbB.mask) | 0) === 0) return false;
347
- if (((rbB.layer & rbA.mask) | 0) === 0) return false;
384
+ if (((rbA.layer & rbB.mask) | 0) === 0) {
385
+ return false;
386
+ }
387
+ if (((rbB.layer & rbA.mask) | 0) === 0) {
388
+ return false;
389
+ }
348
390
 
349
391
  // User callback gate, if installed.
350
392
  const fn = this.__contact_filter;
@@ -812,7 +854,7 @@ export class PhysicsSystem extends System {
812
854
  if (rigidBody.kind !== BodyKind.Dynamic) {
813
855
  return;
814
856
  }
815
- rigidBody.accumulatedForce.add( force);
857
+ rigidBody.accumulatedForce.add(force);
816
858
  this.__wake_body(rigidBody);
817
859
  }
818
860
 
@@ -829,6 +871,50 @@ export class PhysicsSystem extends System {
829
871
  }
830
872
  }
831
873
 
874
+ /**
875
+ * Teleport a body to a new pose, bypassing integration: writes the body's
876
+ * Transform directly and wakes it. For an interpolated body this also flags a
877
+ * render `snap` on its {@link Interpolated} component, so the producer keeps
878
+ * this pose (rather than restoring the previous tick over it) and the
879
+ * renderer shows the new pose without sliding across the jump.
880
+ *
881
+ * This is the authoritative way to reposition an interpolated body — a raw
882
+ * `Transform` write would be undone by the per-step restore. Velocity is left
883
+ * as-is; zero it via {@link setLinearVelocity} if the teleport should also
884
+ * stop the body.
885
+ *
886
+ * @param {RigidBody} rigidBody
887
+ * @param {Vector3|{x:number,y:number,z:number}} position world position
888
+ * @param {Quaternion|{x:number,y:number,z:number,w:number}} rotation world unit-quaternion rotation
889
+ */
890
+ setPose(rigidBody, position, rotation) {
891
+ const idx = body_id_index(rigidBody._bodyId);
892
+ const transform = this.__transforms[idx];
893
+ if (transform === undefined) {
894
+ return;
895
+ }
896
+
897
+ transform.position.set(position.x, position.y, position.z);
898
+ transform.rotation.set(rotation.x, rotation.y, rotation.z, rotation.w);
899
+
900
+ this.__wake_body(rigidBody);
901
+
902
+ // Flag a snap on the body's Interpolated component, if any, so the
903
+ // interpolation producer (restore) and consumer (blend) treat this pose
904
+ // as authoritative this frame instead of interpolating across the jump.
905
+ const em = this.entityManager;
906
+ if (em !== null && em !== undefined) {
907
+ const dataset = em.dataset;
908
+ if (dataset !== null && dataset !== undefined) {
909
+ const entity = this.storage.entity_at(idx);
910
+ const interpolated = dataset.getComponent(entity, Interpolated);
911
+ if (interpolated !== undefined && interpolated !== null) {
912
+ interpolated.snap = true;
913
+ }
914
+ }
915
+ }
916
+ }
917
+
832
918
  /**
833
919
  * Force the body awake. Static bodies are ignored.
834
920
  * @param {RigidBody} rigidBody
@@ -931,8 +1017,12 @@ export class PhysicsSystem extends System {
931
1017
  rb.sleep_group_next = -1;
932
1018
  rb.sleep_group_prev = -1;
933
1019
  rb.sleepState = SleepState.Sleeping;
934
- rb.linearVelocity[0] = 0; rb.linearVelocity[1] = 0; rb.linearVelocity[2] = 0;
935
- rb.angularVelocity[0] = 0; rb.angularVelocity[1] = 0; rb.angularVelocity[2] = 0;
1020
+ rb.linearVelocity[0] = 0;
1021
+ rb.linearVelocity[1] = 0;
1022
+ rb.linearVelocity[2] = 0;
1023
+ rb.angularVelocity[0] = 0;
1024
+ rb.angularVelocity[1] = 0;
1025
+ rb.angularVelocity[2] = 0;
936
1026
  storage.mark_sleeping(idx);
937
1027
  return;
938
1028
  }
@@ -946,22 +1036,16 @@ export class PhysicsSystem extends System {
946
1036
  rb.sleep_group_next = next_idx;
947
1037
  rb.sleep_group_prev = prev_idx;
948
1038
  rb.sleepState = SleepState.Sleeping;
949
- rb.linearVelocity[0] = 0; rb.linearVelocity[1] = 0; rb.linearVelocity[2] = 0;
950
- rb.angularVelocity[0] = 0; rb.angularVelocity[1] = 0; rb.angularVelocity[2] = 0;
1039
+ rb.linearVelocity[0] = 0;
1040
+ rb.linearVelocity[1] = 0;
1041
+ rb.linearVelocity[2] = 0;
1042
+ rb.angularVelocity[0] = 0;
1043
+ rb.angularVelocity[1] = 0;
1044
+ rb.angularVelocity[2] = 0;
951
1045
  storage.mark_sleeping(idx);
952
1046
  }
953
1047
  }
954
1048
 
955
- /**
956
- * Get the body index for a packed body id without revalidation. Used by
957
- * query traversals that already trust the id came from a live BVH leaf.
958
- * @param {number} packed_body_id
959
- * @returns {number}
960
- */
961
- __index_of(packed_body_id) {
962
- return body_id_index(packed_body_id);
963
- }
964
-
965
1049
  /**
966
1050
  * Broadphase raycast against both BVHs. Fills `result` with the nearest
967
1051
  * hit and returns `true` on hit, `false` on miss.
@@ -1168,7 +1252,7 @@ export class PhysicsSystem extends System {
1168
1252
  const lv = rb.linearVelocity;
1169
1253
  const av = rb.angularVelocity;
1170
1254
  const v_sqr = lv[0] * lv[0] + lv[1] * lv[1] + lv[2] * lv[2]
1171
- + av[0] * av[0] + av[1] * av[1] + av[2] * av[2];
1255
+ + av[0] * av[0] + av[1] * av[1] + av[2] * av[2];
1172
1256
  if (v_sqr > max_v_sqr) max_v_sqr = v_sqr;
1173
1257
  }
1174
1258
 
@@ -1245,14 +1329,22 @@ export class PhysicsSystem extends System {
1245
1329
  const pt = payload.point;
1246
1330
  const nm = payload.normal;
1247
1331
  if (has_contact) {
1248
- const wax = data[slot_off], way = data[slot_off + 1], waz = data[slot_off + 2];
1332
+ const wax = data[slot_off], way = data[slot_off + 1], waz = data[slot_off + 2];
1249
1333
  const wbx = data[slot_off + 3], wby = data[slot_off + 4], wbz = data[slot_off + 5];
1250
- pt[0] = (wax + wbx) * 0.5; pt[1] = (way + wby) * 0.5; pt[2] = (waz + wbz) * 0.5;
1251
- nm[0] = data[slot_off + 6]; nm[1] = data[slot_off + 7]; nm[2] = data[slot_off + 8];
1334
+ pt[0] = (wax + wbx) * 0.5;
1335
+ pt[1] = (way + wby) * 0.5;
1336
+ pt[2] = (waz + wbz) * 0.5;
1337
+ nm[0] = data[slot_off + 6];
1338
+ nm[1] = data[slot_off + 7];
1339
+ nm[2] = data[slot_off + 8];
1252
1340
  payload.depth = data[slot_off + 9];
1253
1341
  } else {
1254
- pt[0] = 0; pt[1] = 0; pt[2] = 0;
1255
- nm[0] = 0; nm[1] = 0; nm[2] = 0;
1342
+ pt[0] = 0;
1343
+ pt[1] = 0;
1344
+ pt[2] = 0;
1345
+ nm[0] = 0;
1346
+ nm[1] = 0;
1347
+ nm[2] = 0;
1256
1348
  payload.depth = 0;
1257
1349
  }
1258
1350
  payload.entityA = entA;
@@ -1260,9 +1352,16 @@ export class PhysicsSystem extends System {
1260
1352
 
1261
1353
  let event_name;
1262
1354
  let signal;
1263
- if (kind === ContactEventKind.Begin) { event_name = PhysicsEvents.ContactBegin; signal = this.onContactBegin; }
1264
- else if (kind === ContactEventKind.Stay) { event_name = PhysicsEvents.ContactStay; signal = this.onContactStay; }
1265
- else { event_name = PhysicsEvents.ContactEnd; signal = this.onContactEnd; }
1355
+ if (kind === ContactEventKind.Begin) {
1356
+ event_name = PhysicsEvents.ContactBegin;
1357
+ signal = this.onContactBegin;
1358
+ } else if (kind === ContactEventKind.Stay) {
1359
+ event_name = PhysicsEvents.ContactStay;
1360
+ signal = this.onContactStay;
1361
+ } else {
1362
+ event_name = PhysicsEvents.ContactEnd;
1363
+ signal = this.onContactEnd;
1364
+ }
1266
1365
 
1267
1366
  signal.send1(payload);
1268
1367
 
@@ -1273,12 +1372,115 @@ export class PhysicsSystem extends System {
1273
1372
  }
1274
1373
  }
1275
1374
 
1375
+ /**
1376
+ * Producer — restore pass. At the top of a fixed step, reset every awake
1377
+ * {@link Interpolated} body's live components to their authoritative state
1378
+ * from the previous tick's snapshot, undoing any render-time interpolation
1379
+ * the {@link InterpolationSystem} wrote between frames so the sim integrates
1380
+ * from truth, not an interpolated pose. A body with no previous snapshot
1381
+ * (first step ever, or just woken) is left as-is — its live state is already
1382
+ * authoritative. No-op unless {@link interpolationLog} is wired.
1383
+ * @private
1384
+ */
1385
+ __interp_restore() {
1386
+ const log = this.interpolationLog;
1387
+ const em = this.entityManager;
1388
+ if (log === null || em === null || em === undefined) return;
1389
+ const dataset = em.dataset;
1390
+ if (dataset === null || dataset === undefined) return;
1391
+
1392
+ const prev_tick = em.fixedStepTick - 1;
1393
+ if (prev_tick < 0) return;
1394
+
1395
+ const storage = this.storage;
1396
+ const count = storage.awake_count;
1397
+ const scratch = this.__interp_scratch;
1398
+
1399
+ for (let i = 0; i < count; i++) {
1400
+ const idx = storage.awake_at(i);
1401
+ const entity = storage.entity_at(idx);
1402
+ const interpolated = dataset.getComponent(entity, Interpolated);
1403
+ if (interpolated === undefined || interpolated === null) continue;
1404
+ // A teleported body (snap set, e.g. via setPose) keeps its live pose
1405
+ // this step — restoring the previous tick would undo the teleport.
1406
+ if (interpolated.snap) continue;
1407
+ const key = interpolated.key;
1408
+ if (key < 0) continue;
1409
+
1410
+ const interpolands = interpolated.interpolands;
1411
+ for (let k = 0; k < interpolands.length; k++) {
1412
+ const ip = interpolands[k];
1413
+ scratch.position = 0;
1414
+ // Snap to the previous tick's snapshot (both offsets equal → t irrelevant).
1415
+ const ok = log.interpolate(scratch, key, ip.type_id, prev_tick, prev_tick, 0, ip.interpolation_adapter);
1416
+ if (!ok) continue;
1417
+ const target = dataset.getComponent(entity, ip.component_class);
1418
+ if (target === undefined || target === null) continue;
1419
+ scratch.position = 0;
1420
+ ip.serialization_adapter.deserialize(scratch, target);
1421
+ }
1422
+ }
1423
+ }
1424
+
1425
+ /**
1426
+ * Producer — record pass. At the end of a fixed step, snapshot every awake
1427
+ * {@link Interpolated} body's live components into the shared log under the
1428
+ * current `entityManager.fixedStepTick`. The render-time
1429
+ * {@link InterpolationSystem} blends consecutive ticks from these snapshots.
1430
+ * Only awake (moving) bodies are recorded, so the log stays sparse. No-op
1431
+ * unless {@link interpolationLog} is wired.
1432
+ * @private
1433
+ */
1434
+ __interp_record() {
1435
+ const log = this.interpolationLog;
1436
+ const em = this.entityManager;
1437
+ if (log === null || em === null || em === undefined) return;
1438
+ const dataset = em.dataset;
1439
+ if (dataset === null || dataset === undefined) return;
1440
+
1441
+ const tick = em.fixedStepTick;
1442
+ const storage = this.storage;
1443
+ const count = storage.awake_count;
1444
+
1445
+ log.begin_tick(tick);
1446
+ for (let i = 0; i < count; i++) {
1447
+ const idx = storage.awake_at(i);
1448
+ const entity = storage.entity_at(idx);
1449
+ const interpolated = dataset.getComponent(entity, Interpolated);
1450
+ if (interpolated === undefined || interpolated === null) continue;
1451
+ const key = interpolated.key;
1452
+ if (key < 0) continue;
1453
+
1454
+ const interpolands = interpolated.interpolands;
1455
+ for (let k = 0; k < interpolands.length; k++) {
1456
+ const ip = interpolands[k];
1457
+ const target = dataset.getComponent(entity, ip.component_class);
1458
+ if (target === undefined || target === null) continue;
1459
+ const buf = log.begin_record(key, ip.type_id);
1460
+ ip.serialization_adapter.serialize(buf, target);
1461
+ log.end_record();
1462
+ }
1463
+ }
1464
+ log.end_tick();
1465
+ }
1466
+
1276
1467
  fixedUpdate(dt) {
1468
+ // Producer: restore authoritative pose (undo render interpolation)
1469
+ // before the sim reads any Transform this step.
1470
+ this.__interp_restore();
1471
+
1277
1472
  const gx = this.gravity.x;
1278
1473
  const gy = this.gravity.y;
1279
1474
  const gz = this.gravity.z;
1280
1475
 
1281
- const count = this.storage.awake_count;
1476
+ const storage = this.storage;
1477
+ const bodies = this.__bodies;
1478
+ const transforms = this.__transforms;
1479
+ const joints = this.__joints;
1480
+ const manifolds = this.manifolds;
1481
+
1482
+
1483
+ const count = storage.awake_count;
1282
1484
 
1283
1485
  // Stage 1: consume the per-frame force / torque accumulators into
1284
1486
  // velocity at the full `dt`, exactly once (a user force is a per-frame
@@ -1288,9 +1490,11 @@ export class PhysicsSystem extends System {
1288
1490
  // that substep's contact warm-start, keeping resting stacks at zero
1289
1491
  // velocity.
1290
1492
  for (let i = 0; i < count; i++) {
1291
- const idx = this.storage.awake_at(i);
1292
- const rb = this.__bodies[idx];
1293
- const tr = this.__transforms[idx];
1493
+ const idx = storage.awake_at(i);
1494
+
1495
+ const rb = bodies[idx];
1496
+ const tr = transforms[idx];
1497
+
1294
1498
  integrate_velocity_forces(rb, tr, dt);
1295
1499
  }
1296
1500
 
@@ -1300,19 +1504,30 @@ export class PhysicsSystem extends System {
1300
1504
  // is a sub-millimetre slack difference, safely inside the margin.
1301
1505
  const lists = this.__body_collider_lists;
1302
1506
  for (let i = 0; i < count; i++) {
1303
- const idx = this.storage.awake_at(i);
1304
- const rb = this.__bodies[idx];
1507
+
1508
+ const idx = storage.awake_at(i);
1509
+ const rb = bodies[idx];
1305
1510
  const list = lists[idx];
1306
- if (list === undefined) continue;
1511
+
1512
+ if (list === undefined) {
1513
+ continue;
1514
+ }
1515
+
1307
1516
  const lv = rb.linearVelocity;
1308
- for (let k = 0; k < list.length; k++) {
1517
+
1518
+ const list_length = list.length;
1519
+
1520
+ for (let k = 0; k < list_length; k++) {
1521
+
1309
1522
  const entry = list[k];
1523
+
1310
1524
  compute_fat_world_aabb(
1311
1525
  scratch_world_aabb, 0,
1312
1526
  entry.collider.shape, entry.transform,
1313
1527
  lv[0], lv[1], lv[2],
1314
1528
  dt
1315
1529
  );
1530
+
1316
1531
  this.dynamicBvh.node_move_aabb(entry.bvhNode, scratch_world_aabb);
1317
1532
  }
1318
1533
  }
@@ -1321,13 +1536,14 @@ export class PhysicsSystem extends System {
1321
1536
  // outer-step motion, so the pair set stays valid across all substeps
1322
1537
  // — broadphase runs once.
1323
1538
  generate_pairs(
1324
- this.storage,
1539
+ storage,
1325
1540
  this.dynamicBvh,
1326
1541
  this.staticBvh,
1327
- this.manifolds,
1542
+ manifolds,
1328
1543
  lists,
1329
1544
  this.pairs,
1330
- this.__pair_filter_bound,
1545
+ this.__pair_filter,
1546
+ this,
1331
1547
  );
1332
1548
 
1333
1549
  // Stage 4: wake propagation — through broadphase pairs, then through
@@ -1338,11 +1554,11 @@ export class PhysicsSystem extends System {
1338
1554
  // Stage 5: narrowphase — once per outer step. The substep loop below
1339
1555
  // re-derives each contact's penetration analytically from the moved
1340
1556
  // poses rather than re-running geometry.
1341
- narrowphase_step(this.pairs, this.manifolds, this.__body_collider_lists);
1557
+ narrowphase_step(this.pairs, manifolds, this.__body_collider_lists);
1342
1558
 
1343
1559
  // Stage 6: partition awake bodies + touched contacts into islands.
1344
1560
  // Consumed by the solver (flattened contact list) and the sleep test.
1345
- this.islands.build(this.storage, this.manifolds, this.__bodies, this.__body_collider_lists, this.__joints);
1561
+ this.islands.build(storage, manifolds, bodies, this.__body_collider_lists, joints);
1346
1562
 
1347
1563
  // Stage 7: TGS substep loop.
1348
1564
  //
@@ -1359,7 +1575,7 @@ export class PhysicsSystem extends System {
1359
1575
  // the loop.
1360
1576
  const N = this.substeps;
1361
1577
  const h = dt / N;
1362
- const count_after_wake = this.storage.awake_count;
1578
+ const count_after_wake = storage.awake_count;
1363
1579
 
1364
1580
  // CCD: capture start-of-step positions for flagged bodies over the
1365
1581
  // post-wake awake set (poses are unchanged until the substep loop below
@@ -1368,21 +1584,37 @@ export class PhysicsSystem extends System {
1368
1584
  // start matches the end the resolve pass reads. Zero-cost when no body
1369
1585
  // is flagged.
1370
1586
  const ccd_on = this.ccdEnabled;
1587
+
1371
1588
  if (ccd_on) {
1372
- const ccd_need = this.storage.high_water_mark * 3;
1589
+
1590
+ const ccd_need = storage.high_water_mark * 3;
1591
+
1373
1592
  if (this.__ccd_start_pos.length < ccd_need) {
1374
1593
  this.__ccd_start_pos = new Float64Array(ccd_need);
1375
1594
  }
1595
+
1376
1596
  const ccd_start = this.__ccd_start_pos;
1597
+
1377
1598
  for (let i = 0; i < count_after_wake; i++) {
1378
- const idx = this.storage.awake_at(i);
1379
- const rb = this.__bodies[idx];
1380
- if (rb.kind !== BodyKind.Dynamic) continue;
1381
- if ((rb.flags & RigidBodyFlags.CCD) === 0) continue;
1599
+ const idx = storage.awake_at(i);
1600
+ const rb = bodies[idx];
1601
+
1602
+ if (rb.kind !== BodyKind.Dynamic) {
1603
+ continue;
1604
+ }
1605
+ if ((rb.flags & RigidBodyFlags.CCD) === 0) {
1606
+ continue;
1607
+ }
1608
+
1382
1609
  const list = this.__body_collider_lists[idx];
1383
- if (list === undefined || list.length === 0) continue;
1610
+
1611
+ if (list === undefined || list.length === 0) {
1612
+ continue;
1613
+ }
1614
+
1384
1615
  const cp = list[0].transform.position;
1385
1616
  const cb = idx * 3;
1617
+
1386
1618
  ccd_start[cb] = cp[0];
1387
1619
  ccd_start[cb + 1] = cp[1];
1388
1620
  ccd_start[cb + 2] = cp[2];
@@ -1395,55 +1627,117 @@ export class PhysicsSystem extends System {
1395
1627
  // reallocate again this step.
1396
1628
  this.__reset_pseudo_velocity();
1397
1629
  const pseudoVel = this.__pseudo_velocity;
1398
- const pseudo_len = this.storage.high_water_mark * 6;
1630
+ const pseudo_len = storage.high_water_mark * 6;
1631
+
1632
+ // Gather the data-oriented solver state for every body the substep loop
1633
+ // will touch: the post-wake awake set (all dynamics, at their post-force
1634
+ // velocity) plus the static / kinematic anchors and jointed partners
1635
+ // referenced by this step's contacts and joints. From here the substep
1636
+ // loop reads / writes velocity + orientation through `ss_data` with no
1637
+ // component-object dereference; persistent velocity is scattered back
1638
+ // onto the RigidBodies after the solve.
1639
+ const solver_state = this.__solver_state;
1640
+ solver_state.begin(storage.high_water_mark);
1641
+
1642
+ for (let i = 0; i < count_after_wake; i++) {
1643
+ const idx = storage.awake_at(i);
1644
+ solver_state.gather(idx, bodies[idx], transforms[idx]);
1645
+ }
1646
+
1647
+ const island_contacts = this.islands.contact_data;
1648
+ const island_contact_total = this.islands.contact_offsets[this.islands.island_count];
1649
+
1650
+ for (let i = 0; i < island_contact_total; i++) {
1651
+ const slot = island_contacts[i];
1652
+
1653
+ const ia = body_id_index(manifolds.bodyA(slot));
1654
+ const ib = body_id_index(manifolds.bodyB(slot));
1655
+
1656
+ solver_state.gather(ia, bodies[ia], transforms[ia]);
1657
+ solver_state.gather(ib, bodies[ib], transforms[ib]);
1658
+ }
1659
+
1660
+ const joint_count = joints.length;
1661
+ for (let i = 0; i < joint_count; i++) {
1662
+ const joint = joints[i];
1399
1663
 
1400
- prepare_contacts(this.manifolds, this, h);
1664
+ if (joint === undefined || joint === null) {
1665
+ continue;
1666
+ }
1667
+
1668
+ if (!storage.is_valid(joint._bodyIdA)) {
1669
+ continue;
1670
+ }
1671
+
1672
+ const ia = body_id_index(joint._bodyIdA);
1673
+
1674
+ solver_state.gather(ia, bodies[ia], transforms[ia]);
1675
+
1676
+ if (joint._bodyIdB !== JOINT_WORLD && storage.is_valid(joint._bodyIdB)) {
1677
+
1678
+ const ib = body_id_index(joint._bodyIdB);
1679
+
1680
+ solver_state.gather(ib, bodies[ib], transforms[ib]);
1681
+
1682
+ }
1683
+ }
1684
+
1685
+ const ss_data = solver_state.data;
1686
+
1687
+ prepare_contacts(manifolds, this, h);
1401
1688
 
1402
1689
  for (let s = 0; s < N; s++) {
1690
+
1403
1691
  // Gravity (+ damping) for this substep.
1404
1692
  for (let i = 0; i < count_after_wake; i++) {
1405
- const idx = this.storage.awake_at(i);
1406
- integrate_velocity_gravity(this.__bodies[idx], this.__transforms[idx], gx, gy, gz, h);
1693
+ const idx = storage.awake_at(i);
1694
+ integrate_velocity_gravity(ss_data, idx * SBS_STRIDE, bodies[idx], gx, gy, gz, h);
1407
1695
  }
1408
1696
 
1409
1697
  // Re-derive contact geometry at the current poses: concave pairs
1410
1698
  // re-run narrowphase (fresh feature/normal as the body rocks),
1411
1699
  // convex pairs rotate frozen anchors analytically. Then replay
1412
1700
  // the per-substep warm-start and solve velocity.
1413
- redetect_concave_contacts(this.manifolds, this);
1414
- refresh_contacts(this.manifolds, this);
1415
- warm_start_contacts(this.manifolds, this);
1416
- solve_velocity(this.manifolds, this, this.velocityIterations);
1701
+ redetect_concave_contacts(manifolds, this);
1702
+ refresh_contacts(manifolds, this.__transforms);
1703
+ warm_start_contacts(manifolds, this);
1704
+ solve_velocity(manifolds, this, this.velocityIterations);
1417
1705
 
1418
1706
  // Joints share the substep: warm-start + velocity-solve the 6-DOF
1419
1707
  // constraints on real velocity, coupled with the contacts above
1420
1708
  // (a body touched by both sees one substep of Gauss-Seidel across
1421
1709
  // contacts then joints). Position correction for locked DOFs is a
1422
1710
  // SPOOK bias inside this solve, so no separate joint position pass.
1423
- if (this.__joints.length > 0) {
1424
- solve_joints(this.__joints, this, h, this.jointIterations);
1711
+ if (joints.length > 0) {
1712
+ solve_joints(joints, this, h, this.jointIterations);
1425
1713
  }
1426
1714
 
1427
1715
  // Position correction writes pseudo-velocity (zeroed first so it
1428
1716
  // is a fresh per-substep correction), folded into the pose by the
1429
1717
  // position integrate and then discarded.
1430
1718
  pseudoVel.fill(0, 0, pseudo_len);
1431
- solve_position(this.manifolds, this, this.positionIterations);
1719
+ solve_position(manifolds, this, this.positionIterations);
1432
1720
 
1433
1721
  for (let i = 0; i < count_after_wake; i++) {
1434
- const idx = this.storage.awake_at(i);
1435
- const rb = this.__bodies[idx];
1436
- const tr = this.__transforms[idx];
1722
+ const idx = storage.awake_at(i);
1723
+ const rb = bodies[idx];
1724
+ const tr = transforms[idx];
1437
1725
  const base = idx * 6;
1438
- integrate_position(rb, tr, h,
1439
- pseudoVel[base], pseudoVel[base + 1], pseudoVel[base + 2],
1726
+ integrate_position(ss_data, idx * SBS_STRIDE, rb, tr, h,
1727
+ pseudoVel[base], pseudoVel[base + 1], pseudoVel[base + 2],
1440
1728
  pseudoVel[base + 3], pseudoVel[base + 4], pseudoVel[base + 5]);
1441
1729
  }
1442
1730
  }
1443
1731
 
1444
1732
  // Stage 8: one-shot restitution, after the substep loop, keyed off
1445
1733
  // the approach velocity captured at prepare time.
1446
- apply_restitution(this.manifolds, this);
1734
+ apply_restitution(manifolds, this);
1735
+
1736
+ // Scatter the solved persistent linear / angular velocity back onto the
1737
+ // RigidBody components (pose was written through to the Transforms each
1738
+ // substep). After this the bodies are authoritative again for CCD,
1739
+ // interpolation recording, and the sleep test below.
1740
+ solver_state.scatter(bodies);
1447
1741
 
1448
1742
  // Stage 8.5: continuous collision — sweep CCD-flagged fast movers along
1449
1743
  // their net step translation and stop them at the first blocker, so they
@@ -1454,18 +1748,24 @@ export class PhysicsSystem extends System {
1454
1748
  ccd_resolve(this);
1455
1749
  }
1456
1750
 
1751
+ // Producer: record the post-step authoritative pose of every awake
1752
+ // interpolated body under this step's tick. Before the sleep test, so a
1753
+ // body that settles this step still records its final pose for the last
1754
+ // interpolation interval.
1755
+ this.__interp_record();
1756
+
1457
1757
  // Stage 9: sleep test.
1458
1758
  this.__sleep_test(dt);
1459
1759
 
1460
1760
  // Stage 10: diff manifolds against the previous frame and dispatch
1461
1761
  // Begin / Stay / End events. MUST run before advance_frame, which
1462
1762
  // rolls the touched flags.
1463
- diff_manifolds(this.manifolds, this.storage, this.contactEvents);
1763
+ diff_manifolds(manifolds, storage, this.contactEvents);
1464
1764
  this.__dispatch_contact_events();
1465
1765
 
1466
1766
  // Stage 11 (end-of-step): roll touched → prev_touched and evict slots
1467
1767
  // whose pair has not been touched within the grace window.
1468
- this.manifolds.advance_frame();
1768
+ manifolds.advance_frame();
1469
1769
  }
1470
1770
  }
1471
1771