@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.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +503 -0
- package/dist/bodies.d.ts +121 -0
- package/dist/bodies.js +298 -0
- package/dist/buoyancy.d.ts +46 -0
- package/dist/buoyancy.js +45 -0
- package/dist/cloth.d.ts +226 -0
- package/dist/cloth.js +587 -0
- package/dist/collide/aabb.d.ts +10 -0
- package/dist/collide/aabb.js +11 -0
- package/dist/collide/body.d.ts +202 -0
- package/dist/collide/body.js +142 -0
- package/dist/collide/eject.d.ts +39 -0
- package/dist/collide/eject.js +142 -0
- package/dist/collide/index.d.ts +18 -0
- package/dist/collide/index.js +5 -0
- package/dist/collide/scratch.d.ts +8 -0
- package/dist/collide/scratch.js +9 -0
- package/dist/collide/segment.d.ts +12 -0
- package/dist/collide/segment.js +109 -0
- package/dist/collide/sweep.d.ts +3 -0
- package/dist/collide/sweep.js +1036 -0
- package/dist/colliderSet.d.ts +239 -0
- package/dist/colliderSet.js +546 -0
- package/dist/compoundContact.d.ts +70 -0
- package/dist/compoundContact.js +221 -0
- package/dist/controller.d.ts +351 -0
- package/dist/controller.js +641 -0
- package/dist/cylinder.d.ts +78 -0
- package/dist/cylinder.js +294 -0
- package/dist/cylinderContact.d.ts +7 -0
- package/dist/cylinderContact.js +704 -0
- package/dist/decompose.d.ts +105 -0
- package/dist/decompose.js +979 -0
- package/dist/events.d.ts +43 -0
- package/dist/events.js +95 -0
- package/dist/executor.d.ts +147 -0
- package/dist/executor.js +173 -0
- package/dist/faces.d.ts +26 -0
- package/dist/faces.js +34 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.js +139 -0
- package/dist/groundMotor.d.ts +34 -0
- package/dist/groundMotor.js +41 -0
- package/dist/heightfieldShape.d.ts +128 -0
- package/dist/heightfieldShape.js +328 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +47 -0
- package/dist/island.d.ts +55 -0
- package/dist/island.js +227 -0
- package/dist/islandSolve.d.ts +70 -0
- package/dist/islandSolve.js +171 -0
- package/dist/islandWorker.d.ts +18 -0
- package/dist/islandWorker.js +125 -0
- package/dist/joints.d.ts +180 -0
- package/dist/joints.js +1094 -0
- package/dist/manifold.d.ts +89 -0
- package/dist/manifold.js +608 -0
- package/dist/mass.d.ts +53 -0
- package/dist/mass.js +353 -0
- package/dist/meshContact.d.ts +57 -0
- package/dist/meshContact.js +393 -0
- package/dist/meshShape.d.ts +76 -0
- package/dist/meshShape.js +175 -0
- package/dist/pairs.d.ts +62 -0
- package/dist/pairs.js +154 -0
- package/dist/query.d.ts +71 -0
- package/dist/query.js +683 -0
- package/dist/ragdoll.d.ts +135 -0
- package/dist/ragdoll.js +489 -0
- package/dist/shape.d.ts +177 -0
- package/dist/shape.js +533 -0
- package/dist/solver.d.ts +136 -0
- package/dist/solver.js +601 -0
- package/dist/stage.d.ts +167 -0
- package/dist/stage.js +350 -0
- package/dist/stallEscape.d.ts +141 -0
- package/dist/stallEscape.js +254 -0
- package/dist/sweptStep.d.ts +173 -0
- package/dist/sweptStep.js +472 -0
- package/dist/tree.d.ts +61 -0
- package/dist/tree.js +348 -0
- package/dist/vehicle.d.ts +154 -0
- package/dist/vehicle.js +343 -0
- package/dist/workerPool.d.ts +107 -0
- package/dist/workerPool.js +239 -0
- package/dist/workers.d.ts +37 -0
- package/dist/workers.js +35 -0
- package/dist/world.d.ts +298 -0
- package/dist/world.js +712 -0
- package/package.json +55 -0
- package/src/bodies.ts +369 -0
- package/src/buoyancy.ts +78 -0
- package/src/cloth.ts +734 -0
- package/src/collide/aabb.ts +28 -0
- package/src/collide/body.ts +336 -0
- package/src/collide/eject.ts +156 -0
- package/src/collide/index.ts +26 -0
- package/src/collide/scratch.ts +10 -0
- package/src/collide/segment.ts +127 -0
- package/src/collide/sweep.ts +1132 -0
- package/src/colliderSet.ts +637 -0
- package/src/compoundContact.ts +284 -0
- package/src/controller.ts +817 -0
- package/src/cylinder.ts +340 -0
- package/src/cylinderContact.ts +825 -0
- package/src/decompose.ts +1198 -0
- package/src/events.ts +108 -0
- package/src/executor.ts +224 -0
- package/src/faces.ts +37 -0
- package/src/fingerprint.ts +146 -0
- package/src/groundMotor.ts +53 -0
- package/src/heightfieldShape.ts +395 -0
- package/src/index.ts +164 -0
- package/src/island.ts +219 -0
- package/src/islandSolve.ts +218 -0
- package/src/islandWorker.ts +141 -0
- package/src/joints.ts +1367 -0
- package/src/manifold.ts +798 -0
- package/src/mass.ts +424 -0
- package/src/meshContact.ts +443 -0
- package/src/meshShape.ts +261 -0
- package/src/pairs.ts +169 -0
- package/src/query.ts +844 -0
- package/src/ragdoll.ts +662 -0
- package/src/shape.ts +656 -0
- package/src/solver.ts +873 -0
- package/src/stage.ts +406 -0
- package/src/stallEscape.ts +362 -0
- package/src/sweptStep.ts +609 -0
- package/src/tree.ts +366 -0
- package/src/vehicle.ts +489 -0
- package/src/workerPool.ts +283 -0
- package/src/workers.ts +38 -0
- 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
|
+
}
|