@driftengine/physics 3.61.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 (136) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +503 -0
  4. package/dist/bodies.d.ts +121 -0
  5. package/dist/bodies.js +298 -0
  6. package/dist/buoyancy.d.ts +46 -0
  7. package/dist/buoyancy.js +45 -0
  8. package/dist/cloth.d.ts +226 -0
  9. package/dist/cloth.js +587 -0
  10. package/dist/collide/aabb.d.ts +10 -0
  11. package/dist/collide/aabb.js +11 -0
  12. package/dist/collide/body.d.ts +202 -0
  13. package/dist/collide/body.js +142 -0
  14. package/dist/collide/eject.d.ts +39 -0
  15. package/dist/collide/eject.js +142 -0
  16. package/dist/collide/index.d.ts +18 -0
  17. package/dist/collide/index.js +5 -0
  18. package/dist/collide/scratch.d.ts +8 -0
  19. package/dist/collide/scratch.js +9 -0
  20. package/dist/collide/segment.d.ts +12 -0
  21. package/dist/collide/segment.js +109 -0
  22. package/dist/collide/sweep.d.ts +3 -0
  23. package/dist/collide/sweep.js +1036 -0
  24. package/dist/colliderSet.d.ts +239 -0
  25. package/dist/colliderSet.js +546 -0
  26. package/dist/compoundContact.d.ts +70 -0
  27. package/dist/compoundContact.js +221 -0
  28. package/dist/controller.d.ts +351 -0
  29. package/dist/controller.js +641 -0
  30. package/dist/cylinder.d.ts +78 -0
  31. package/dist/cylinder.js +294 -0
  32. package/dist/cylinderContact.d.ts +7 -0
  33. package/dist/cylinderContact.js +704 -0
  34. package/dist/decompose.d.ts +105 -0
  35. package/dist/decompose.js +979 -0
  36. package/dist/events.d.ts +43 -0
  37. package/dist/events.js +95 -0
  38. package/dist/executor.d.ts +147 -0
  39. package/dist/executor.js +173 -0
  40. package/dist/faces.d.ts +26 -0
  41. package/dist/faces.js +34 -0
  42. package/dist/fingerprint.d.ts +61 -0
  43. package/dist/fingerprint.js +139 -0
  44. package/dist/groundMotor.d.ts +34 -0
  45. package/dist/groundMotor.js +41 -0
  46. package/dist/heightfieldShape.d.ts +128 -0
  47. package/dist/heightfieldShape.js +328 -0
  48. package/dist/index.d.ts +73 -0
  49. package/dist/index.js +47 -0
  50. package/dist/island.d.ts +55 -0
  51. package/dist/island.js +227 -0
  52. package/dist/islandSolve.d.ts +70 -0
  53. package/dist/islandSolve.js +171 -0
  54. package/dist/islandWorker.d.ts +18 -0
  55. package/dist/islandWorker.js +125 -0
  56. package/dist/joints.d.ts +180 -0
  57. package/dist/joints.js +1094 -0
  58. package/dist/manifold.d.ts +89 -0
  59. package/dist/manifold.js +608 -0
  60. package/dist/mass.d.ts +53 -0
  61. package/dist/mass.js +353 -0
  62. package/dist/meshContact.d.ts +57 -0
  63. package/dist/meshContact.js +393 -0
  64. package/dist/meshShape.d.ts +76 -0
  65. package/dist/meshShape.js +175 -0
  66. package/dist/pairs.d.ts +62 -0
  67. package/dist/pairs.js +154 -0
  68. package/dist/query.d.ts +71 -0
  69. package/dist/query.js +683 -0
  70. package/dist/ragdoll.d.ts +135 -0
  71. package/dist/ragdoll.js +489 -0
  72. package/dist/shape.d.ts +177 -0
  73. package/dist/shape.js +533 -0
  74. package/dist/solver.d.ts +136 -0
  75. package/dist/solver.js +601 -0
  76. package/dist/stage.d.ts +167 -0
  77. package/dist/stage.js +350 -0
  78. package/dist/stallEscape.d.ts +141 -0
  79. package/dist/stallEscape.js +254 -0
  80. package/dist/sweptStep.d.ts +173 -0
  81. package/dist/sweptStep.js +472 -0
  82. package/dist/tree.d.ts +61 -0
  83. package/dist/tree.js +348 -0
  84. package/dist/vehicle.d.ts +154 -0
  85. package/dist/vehicle.js +343 -0
  86. package/dist/workerPool.d.ts +107 -0
  87. package/dist/workerPool.js +239 -0
  88. package/dist/workers.d.ts +37 -0
  89. package/dist/workers.js +35 -0
  90. package/dist/world.d.ts +298 -0
  91. package/dist/world.js +712 -0
  92. package/package.json +55 -0
  93. package/src/bodies.ts +369 -0
  94. package/src/buoyancy.ts +78 -0
  95. package/src/cloth.ts +734 -0
  96. package/src/collide/aabb.ts +28 -0
  97. package/src/collide/body.ts +336 -0
  98. package/src/collide/eject.ts +156 -0
  99. package/src/collide/index.ts +26 -0
  100. package/src/collide/scratch.ts +10 -0
  101. package/src/collide/segment.ts +127 -0
  102. package/src/collide/sweep.ts +1132 -0
  103. package/src/colliderSet.ts +637 -0
  104. package/src/compoundContact.ts +284 -0
  105. package/src/controller.ts +817 -0
  106. package/src/cylinder.ts +340 -0
  107. package/src/cylinderContact.ts +825 -0
  108. package/src/decompose.ts +1198 -0
  109. package/src/events.ts +108 -0
  110. package/src/executor.ts +224 -0
  111. package/src/faces.ts +37 -0
  112. package/src/fingerprint.ts +146 -0
  113. package/src/groundMotor.ts +53 -0
  114. package/src/heightfieldShape.ts +395 -0
  115. package/src/index.ts +164 -0
  116. package/src/island.ts +219 -0
  117. package/src/islandSolve.ts +218 -0
  118. package/src/islandWorker.ts +141 -0
  119. package/src/joints.ts +1367 -0
  120. package/src/manifold.ts +798 -0
  121. package/src/mass.ts +424 -0
  122. package/src/meshContact.ts +443 -0
  123. package/src/meshShape.ts +261 -0
  124. package/src/pairs.ts +169 -0
  125. package/src/query.ts +844 -0
  126. package/src/ragdoll.ts +662 -0
  127. package/src/shape.ts +656 -0
  128. package/src/solver.ts +873 -0
  129. package/src/stage.ts +406 -0
  130. package/src/stallEscape.ts +362 -0
  131. package/src/sweptStep.ts +609 -0
  132. package/src/tree.ts +366 -0
  133. package/src/vehicle.ts +489 -0
  134. package/src/workerPool.ts +283 -0
  135. package/src/workers.ts +38 -0
  136. package/src/world.ts +961 -0
package/dist/world.js ADDED
@@ -0,0 +1,712 @@
1
+ import { BODY_DYNAMIC, BODY_SENSOR, BODY_STATIC, BodySet } from './bodies.js';
2
+ import { shapeBounds } from './shape.js';
3
+ import { collideShapes, createManifold } from './manifold.js';
4
+ import { MAX_MESH_MANIFOLDS, collideMesh, isMesh } from './meshContact.js';
5
+ import { MAX_BODY_PARTS, MAX_PAIR_MANIFOLDS, collideCompound, partsBoundRadius, } from './compoundContact.js';
6
+ import { MAX_BODIES, PairSet, pairA, pairB, pairKey } from './pairs.js';
7
+ import { ContactConstraints, applyRestitution, prepareContact, solveContacts, warmStart, } from './solver.js';
8
+ import { DynamicTree } from './tree.js';
9
+ import { overlapWorld, raycastWorld, shapecastWorld } from './query.js';
10
+ import { ContactEvents } from './events.js';
11
+ import { IslandSet } from './island.js';
12
+ import { JointSet, resetJointImpulses, solveJoints } from './joints.js';
13
+ import { SerialExecutor } from './executor.js';
14
+ import { DIAL_ALLOW_SLEEP, DIAL_ANGULAR_DAMPING, DIAL_CONTACT_DAMPING, DIAL_CONTACT_HERTZ, DIAL_COUNT, DIAL_DT, DIAL_FRICTION_MODEL, DIAL_GRAVITY_X, DIAL_GRAVITY_Y, DIAL_GRAVITY_Z, DIAL_ITERATIONS, DIAL_JOINT_DAMPING, DIAL_JOINT_HERTZ, DIAL_LINEAR_DAMPING, DIAL_SUBSTEPS, SLEEP_TICKS, bodyAsleep, frictionDial, solveIslandInto, } from './islandSolve.js';
15
+ const IMPULSE_I = new Float32Array(6);
16
+ const SLEEP_LINEAR = 0.05;
17
+ const SLEEP_ANGULAR = 0.05;
18
+ export class PhysicsWorld {
19
+ bodies = new BodySet();
20
+ joints = new JointSet();
21
+ /** What touched what during the last tick, ordered by pair key. Drain it after `step`. */
22
+ events = new ContactEvents();
23
+ /**
24
+ * Pairs that never collide, whatever their layers say.
25
+ *
26
+ * **The mask says what a body is; this says who two bodies are to each other.** See `pairKey`
27
+ * for why the two are not the same question and why a ragdoll can only be expressed by the
28
+ * second one.
29
+ *
30
+ * Kept as a set of packed keys and consulted only when it holds something, so a world that
31
+ * never calls `ignorePair` pays one integer compare per candidate pair.
32
+ */
33
+ ignoredPairs = new Set();
34
+ substeps;
35
+ iterations;
36
+ gravityX;
37
+ gravityY;
38
+ gravityZ;
39
+ contactHertz;
40
+ contactDamping;
41
+ jointHertz;
42
+ jointDamping;
43
+ linearDamping;
44
+ angularDamping;
45
+ speculativeMargin;
46
+ allowSleep;
47
+ /** Read at every substep, so a consumer may switch it between ticks. See `WorldOptions`. */
48
+ frictionModel;
49
+ /** Who runs the islands. Swappable, and a gate asserts two characters agree. */
50
+ executor = new SerialExecutor();
51
+ /**
52
+ * What the island solving is actually doing.
53
+ *
54
+ * **Read it rather than assuming the `workers` option took.** A pool needs a cross-origin isolated
55
+ * page and a bundler that understands a worker entry, and when either is missing this says which
56
+ * in a sentence fit to put on a debug overlay. `createSplatSortWorker` warns once on the console
57
+ * for the same reason; this is the same fact as data, because a console warning is not something a
58
+ * consumer can show a player or assert in a test.
59
+ */
60
+ parallelism;
61
+ /**
62
+ * The scalars an island's solve reads, in the layout `islandSolve.ts` names.
63
+ *
64
+ * Written once a tick by `step` so a consumer may change gravity or the friction model between
65
+ * ticks and have every character see it, including one reading them out of shared memory.
66
+ */
67
+ dials = new Float64Array(DIAL_COUNT);
68
+ tree = new DynamicTree();
69
+ islands = new IslandSet();
70
+ stepDt = 1 / 60;
71
+ leafOf = new Int32Array(64);
72
+ pairs = new PairSet();
73
+ constraints = new ContactConstraints();
74
+ manifold = createManifold();
75
+ box = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
76
+ poseA = { x: 0, y: 0, z: 0, qx: 0, qy: 0, qz: 0, qw: 1 };
77
+ poseB = { x: 0, y: 0, z: 0, qx: 0, qy: 0, qz: 0, qw: 1 };
78
+ /** Previous tick's impulses, by pair key then feature id. */
79
+ /**
80
+ * One manifold per contact plane a mesh pair produced. Allocated once; the tick may not.
81
+ *
82
+ * `MAX_MESH_MANIFOLDS` planes is a body wedged in a corner with room to spare. A pair that fills
83
+ * it drops the rest, which for a convex body against a mesh means dropping the shallowest planes
84
+ * — the tree returns candidates in its own order, so what is kept is the first eight it met.
85
+ */
86
+ /**
87
+ * The manifold set a compound pair fills, sized so a compound against one convex shape cannot
88
+ * overflow: one plane per part, and `MAX_PAIR_MANIFOLDS` is `MAX_BODY_PARTS`.
89
+ *
90
+ * Separate from `meshManifolds` rather than one larger set shared by both, because `collideMesh`
91
+ * fills up to the length it is given — handing it thirty-two would change how many contact planes
92
+ * a shipped mesh pair produces, which is a behaviour change nobody asked for.
93
+ */
94
+ pairManifolds = Array.from({ length: MAX_PAIR_MANIFOLDS }, createManifold);
95
+ meshManifolds = Array.from({ length: MAX_MESH_MANIFOLDS }, () => createManifold());
96
+ warm = new Map();
97
+ nextWarm = new Map();
98
+ warmN = new Float32Array(4);
99
+ warmT1 = new Float32Array(4);
100
+ warmT2 = new Float32Array(4);
101
+ constructor(options = {}) {
102
+ this.substeps = Math.max(1, options.substeps ?? 4);
103
+ this.iterations = Math.max(1, options.iterations ?? 1);
104
+ this.gravityX = options.gravityX ?? 0;
105
+ this.gravityY = options.gravityY ?? -9.81;
106
+ this.gravityZ = options.gravityZ ?? 0;
107
+ this.contactHertz = options.contactHertz ?? 30;
108
+ this.contactDamping = options.contactDamping ?? 10;
109
+ this.jointHertz = options.jointHertz ?? 60;
110
+ this.jointDamping = options.jointDamping ?? 2;
111
+ this.linearDamping = options.linearDamping ?? 0;
112
+ this.angularDamping = options.angularDamping ?? 0.05;
113
+ this.speculativeMargin = options.speculativeMargin ?? 0.04;
114
+ this.allowSleep = options.allowSleep ?? true;
115
+ this.frictionModel = options.frictionModel ?? 'box';
116
+ /*
117
+ * **What was asked for and what this machine will give are two numbers, and this used to
118
+ * report the second under the first's name.**
119
+ *
120
+ * `Parallelism.requested` is documented as "workers the consumer asked for" and `reason` as
121
+ * "empty when `running` matches `requested`" — and a world asked for four on a two-core machine
122
+ * reported `requested: 1, running: 1, reason: ''`. Every field agreed with every other and none
123
+ * of them said that three quarters of the request had been dropped. That is the silent clamp
124
+ * this whole surface exists to avoid: `parallelism`'s own note says to read it *rather than
125
+ * assuming the `workers` option took*, and on the most common shortfall there is it had nothing
126
+ * to say.
127
+ *
128
+ * Found by CI rather than here, which is the other half of it: a character has two cores and this
129
+ * machine has twenty-four, so the clamp never bit locally and the tests that assert the
130
+ * documented meaning were red on every push.
131
+ */
132
+ const asked = Math.max(0, Math.floor(options.workers ?? 0));
133
+ const granted = clampWorkers(asked);
134
+ this.parallelism = { requested: asked, running: 0, reason: '' };
135
+ if (granted > 0) {
136
+ if (options.pool === undefined) {
137
+ this.parallelism.reason =
138
+ 'workers were asked for but no pool was given. Import createIslandPool from ' +
139
+ '@driftengine/physics/src/workers.ts and pass it as the pool option; it is behind its ' +
140
+ 'own specifier so that importing PhysicsWorld does not put a worker in your bundle.';
141
+ }
142
+ else {
143
+ const pool = options.pool({ workers: granted, spawn: options.spawn });
144
+ if (pool.executor !== null)
145
+ this.executor = pool.executor;
146
+ this.parallelism.running = pool.running;
147
+ this.parallelism.reason = pool.reason;
148
+ }
149
+ }
150
+ /*
151
+ * The clamp, said out loud — but only where nothing else already explained the shortfall. A
152
+ * pool that could not start has a better sentence than this one and keeps it.
153
+ */
154
+ if (this.parallelism.reason === '' && this.parallelism.running < asked) {
155
+ const cores = coreCount();
156
+ this.parallelism.reason =
157
+ `${asked} workers were asked for and ${this.parallelism.running} run: this machine ` +
158
+ `reports ${cores === undefined ? 'an unknown number of' : String(cores)} cores, and one ` +
159
+ 'is kept for the thread that steps the world.';
160
+ }
161
+ }
162
+ /**
163
+ * Stop the worker pool, if there is one. Safe to call twice, and safe on a world without one.
164
+ *
165
+ * **A world with a pool must be disposed.** Its workers hold the staging `SharedArrayBuffer`, so
166
+ * a consumer replacing a world between levels leaks both the threads and every body's state with
167
+ * them.
168
+ */
169
+ dispose() {
170
+ this.executor.dispose?.();
171
+ this.parallelism.running = 0;
172
+ }
173
+ /** Join two bodies. The relative pose they are in now becomes the one the joint holds. */
174
+ addJoint(desc) {
175
+ return this.joints.add(this.bodies, desc);
176
+ }
177
+ addBody(desc) {
178
+ /*
179
+ * **A triangle mesh may only be static, and this is where that is enforced.** A moving one
180
+ * needs its tree refitted every tick and needs a mass tensor a triangle soup does not have;
181
+ * the usual answer to a moving concave thing is convex decomposition, which is a row of its
182
+ * own. Refused at `add` rather than left to produce a body that falls through the world, which
183
+ * is what a mesh with no volume and no inertia would do.
184
+ */
185
+ if (desc.shape?.triangles !== undefined && desc.type !== BODY_STATIC) {
186
+ throw new Error('addBody: a triangle mesh shape may only be static. A moving concave body wants convex ' +
187
+ 'decomposition into hulls, which `decomposeConvex` produces and `shapes` accepts.');
188
+ }
189
+ /*
190
+ * **Both, or neither, is refused at the call rather than resolved in the tick.** There is no
191
+ * sensible reading of a body carrying a single shape and a list of them: the narrow phase would
192
+ * have to pick one, and either pick is a body that collides as something nobody described.
193
+ */
194
+ if ((desc.shape === undefined) === (desc.shapes === undefined)) {
195
+ throw new Error('addBody: a body is built from exactly one of `shape` or `shapes`, and this one has ' +
196
+ (desc.shape === undefined ? 'neither' : 'both'));
197
+ }
198
+ if (desc.shapes !== undefined) {
199
+ if (desc.shapes.length === 0)
200
+ throw new Error('addBody: `shapes` is empty');
201
+ /*
202
+ * **A cap is a refusal here and not a truncation in the tick.** The manifold set is sized to
203
+ * `MAX_PAIR_MANIFOLDS`, and a dropped part is a limb of the body that stops colliding — one
204
+ * leg of a chair sinking through a floor while the other three hold. Failing at construction
205
+ * is this engine's "fail fast at init, never throw in the frame loop" applied where it can be.
206
+ */
207
+ if (desc.shapes.length > MAX_BODY_PARTS) {
208
+ throw new Error(`addBody: ${desc.shapes.length} parts exceeds MAX_BODY_PARTS of ${MAX_BODY_PARTS} — ` +
209
+ 'ask decomposeConvex for fewer hulls');
210
+ }
211
+ for (const part of desc.shapes) {
212
+ if (part.triangles !== undefined) {
213
+ throw new Error('addBody: a triangle mesh cannot be a part of a compound body');
214
+ }
215
+ }
216
+ }
217
+ const i = this.bodies.add(desc);
218
+ if (i >= this.leafOf.length) {
219
+ const wider = new Int32Array(this.leafOf.length * 2);
220
+ wider.set(this.leafOf);
221
+ this.leafOf = wider;
222
+ }
223
+ this.leafOf[i] = this.tree.insert(i, this.boundsOf(i));
224
+ return i;
225
+ }
226
+ /**
227
+ * Never collide these two, whichever order they are given in and whatever their layers say.
228
+ *
229
+ * Both indices are followed through `removeBody`, so an exclusion cannot outlive the body it
230
+ * was made about and cannot be inherited by whatever takes its slot.
231
+ */
232
+ ignorePair(a, b) {
233
+ if (a === b)
234
+ return;
235
+ this.ignoredPairs.add(pairKey(a, b));
236
+ }
237
+ /** Undo one `ignorePair`. Doing it to a pair that was never excluded is not an error. */
238
+ allowPair(a, b) {
239
+ this.ignoredPairs.delete(pairKey(a, b));
240
+ }
241
+ /** Whether these two are excluded. */
242
+ pairIgnored(a, b) {
243
+ return this.ignoredPairs.has(pairKey(a, b));
244
+ }
245
+ /**
246
+ * Follow the exclusions through a swap-remove.
247
+ *
248
+ * **A body index is a slot and not an identity**, which is the same fact `removeBody` clears the
249
+ * warm cache and the contact events for: removing one moves the last body into the hole, so a
250
+ * remembered index is either gone or now names somebody else. An exclusion left alone would
251
+ * quietly stop the newcomer colliding with whatever the departed was jointed to.
252
+ */
253
+ followRemoval(index, moved) {
254
+ if (this.ignoredPairs.size === 0)
255
+ return;
256
+ const kept = [];
257
+ for (const key of this.ignoredPairs) {
258
+ const a = Math.floor(key / MAX_BODIES);
259
+ const b = key % MAX_BODIES;
260
+ /* The body that left takes its exclusions with it. */
261
+ if (a === index || b === index)
262
+ continue;
263
+ if (moved >= 0 && (a === moved || b === moved)) {
264
+ kept.push(pairKey(a === moved ? index : a, b === moved ? index : b));
265
+ continue;
266
+ }
267
+ kept.push(key);
268
+ }
269
+ this.ignoredPairs.clear();
270
+ for (const key of kept)
271
+ this.ignoredPairs.add(key);
272
+ }
273
+ removeBody(index) {
274
+ this.tree.remove(this.leafOf[index] ?? 0);
275
+ const moved = this.bodies.remove(index);
276
+ this.followRemoval(index, moved);
277
+ if (moved >= 0) {
278
+ this.leafOf[index] = this.leafOf[moved] ?? 0;
279
+ // The tree stores the body index it was inserted with, so it has to learn the new one.
280
+ this.tree.proxy[this.leafOf[index] ?? 0] = index;
281
+ }
282
+ this.warm.clear();
283
+ // Every index past the removed one may have moved, so a remembered pair is a lie.
284
+ this.events.clear();
285
+ }
286
+ /** Advance one fixed tick. */
287
+ step(dt) {
288
+ const bodies = this.bodies;
289
+ for (let i = 0; i < bodies.count; i++) {
290
+ if (bodies.type[i] === BODY_STATIC)
291
+ continue;
292
+ this.tree.move(this.leafOf[i] ?? 0, this.boundsOf(i));
293
+ }
294
+ const pairCount = this.pairs.build(bodies, this.tree, this.leafOf, this.ignoredPairs);
295
+ this.constraints.clear();
296
+ this.nextWarm.clear();
297
+ this.events.begin();
298
+ let awake = 0;
299
+ for (let i = 0; i < pairCount; i++) {
300
+ const key = this.pairs.keys[i] ?? 0;
301
+ const a = pairA(key);
302
+ const b = pairB(key);
303
+ const shapeA = bodies.shape[a];
304
+ const shapeB = bodies.shape[b];
305
+ const partsA = bodies.shapes[a];
306
+ const partsB = bodies.shapes[b];
307
+ if ((!shapeA && !partsA) || (!shapeB && !partsB))
308
+ continue;
309
+ /*
310
+ * A sleeping pair still builds its constraint, and that is deliberate. Skipping it drops the
311
+ * contact, which drops the edge, which dissolves the island into singletons — so waking one
312
+ * box of a sleeping stack could no longer reach the boxes above it. The *solve* is what gets
313
+ * skipped, in `solveIsland`, which is the expensive half.
314
+ *
315
+ * **What this costs** is narrowphase for a sleeping stack, which sleeping was partly meant to
316
+ * save. **What would make it wrong** is a world of tens of thousands of sleeping bodies where
317
+ * that cost shows; the answer then is to persist island membership across sleep rather than
318
+ * to re-derive it from contacts, which is a larger change than this one.
319
+ */
320
+ this.readPose(a, this.poseA);
321
+ this.readPose(b, this.poseB);
322
+ /*
323
+ * **A mesh pair fills a set of manifolds where every other pair fills one.** A body in the
324
+ * corner of a mesh room has two or three contact planes, and reducing them to one picks a
325
+ * wall and lets the body through the others. The solver already supports it: `prepareContact`
326
+ * appends a constraint rather than replacing one, and `storeWarm` accumulates by pair.
327
+ */
328
+ /*
329
+ * **A compound pair fills a set too, for the same reason and by the same machinery.** One
330
+ * manifold per touching part, with the part pair folded into every feature id — the fold
331
+ * `collideMesh` already does with a triangle index, and for the identical reason: a body
332
+ * resting across two parts must not hand one part's accumulated impulse to the other on the
333
+ * tick a contact moves between them.
334
+ */
335
+ const compoundPair = partsA !== undefined || partsB !== undefined;
336
+ const meshPair = !compoundPair && (isMesh(shapeA) || isMesh(shapeB));
337
+ const set = compoundPair ? this.pairManifolds : this.meshManifolds;
338
+ const manifolds = compoundPair
339
+ ? collideCompound(shapeA, partsA, this.poseA, shapeB, partsB, this.poseB, this.speculativeMargin, this.pairManifolds)
340
+ : meshPair
341
+ ? collideMesh(shapeA, this.poseA, shapeB, this.poseB, this.speculativeMargin, this.meshManifolds)
342
+ : collideShapes(shapeA, this.poseA, shapeB, this.poseB, this.speculativeMargin, this.manifold)
343
+ ? 1
344
+ : 0;
345
+ if (manifolds === 0)
346
+ continue;
347
+ /*
348
+ * Deliberately no wake here. Waking every body that has a contact resets the counter on every
349
+ * tick, so a resting stack can never accumulate one and nothing in the world ever sleeps —
350
+ * which is what this did. A sleeping body touched by a moving one is woken by its *island*
351
+ * instead: they share one, the island is not all still, and every counter in it resets.
352
+ */
353
+ awake++;
354
+ this.events.touching(key);
355
+ /*
356
+ * A sensor is reported and never resolved. Building its constraint would push it out of the
357
+ * thing it is meant to notice, which is the whole of what a sensor is not.
358
+ */
359
+ if ((bodies.flags[a] ?? 0) & BODY_SENSOR || (bodies.flags[b] ?? 0) & BODY_SENSOR)
360
+ continue;
361
+ const stored = this.warm.get(key);
362
+ for (let m = 0; m < manifolds; m++) {
363
+ const manifold = compoundPair || meshPair ? set[m] : this.manifold;
364
+ this.loadWarm(stored, manifold);
365
+ prepareContact(bodies, a, b, manifold, this.constraints, this.warmN, this.warmT1, this.warmT2);
366
+ }
367
+ }
368
+ /*
369
+ * Integrate velocities, warm start, solve, integrate positions, relax — in that order, every
370
+ * substep.
371
+ *
372
+ * **The accumulated impulse is a per-substep quantity**, so warm starting only on the first
373
+ * substep hands a resting contact a quarter of the impulse it needs to hold anything up. A
374
+ * single box on static ground survived that, because the bias alone could carry it; a stack of
375
+ * ten collapsed completely.
376
+ *
377
+ * **And positions are integrated after the biased solve, not before it**, so the solve corrects
378
+ * the positions it is about to produce rather than ones already advanced past it. The relax
379
+ * pass then runs against the new positions with the bias off, which is what takes back the
380
+ * energy the bias added.
381
+ */
382
+ /*
383
+ * Islands are built here and each is advanced through the *whole* substep loop by the executor,
384
+ * rather than integration being a global pass with only the solve split. That is the shape a
385
+ * worker pool needs: one island, one character, no shared state — and a body with no contacts is a
386
+ * singleton island so nothing falls outside it.
387
+ */
388
+ this.stepDt = dt;
389
+ this.writeDials();
390
+ resetJointImpulses(this.joints);
391
+ const islandCount = this.islands.build(bodies, this.constraints, this.joints);
392
+ this.executor.run(islandCount, this);
393
+ const failure = this.executor.failure;
394
+ if (failure !== undefined && failure !== null && this.parallelism.running > 0) {
395
+ this.parallelism.running = 0;
396
+ this.parallelism.reason = failure;
397
+ }
398
+ this.events.end(pairA, pairB);
399
+ this.storeWarm();
400
+ this.updateSleep();
401
+ void awake;
402
+ }
403
+ /**
404
+ * Advance one island through every substep. The executor calls this; nothing else should.
405
+ *
406
+ * Integration is over this island's bodies only, which is what makes the call independent of
407
+ * every other island rather than merely ordered against them.
408
+ */
409
+ solveIsland(island) {
410
+ solveIslandInto(this.solveState, island);
411
+ }
412
+ /**
413
+ * The flat view of this world an island's solve needs, and the only view a worker could hold.
414
+ *
415
+ * Built fresh because `grow` replaces the arrays underneath it; the object is five references and
416
+ * one allocation, which the solve that follows dwarfs.
417
+ *
418
+ * **Public because a character off this thread needs it.** `SerialExecutor` calls `solveIsland`;
419
+ * a staged or pooled character mirrors this state into shared memory and solves that instead, which
420
+ * it cannot do through a method closed over the live arrays.
421
+ */
422
+ get solveState() {
423
+ return {
424
+ bodies: this.bodies,
425
+ constraints: this.constraints,
426
+ joints: this.joints,
427
+ islands: this.islands,
428
+ dials: this.dials,
429
+ };
430
+ }
431
+ /** Copy the dials into the block every character reads. Called once a tick, before the islands run. */
432
+ writeDials() {
433
+ const d = this.dials;
434
+ d[DIAL_SUBSTEPS] = this.substeps;
435
+ d[DIAL_ITERATIONS] = this.iterations;
436
+ d[DIAL_DT] = this.stepDt;
437
+ d[DIAL_GRAVITY_X] = this.gravityX;
438
+ d[DIAL_GRAVITY_Y] = this.gravityY;
439
+ d[DIAL_GRAVITY_Z] = this.gravityZ;
440
+ d[DIAL_LINEAR_DAMPING] = this.linearDamping;
441
+ d[DIAL_ANGULAR_DAMPING] = this.angularDamping;
442
+ d[DIAL_CONTACT_HERTZ] = this.contactHertz;
443
+ d[DIAL_CONTACT_DAMPING] = this.contactDamping;
444
+ d[DIAL_JOINT_HERTZ] = this.jointHertz;
445
+ d[DIAL_JOINT_DAMPING] = this.jointDamping;
446
+ d[DIAL_ALLOW_SLEEP] = this.allowSleep ? 1 : 0;
447
+ d[DIAL_FRICTION_MODEL] = frictionDial(this.frictionModel);
448
+ }
449
+ /** A body's world bounds, grown by the speculative margin so a contact is seen before it is made. */
450
+ boundsOf(i) {
451
+ const shape = this.bodies.shape[i];
452
+ const parts = this.bodies.shapes[i];
453
+ if (!shape && !parts) {
454
+ this.box.minX = this.bodies.posX[i] ?? 0;
455
+ this.box.minY = this.bodies.posY[i] ?? 0;
456
+ this.box.minZ = this.bodies.posZ[i] ?? 0;
457
+ this.box.maxX = this.box.minX;
458
+ this.box.maxY = this.box.minY;
459
+ this.box.maxZ = this.box.minZ;
460
+ return this.box;
461
+ }
462
+ /*
463
+ * The bound radius rather than the rotated hull: a sphere about the centre encloses the shape
464
+ * in every orientation, so a turning body never needs its bounds recomputed from its vertices.
465
+ * What this gives up is tightness for a long thin body; what would make it wrong is a
466
+ * measurement showing the broadphase reporting far more candidates than it should.
467
+ */
468
+ /*
469
+ * A compound's radius is the largest of its parts' and not their sum, because every part is
470
+ * already in the body's frame: one built off to a side carries that offset in its own radius.
471
+ */
472
+ const r = (parts ? partsBoundRadius(parts) : shape.boundRadius) +
473
+ this.speculativeMargin;
474
+ const x = this.bodies.posX[i] ?? 0;
475
+ const y = this.bodies.posY[i] ?? 0;
476
+ const z = this.bodies.posZ[i] ?? 0;
477
+ this.box.minX = x - r;
478
+ this.box.minY = y - r;
479
+ this.box.minZ = z - r;
480
+ this.box.maxX = x + r;
481
+ this.box.maxY = y + r;
482
+ this.box.maxZ = z + r;
483
+ return this.box;
484
+ }
485
+ readPose(i, out) {
486
+ out.x = this.bodies.posX[i] ?? 0;
487
+ out.y = this.bodies.posY[i] ?? 0;
488
+ out.z = this.bodies.posZ[i] ?? 0;
489
+ out.qx = this.bodies.rotX[i] ?? 0;
490
+ out.qy = this.bodies.rotY[i] ?? 0;
491
+ out.qz = this.bodies.rotZ[i] ?? 0;
492
+ out.qw = this.bodies.rotW[i] ?? 1;
493
+ }
494
+ loadWarm(stored, m) {
495
+ for (let i = 0; i < m.count; i++) {
496
+ this.warmN[i] = 0;
497
+ this.warmT1[i] = 0;
498
+ this.warmT2[i] = 0;
499
+ if (!stored)
500
+ continue;
501
+ // Matched by feature id, never by proximity: a tolerance would be order-dependent.
502
+ for (let k = 0; k + 3 < stored.length; k += 4) {
503
+ if (stored[k] === (m.featureIds[i] ?? 0)) {
504
+ this.warmN[i] = stored[k + 1] ?? 0;
505
+ this.warmT1[i] = stored[k + 2] ?? 0;
506
+ this.warmT2[i] = stored[k + 3] ?? 0;
507
+ break;
508
+ }
509
+ }
510
+ }
511
+ }
512
+ storeWarm() {
513
+ const c = this.constraints;
514
+ for (let k = 0; k < c.count; k++) {
515
+ const key = (c.bodyA[k] ?? 0) * MAX_BODIES + (c.bodyB[k] ?? 0);
516
+ const start = c.pointStart[k] ?? 0;
517
+ const n = c.pointCount[k] ?? 0;
518
+ /*
519
+ * **Appended rather than replaced, because a mesh pair produces several constraints.** Every
520
+ * other pair produces one and the two are the same code path; a `set` here would keep the
521
+ * last of a mesh pair's planes and throw the rest away, which is a floor that warm-starts
522
+ * and a wall that does not. `loadWarm` matches by feature id and `meshContact.ts` folds the
523
+ * triangle into every id, so one array per pair stays unambiguous.
524
+ */
525
+ const before = this.nextWarm.get(key);
526
+ const at = before?.length ?? 0;
527
+ const store = new Float32Array(at + n * 4);
528
+ if (before)
529
+ store.set(before);
530
+ for (let i = 0; i < n; i++) {
531
+ store[at + i * 4] = c.featureId[start + i] ?? 0;
532
+ store[at + i * 4 + 1] = c.normalImpulse[start + i] ?? 0;
533
+ store[at + i * 4 + 2] = c.tangent1Impulse[start + i] ?? 0;
534
+ store[at + i * 4 + 3] = c.tangent2Impulse[start + i] ?? 0;
535
+ }
536
+ this.nextWarm.set(key, store);
537
+ }
538
+ const swap = this.warm;
539
+ this.warm = this.nextWarm;
540
+ this.nextWarm = swap;
541
+ }
542
+ /**
543
+ * Islands sleep, not bodies.
544
+ *
545
+ * A box that slept while its neighbour was still moving would be woken by that neighbour on the
546
+ * very next tick and never rest — so a body accumulates its counter only when **every** body in
547
+ * its island is below both thresholds, and the whole island resets together when one is not.
548
+ *
549
+ * Tick-counted and never time-based, because sleeping changes results and results have to replay.
550
+ */
551
+ updateSleep() {
552
+ const bodies = this.bodies;
553
+ for (let island = 0; island < this.islands.count; island++) {
554
+ const from = this.islands.bodyStart[island] ?? 0;
555
+ const to = this.islands.bodyStart[island + 1] ?? from;
556
+ let allStill = true;
557
+ for (let at = from; at < to && allStill; at++) {
558
+ const i = this.islands.bodyOrder[at] ?? 0;
559
+ if (bodies.type[i] !== BODY_DYNAMIC)
560
+ continue;
561
+ const vx = bodies.velX[i] ?? 0;
562
+ const vy = bodies.velY[i] ?? 0;
563
+ const vz = bodies.velZ[i] ?? 0;
564
+ const wx = bodies.angX[i] ?? 0;
565
+ const wy = bodies.angY[i] ?? 0;
566
+ const wz = bodies.angZ[i] ?? 0;
567
+ if (vx * vx + vy * vy + vz * vz >= SLEEP_LINEAR * SLEEP_LINEAR ||
568
+ wx * wx + wy * wy + wz * wz >= SLEEP_ANGULAR * SLEEP_ANGULAR) {
569
+ allStill = false;
570
+ }
571
+ }
572
+ for (let at = from; at < to; at++) {
573
+ const i = this.islands.bodyOrder[at] ?? 0;
574
+ if (bodies.type[i] !== BODY_DYNAMIC)
575
+ continue;
576
+ bodies.sleepTicks[i] = allStill
577
+ ? Math.min(SLEEP_TICKS, (bodies.sleepTicks[i] ?? 0) + 1)
578
+ : 0;
579
+ }
580
+ }
581
+ }
582
+ /** Wake a body and everything sharing its island, which is what an external impulse must do. */
583
+ wakeIsland(index) {
584
+ const island = this.islands.islandOf[index] ?? -1;
585
+ if (island < 0) {
586
+ this.bodies.sleepTicks[index] = 0;
587
+ return;
588
+ }
589
+ const from = this.islands.bodyStart[island] ?? 0;
590
+ const to = this.islands.bodyStart[island + 1] ?? from;
591
+ for (let at = from; at < to; at++) {
592
+ this.bodies.sleepTicks[this.islands.bodyOrder[at] ?? 0] = 0;
593
+ }
594
+ }
595
+ /** Whether every body in an island has been still long enough to stop being solved. */
596
+ /** The nearest body a ray meets. Fills `out` and allocates nothing. */
597
+ raycast(ox, oy, oz, dx, dy, dz, maxDistance, out, filter) {
598
+ return raycastWorld(this.bodies, this.tree, ox, oy, oz, dx, dy, dz, maxDistance, out, filter);
599
+ }
600
+ /** The first body a swept shape touches. */
601
+ shapecast(shape, pose, dx, dy, dz, out, filter) {
602
+ return shapecastWorld(this.bodies, this.tree, shape, pose, dx, dy, dz, out, filter);
603
+ }
604
+ /** Every body overlapping a shape, filling `out` to its length. Returns how many were written. */
605
+ overlap(shape, pose, out, filter) {
606
+ return overlapWorld(this.bodies, this.tree, shape, pose, out, filter);
607
+ }
608
+ /**
609
+ * Apply an impulse at a world-space point.
610
+ *
611
+ * **The point is not optional**, and `bindings/core.ts` makes it required at the language boundary
612
+ * for the reason `AGENTS.md`'s step four gives: a caller who omits it gets an impulse through the
613
+ * centre of mass, which produces no rotation at all. That is a wrong *result* rather than an
614
+ * error, and it reads as a broken impulse rather than as a missing argument.
615
+ */
616
+ applyImpulse(body, px, py, pz, atX, atY, atZ) {
617
+ const im = this.bodies.invMass[body] ?? 0;
618
+ if (im === 0)
619
+ return;
620
+ this.bodies.velX[body] = (this.bodies.velX[body] ?? 0) + px * im;
621
+ this.bodies.velY[body] = (this.bodies.velY[body] ?? 0) + py * im;
622
+ this.bodies.velZ[body] = (this.bodies.velZ[body] ?? 0) + pz * im;
623
+ const rx = atX - (this.bodies.posX[body] ?? 0);
624
+ const ry = atY - (this.bodies.posY[body] ?? 0);
625
+ const rz = atZ - (this.bodies.posZ[body] ?? 0);
626
+ this.bodies.worldInverseInertia(body, IMPULSE_I);
627
+ const tx = ry * pz - rz * py;
628
+ const ty = rz * px - rx * pz;
629
+ const tz = rx * py - ry * px;
630
+ this.bodies.angX[body] =
631
+ (this.bodies.angX[body] ?? 0) +
632
+ (IMPULSE_I[0] ?? 0) * tx +
633
+ (IMPULSE_I[3] ?? 0) * ty +
634
+ (IMPULSE_I[4] ?? 0) * tz;
635
+ this.bodies.angY[body] =
636
+ (this.bodies.angY[body] ?? 0) +
637
+ (IMPULSE_I[3] ?? 0) * tx +
638
+ (IMPULSE_I[1] ?? 0) * ty +
639
+ (IMPULSE_I[5] ?? 0) * tz;
640
+ this.bodies.angZ[body] =
641
+ (this.bodies.angZ[body] ?? 0) +
642
+ (IMPULSE_I[4] ?? 0) * tx +
643
+ (IMPULSE_I[5] ?? 0) * ty +
644
+ (IMPULSE_I[2] ?? 0) * tz;
645
+ this.wakeIsland(body);
646
+ }
647
+ /** A force applied for one tick, which is an impulse of `force · dt`. */
648
+ applyForce(body, fx, fy, fz, dt, atX, atY, atZ) {
649
+ this.applyImpulse(body, fx * dt, fy * dt, fz * dt, atX, atY, atZ);
650
+ }
651
+ /** Set a body's velocity outright, waking whatever it was resting against. */
652
+ setVelocity(body, vx, vy, vz) {
653
+ this.bodies.velX[body] = vx;
654
+ this.bodies.velY[body] = vy;
655
+ this.bodies.velZ[body] = vz;
656
+ this.wakeIsland(body);
657
+ }
658
+ /**
659
+ * Move a body outright, which is a teleport rather than a push.
660
+ *
661
+ * The broadphase proxy moves with it, or the body would be found where it used to be until it
662
+ * next drifted out of its own fat bounds.
663
+ */
664
+ setPosition(body, x, y, z) {
665
+ this.bodies.posX[body] = x;
666
+ this.bodies.posY[body] = y;
667
+ this.bodies.posZ[body] = z;
668
+ this.tree.move(this.leafOf[body] ?? 0, this.boundsOf(body));
669
+ this.wakeIsland(body);
670
+ }
671
+ /** A body's mass, or zero where it has none because it is static or kinematic. */
672
+ bodyMass(body) {
673
+ const im = this.bodies.invMass[body] ?? 0;
674
+ return im > 0 ? 1 / im : 0;
675
+ }
676
+ /** How many islands the last step found. */
677
+ get islandCount() {
678
+ return this.islands.count;
679
+ }
680
+ /** Whether a body is currently asleep, for a caller deciding whether to bother. */
681
+ sleeping(index) {
682
+ return this.asleep(index);
683
+ }
684
+ /** Whether a body has been still long enough to stop being integrated. */
685
+ asleep(i) {
686
+ return bodyAsleep(this.bodies, this.allowSleep, i);
687
+ }
688
+ }
689
+ /**
690
+ * Never more workers than the machine has cores to spare.
691
+ *
692
+ * One is left for the thread that asked, which is solving its own share of the islands. A runtime
693
+ * that does not report `hardwareConcurrency` gets what it asked for, because guessing low there
694
+ * would silently halve a server's pool.
695
+ */
696
+ /**
697
+ * What the host says it has, or undefined where it says nothing.
698
+ *
699
+ * Its own function because two callers need it and one of them is a sentence a consumer reads:
700
+ * a clamp that cannot name the number it clamped to is the silent kind again.
701
+ */
702
+ function coreCount() {
703
+ return globalThis.navigator
704
+ ?.hardwareConcurrency;
705
+ }
706
+ function clampWorkers(wanted) {
707
+ const asked = Math.max(0, Math.floor(wanted));
708
+ const cores = coreCount();
709
+ if (asked === 0 || cores === undefined)
710
+ return asked;
711
+ return Math.max(1, Math.min(asked, cores - 1));
712
+ }