@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/src/island.ts ADDED
@@ -0,0 +1,219 @@
1
+ import { BODY_DYNAMIC } from './bodies.ts';
2
+ import type { BodySet } from './bodies.ts';
3
+ import type { JointSet } from './joints.ts';
4
+ import type { ContactConstraints } from './solver.ts';
5
+
6
+ /**
7
+ * Connected components of the contact graph, and the sleeping that rides on them.
8
+ *
9
+ * **Only dynamic bodies join islands.** A static floor touches everything standing on it, so
10
+ * letting it merge would make one island of the whole world and nothing would ever sleep. That is
11
+ * not an optimisation: it is what an island *means*, and it is why two towers on one floor are two.
12
+ *
13
+ * **A body with no contacts is a singleton island**, so every dynamic body belongs to exactly one
14
+ * and the executor can own the whole substep loop for it rather than integration being a separate
15
+ * global pass. That is the shape a worker pool needs.
16
+ *
17
+ * **Islands are ordered by their lowest body index, and bodies within one by index.** That comes
18
+ * from the *ascending scan* below, which numbers an island the first time it meets a member — not
19
+ * from union-find's root choice, which is why swapping the root to the larger index changes no
20
+ * result and no test catches it. Keeping the smaller one is convention and shortens paths a little;
21
+ * nothing depends on it. Nothing here iterates a `Map` or a `Set`.
22
+ */
23
+
24
+ export class IslandSet {
25
+ /** How many islands the last build found. */
26
+ count = 0;
27
+ /** Constraint indices grouped by island. */
28
+ constraintOrder: Int32Array;
29
+ /** Offsets into `constraintOrder`, length `count + 1`. */
30
+ constraintStart: Int32Array;
31
+ /** Dynamic body indices grouped by island. */
32
+ bodyOrder: Int32Array;
33
+ /** Offsets into `bodyOrder`, length `count + 1`. */
34
+ bodyStart: Int32Array;
35
+ /** Which island a body is in, or −1 for a body that is not dynamic. */
36
+ islandOf: Int32Array;
37
+
38
+ private parent: Int32Array;
39
+ private islandIndex: Int32Array;
40
+
41
+ constructor(capacity = 64) {
42
+ this.constraintOrder = new Int32Array(capacity);
43
+ this.constraintStart = new Int32Array(capacity + 1);
44
+ this.bodyOrder = new Int32Array(capacity);
45
+ this.bodyStart = new Int32Array(capacity + 1);
46
+ this.islandOf = new Int32Array(capacity);
47
+ this.parent = new Int32Array(capacity);
48
+ this.islandIndex = new Int32Array(capacity);
49
+ }
50
+
51
+ /** Joint indices grouped by island. */
52
+ jointOrder: Int32Array = new Int32Array(32);
53
+ /** Offsets into `jointOrder`, length `count + 1`. */
54
+ jointStart: Int32Array = new Int32Array(33);
55
+
56
+ build(bodies: BodySet, c: ContactConstraints, joints: JointSet): number {
57
+ const n = bodies.count;
58
+ this.ensureBodies(n);
59
+ this.ensureConstraints(c.count);
60
+
61
+ for (let i = 0; i < n; i++) this.parent[i] = i;
62
+ for (let k = 0; k < c.count; k++) {
63
+ const a = c.bodyA[k] ?? 0;
64
+ const b = c.bodyB[k] ?? 0;
65
+ if (bodies.type[a] !== BODY_DYNAMIC || bodies.type[b] !== BODY_DYNAMIC) continue;
66
+ this.union(a, b);
67
+ }
68
+ /*
69
+ * A joint is an edge in the same graph a contact is. Two bodies held together would otherwise
70
+ * sleep independently and be woken independently, and a jointed pair whose halves disagree
71
+ * about being asleep pulls apart.
72
+ */
73
+ for (let k = 0; k < joints.count; k++) {
74
+ if (joints.broken[k]) continue;
75
+ const a = joints.bodyA[k] ?? 0;
76
+ const b = joints.bodyB[k] ?? 0;
77
+ if (bodies.type[a] !== BODY_DYNAMIC || bodies.type[b] !== BODY_DYNAMIC) continue;
78
+ this.union(a, b);
79
+ }
80
+
81
+ // Roots in ascending order become islands 0, 1, 2 …, so ordering follows body index.
82
+ let islands = 0;
83
+ for (let i = 0; i < n; i++) {
84
+ this.islandOf[i] = -1;
85
+ this.islandIndex[i] = -1;
86
+ }
87
+ for (let i = 0; i < n; i++) {
88
+ if (bodies.type[i] !== BODY_DYNAMIC) continue;
89
+ const root = this.find(i);
90
+ if (this.islandIndex[root] < 0) this.islandIndex[root] = islands++;
91
+ this.islandOf[i] = this.islandIndex[root] ?? -1;
92
+ }
93
+ this.count = islands;
94
+ if (islands + 1 > this.bodyStart.length) {
95
+ this.bodyStart = new Int32Array(islands + 1);
96
+ this.constraintStart = new Int32Array(islands + 1);
97
+ }
98
+
99
+ // Counting sort of bodies into islands: count, prefix, place.
100
+ this.bodyStart.fill(0);
101
+ for (let i = 0; i < n; i++) {
102
+ const island = this.islandOf[i] ?? -1;
103
+ if (island >= 0) this.bodyStart[island + 1]++;
104
+ }
105
+ for (let i = 0; i < islands; i++) {
106
+ this.bodyStart[i + 1] = (this.bodyStart[i + 1] ?? 0) + (this.bodyStart[i] ?? 0);
107
+ }
108
+ const bodyAt = new Int32Array(islands);
109
+ for (let i = 0; i < n; i++) {
110
+ const island = this.islandOf[i] ?? -1;
111
+ if (island < 0) continue;
112
+ this.bodyOrder[(this.bodyStart[island] ?? 0) + (bodyAt[island] ?? 0)] = i;
113
+ bodyAt[island]++;
114
+ }
115
+
116
+ // And of constraints, by whichever of their bodies is dynamic.
117
+ this.constraintStart.fill(0);
118
+ for (let k = 0; k < c.count; k++) {
119
+ const island = this.islandOfConstraint(bodies, c, k);
120
+ if (island >= 0) this.constraintStart[island + 1]++;
121
+ }
122
+ for (let i = 0; i < islands; i++) {
123
+ this.constraintStart[i + 1] =
124
+ (this.constraintStart[i + 1] ?? 0) + (this.constraintStart[i] ?? 0);
125
+ }
126
+ const conAt = new Int32Array(islands);
127
+ for (let k = 0; k < c.count; k++) {
128
+ const island = this.islandOfConstraint(bodies, c, k);
129
+ if (island < 0) continue;
130
+ this.constraintOrder[(this.constraintStart[island] ?? 0) + (conAt[island] ?? 0)] = k;
131
+ conAt[island]++;
132
+ }
133
+ // And of joints, the same way.
134
+ if (islands + 1 > this.jointStart.length) this.jointStart = new Int32Array(islands + 1);
135
+ if (joints.count > this.jointOrder.length) {
136
+ let size = this.jointOrder.length || 1;
137
+ while (size < joints.count) size *= 2;
138
+ this.jointOrder = new Int32Array(size);
139
+ }
140
+ this.jointStart.fill(0);
141
+ for (let k = 0; k < joints.count; k++) {
142
+ const island = this.islandOfJoint(bodies, joints, k);
143
+ if (island >= 0) this.jointStart[island + 1]++;
144
+ }
145
+ for (let i = 0; i < islands; i++) {
146
+ this.jointStart[i + 1] = (this.jointStart[i + 1] ?? 0) + (this.jointStart[i] ?? 0);
147
+ }
148
+ const jointAt = new Int32Array(islands);
149
+ for (let k = 0; k < joints.count; k++) {
150
+ const island = this.islandOfJoint(bodies, joints, k);
151
+ if (island < 0) continue;
152
+ this.jointOrder[(this.jointStart[island] ?? 0) + (jointAt[island] ?? 0)] = k;
153
+ jointAt[island]++;
154
+ }
155
+ return islands;
156
+ }
157
+
158
+ private islandOfJoint(bodies: BodySet, joints: JointSet, k: number): number {
159
+ if (joints.broken[k]) return -1;
160
+ const a = joints.bodyA[k] ?? 0;
161
+ const b = joints.bodyB[k] ?? 0;
162
+ if (bodies.type[a] === BODY_DYNAMIC) return this.islandOf[a] ?? -1;
163
+ if (bodies.type[b] === BODY_DYNAMIC) return this.islandOf[b] ?? -1;
164
+ return -1;
165
+ }
166
+
167
+ private islandOfConstraint(bodies: BodySet, c: ContactConstraints, k: number): number {
168
+ const a = c.bodyA[k] ?? 0;
169
+ const b = c.bodyB[k] ?? 0;
170
+ if (bodies.type[a] === BODY_DYNAMIC) return this.islandOf[a] ?? -1;
171
+ if (bodies.type[b] === BODY_DYNAMIC) return this.islandOf[b] ?? -1;
172
+ return -1;
173
+ }
174
+
175
+ /**
176
+ * The smaller index wins, which shortens paths slightly and is otherwise arbitrary.
177
+ *
178
+ * **It is not what orders islands** — the ascending scan in `build` is. Said here because the
179
+ * version of this comment that claimed otherwise sent a reader looking for a property the code
180
+ * does not have and does not need.
181
+ */
182
+ private union(a: number, b: number): void {
183
+ const ra = this.find(a);
184
+ const rb = this.find(b);
185
+ if (ra === rb) return;
186
+ if (ra < rb) this.parent[rb] = ra;
187
+ else this.parent[ra] = rb;
188
+ }
189
+
190
+ private find(i: number): number {
191
+ let root = i;
192
+ while ((this.parent[root] ?? root) !== root) root = this.parent[root] ?? root;
193
+ // Path compression, so a long chain is walked once.
194
+ let at = i;
195
+ while ((this.parent[at] ?? at) !== at) {
196
+ const next = this.parent[at] ?? at;
197
+ this.parent[at] = root;
198
+ at = next;
199
+ }
200
+ return root;
201
+ }
202
+
203
+ private ensureBodies(n: number): void {
204
+ if (n <= this.parent.length) return;
205
+ let size = this.parent.length || 1;
206
+ while (size < n) size *= 2;
207
+ this.parent = new Int32Array(size);
208
+ this.islandOf = new Int32Array(size);
209
+ this.islandIndex = new Int32Array(size);
210
+ this.bodyOrder = new Int32Array(size);
211
+ }
212
+
213
+ private ensureConstraints(n: number): void {
214
+ if (n <= this.constraintOrder.length) return;
215
+ let size = this.constraintOrder.length || 1;
216
+ while (size < n) size *= 2;
217
+ this.constraintOrder = new Int32Array(size);
218
+ }
219
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * One island's substep loop, as a function over flat state, so a worker can run the same one.
3
+ *
4
+ * **This exists because `PhysicsWorld` cannot cross a realm boundary and an island's solve can.**
5
+ * The world holds `ConvexShape` objects, a `DynamicTree` and a `Map` of warm impulses, none of which
6
+ * a `SharedArrayBuffer` can carry. What the solve actually reads is four sets of typed arrays and
7
+ * eleven numbers, and that much is portable.
8
+ *
9
+ * **One implementation, two callers.** `PhysicsWorld.solveIsland` delegates here and so does
10
+ * `islandWorker.ts`; there is no second copy of the substep order anywhere. The rule against two
11
+ * implementations of one decision is what makes that non-negotiable, and `splatSortWorker.ts` is the
12
+ * precedent for satisfying it across a worker boundary — there by stringifying one function, here by
13
+ * both sides importing one module. **What would make this wrong** is a substep loop appearing inside
14
+ * the worker.
15
+ *
16
+ * The dials are a `Float64Array` and not an object because the worker reads them out of shared
17
+ * memory, and a shape both sides agree on without a per-tick message is the cheapest way to let a
18
+ * consumer change gravity between ticks and have the pool see it.
19
+ */
20
+
21
+ import { BODY_DYNAMIC, BODY_STATIC } from './bodies.ts';
22
+ import type { BodySet } from './bodies.ts';
23
+ import type { IslandSet } from './island.ts';
24
+ import { solveJoints } from './joints.ts';
25
+ import type { JointSet } from './joints.ts';
26
+ import { applyRestitution, solveContacts, warmStart } from './solver.ts';
27
+ import type { ContactConstraints, FrictionModel } from './solver.ts';
28
+
29
+ /* Slots in the dial block. Named because a worker reads them from shared memory by index. */
30
+ export const DIAL_SUBSTEPS = 0;
31
+ export const DIAL_ITERATIONS = 1;
32
+ export const DIAL_DT = 2;
33
+ export const DIAL_GRAVITY_X = 3;
34
+ export const DIAL_GRAVITY_Y = 4;
35
+ export const DIAL_GRAVITY_Z = 5;
36
+ export const DIAL_LINEAR_DAMPING = 6;
37
+ export const DIAL_ANGULAR_DAMPING = 7;
38
+ export const DIAL_CONTACT_HERTZ = 8;
39
+ export const DIAL_CONTACT_DAMPING = 9;
40
+ export const DIAL_JOINT_HERTZ = 10;
41
+ export const DIAL_JOINT_DAMPING = 11;
42
+ export const DIAL_ALLOW_SLEEP = 12;
43
+ export const DIAL_FRICTION_MODEL = 13;
44
+ /** How many slots a dial block has. */
45
+ export const DIAL_COUNT = 14;
46
+
47
+ /**
48
+ * `FrictionModel` crosses as a number, and the mapping is written once here.
49
+ *
50
+ * A string would cross a `postMessage` perfectly well; it would not survive being read out of a
51
+ * `Float64Array`, which is where the dials live so that no message is needed at all.
52
+ */
53
+ export const FRICTION_BOX = 0;
54
+ export const FRICTION_ELLIPTICAL = 1;
55
+
56
+ export function frictionDial(model: FrictionModel): number {
57
+ return model === 'elliptical' ? FRICTION_ELLIPTICAL : FRICTION_BOX;
58
+ }
59
+
60
+ export function frictionModelOf(dial: number): FrictionModel {
61
+ return dial === FRICTION_ELLIPTICAL ? 'elliptical' : 'box';
62
+ }
63
+
64
+ /** Ticks below the sleep thresholds before a body is willing to sleep. */
65
+ export const SLEEP_TICKS = 60;
66
+
67
+ /** Everything one island's solve reaches, and nothing else the world holds. */
68
+ export interface IslandSolveState {
69
+ bodies: BodySet;
70
+ constraints: ContactConstraints;
71
+ joints: JointSet;
72
+ islands: IslandSet;
73
+ /* Explicitly `ArrayBufferLike`: a pool's dials are a view on a `SharedArrayBuffer`, and the
74
+ default parameter is `ArrayBuffer`, which a shared one does not assign to. See
75
+ `parcel/store.ts` for the same note in the other direction. */
76
+ dials: Float64Array<ArrayBufferLike>;
77
+ }
78
+
79
+ /** Whether a body has been still long enough to stop being solved. */
80
+ export function bodyAsleep(bodies: BodySet, allowSleep: boolean, i: number): boolean {
81
+ return allowSleep && (bodies.sleepTicks[i] ?? 0) >= SLEEP_TICKS;
82
+ }
83
+
84
+ /** Whether every body in an island is asleep, which is what lets the island be skipped whole. */
85
+ export function islandAsleep(state: IslandSolveState, island: number): boolean {
86
+ const allowSleep = (state.dials[DIAL_ALLOW_SLEEP] ?? 0) !== 0;
87
+ const from = state.islands.bodyStart[island] ?? 0;
88
+ const to = state.islands.bodyStart[island + 1] ?? from;
89
+ for (let at = from; at < to; at++) {
90
+ if (!bodyAsleep(state.bodies, allowSleep, state.islands.bodyOrder[at] ?? 0)) return false;
91
+ }
92
+ return to > from;
93
+ }
94
+
95
+ function integrateVelocities(state: IslandSolveState, h: number, from: number, to: number): void {
96
+ const bodies = state.bodies;
97
+ const d = state.dials;
98
+ const allowSleep = (d[DIAL_ALLOW_SLEEP] ?? 0) !== 0;
99
+ const gx = d[DIAL_GRAVITY_X] ?? 0;
100
+ const gy = d[DIAL_GRAVITY_Y] ?? 0;
101
+ const gz = d[DIAL_GRAVITY_Z] ?? 0;
102
+ const ld = 1 / (1 + h * (d[DIAL_LINEAR_DAMPING] ?? 0));
103
+ const ad = 1 / (1 + h * (d[DIAL_ANGULAR_DAMPING] ?? 0));
104
+ for (let at = from; at < to; at++) {
105
+ const i = state.islands.bodyOrder[at] ?? 0;
106
+ if (bodies.type[i] !== BODY_DYNAMIC || bodyAsleep(bodies, allowSleep, i)) continue;
107
+ bodies.velX[i] = ((bodies.velX[i] ?? 0) + h * gx) * ld;
108
+ bodies.velY[i] = ((bodies.velY[i] ?? 0) + h * gy) * ld;
109
+ bodies.velZ[i] = ((bodies.velZ[i] ?? 0) + h * gz) * ld;
110
+ bodies.angX[i] = (bodies.angX[i] ?? 0) * ad;
111
+ bodies.angY[i] = (bodies.angY[i] ?? 0) * ad;
112
+ bodies.angZ[i] = (bodies.angZ[i] ?? 0) * ad;
113
+ }
114
+ }
115
+
116
+ function integratePositions(state: IslandSolveState, h: number, from: number, to: number): void {
117
+ const bodies = state.bodies;
118
+ const allowSleep = (state.dials[DIAL_ALLOW_SLEEP] ?? 0) !== 0;
119
+ for (let at = from; at < to; at++) {
120
+ const i = state.islands.bodyOrder[at] ?? 0;
121
+ if (bodies.type[i] === BODY_STATIC || bodyAsleep(bodies, allowSleep, i)) continue;
122
+ bodies.posX[i] = (bodies.posX[i] ?? 0) + h * (bodies.velX[i] ?? 0);
123
+ bodies.posY[i] = (bodies.posY[i] ?? 0) + h * (bodies.velY[i] ?? 0);
124
+ bodies.posZ[i] = (bodies.posZ[i] ?? 0) + h * (bodies.velZ[i] ?? 0);
125
+ // First-order exponential map, then renormalise. No sin, no cos.
126
+ const qx = bodies.rotX[i] ?? 0;
127
+ const qy = bodies.rotY[i] ?? 0;
128
+ const qz = bodies.rotZ[i] ?? 0;
129
+ const qw = bodies.rotW[i] ?? 1;
130
+ const wx = bodies.angX[i] ?? 0;
131
+ const wy = bodies.angY[i] ?? 0;
132
+ const wz = bodies.angZ[i] ?? 0;
133
+ const half = 0.5 * h;
134
+ let nx = qx + half * (wx * qw + wy * qz - wz * qy);
135
+ let ny = qy + half * (wy * qw + wz * qx - wx * qz);
136
+ let nz = qz + half * (wz * qw + wx * qy - wy * qx);
137
+ let nw = qw - half * (wx * qx + wy * qy + wz * qz);
138
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz + nw * nw) || 1;
139
+ nx /= len;
140
+ ny /= len;
141
+ nz /= len;
142
+ nw /= len;
143
+ bodies.rotX[i] = nx;
144
+ bodies.rotY[i] = ny;
145
+ bodies.rotZ[i] = nz;
146
+ bodies.rotW[i] = nw;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Advance one island through every substep.
152
+ *
153
+ * Integration is over this island's bodies only, which is what makes the call independent of every
154
+ * other island rather than merely ordered against them.
155
+ */
156
+ export function solveIslandInto(state: IslandSolveState, island: number): void {
157
+ if (islandAsleep(state, island)) return;
158
+ const d = state.dials;
159
+ const substeps = d[DIAL_SUBSTEPS] ?? 1;
160
+ const iterations = d[DIAL_ITERATIONS] ?? 1;
161
+ const dt = d[DIAL_DT] ?? 0;
162
+ const contactHertz = d[DIAL_CONTACT_HERTZ] ?? 0;
163
+ const contactDamping = d[DIAL_CONTACT_DAMPING] ?? 0;
164
+ const jointHertz = d[DIAL_JOINT_HERTZ] ?? 0;
165
+ const jointDamping = d[DIAL_JOINT_DAMPING] ?? 0;
166
+ const friction = frictionModelOf(d[DIAL_FRICTION_MODEL] ?? 0);
167
+
168
+ const h = dt / substeps;
169
+ const bodies = state.bodies;
170
+ const joints = state.joints;
171
+ const c = state.constraints;
172
+ const order = state.islands.constraintOrder;
173
+ const from = state.islands.constraintStart[island] ?? 0;
174
+ const to = state.islands.constraintStart[island + 1] ?? from;
175
+ const bodyFrom = state.islands.bodyStart[island] ?? 0;
176
+ const bodyTo = state.islands.bodyStart[island + 1] ?? bodyFrom;
177
+ const jointOrder = state.islands.jointOrder;
178
+ const jointFrom = state.islands.jointStart[island] ?? 0;
179
+ const jointTo = state.islands.jointStart[island + 1] ?? jointFrom;
180
+
181
+ for (let s = 0; s < substeps; s++) {
182
+ integrateVelocities(state, h, bodyFrom, bodyTo);
183
+ warmStart(bodies, c, order, from, to);
184
+ for (let it = 0; it < iterations; it++) {
185
+ // Joints before contacts: a joint is structural and a contact is transient, so letting a
186
+ // contact win produces a limb that pulls out of its socket when something lands on it.
187
+ solveJoints(
188
+ bodies,
189
+ joints,
190
+ jointOrder,
191
+ jointFrom,
192
+ jointTo,
193
+ h,
194
+ dt,
195
+ true,
196
+ jointHertz,
197
+ jointDamping,
198
+ );
199
+ solveContacts(bodies, c, order, from, to, h, true, contactHertz, contactDamping, friction);
200
+ }
201
+ integratePositions(state, h, bodyFrom, bodyTo);
202
+ // The relax pass, with the bias off, giving back the energy the bias added.
203
+ solveJoints(
204
+ bodies,
205
+ joints,
206
+ jointOrder,
207
+ jointFrom,
208
+ jointTo,
209
+ h,
210
+ dt,
211
+ false,
212
+ jointHertz,
213
+ jointDamping,
214
+ );
215
+ solveContacts(bodies, c, order, from, to, h, false, contactHertz, contactDamping, friction);
216
+ }
217
+ applyRestitution(bodies, c, order, from, to);
218
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * The worker end of the island pool.
3
+ *
4
+ * **It contains no physics.** It adopts views over the staging buffer, claims islands from a shared
5
+ * cursor, and calls `solveIslandInto` — the same function `PhysicsWorld.solveIsland` calls. A
6
+ * substep loop appearing in this file would be the defect the whole arrangement is shaped to
7
+ * prevent, because a second copy of the solve order drifts from the first the first time either is
8
+ * touched and nothing fails until a consumer's replay does.
9
+ *
10
+ * **It parks rather than polls.** `Atomics.wait` is permitted off a browser's main thread, so an
11
+ * idle worker costs nothing between ticks and wakes on an epoch bump with no message sent.
12
+ *
13
+ * **Two hosts, one file.** A browser module worker delivers messages through `self.onmessage`;
14
+ * `node:worker_threads` delivers them through `parentPort`. The difference is eight lines at the
15
+ * bottom, and paying them is what lets the gate spawn this exact entry point in Node instead of a
16
+ * stand-in that would prove nothing about threads.
17
+ */
18
+
19
+ import { solveIslandInto } from './islandSolve.ts';
20
+ import type { IslandSolveState } from './islandSolve.ts';
21
+ import { SolveStage } from './stage.ts';
22
+ import type { StagePlan } from './stage.ts';
23
+ import {
24
+ CTL_ACK,
25
+ CTL_CURSOR,
26
+ CTL_DONE,
27
+ CTL_EPOCH,
28
+ CTL_ISLANDS,
29
+ CTL_RELOAD,
30
+ CTL_SOLVED,
31
+ CTL_STOP,
32
+ } from './workerPool.ts';
33
+
34
+ /** The only message this worker takes: here is the shared state, and here is who you are. */
35
+ interface StageMessage {
36
+ kind: 'stage';
37
+ index: number;
38
+ control: ArrayBufferLike;
39
+ buffer: ArrayBufferLike;
40
+ plan: StagePlan;
41
+ }
42
+
43
+ let control: Int32Array<ArrayBufferLike> | null = null;
44
+ let state: IslandSolveState | null = null;
45
+ let index = 0;
46
+
47
+ /**
48
+ * Take the shared buffers, say so, and park.
49
+ *
50
+ * The acknowledgement is an atomic and not a reply message because the caller is waiting on it
51
+ * synchronously — it has just told every worker to come out of `Atomics.wait` and cannot process a
52
+ * message of its own until the tick is over.
53
+ */
54
+ function accept(message: StageMessage): void {
55
+ if (message?.kind !== 'stage') return;
56
+ control = new Int32Array(message.control as ArrayBuffer);
57
+ index = message.index;
58
+ state = SolveStage.adopt(message.buffer, message.plan).state;
59
+ /*
60
+ * **The epoch is read before the acknowledgement, and that order is the whole correctness of a
61
+ * re-plan.** The caller is spinning on `CTL_ACK` and bumps the epoch the moment it completes. Read
62
+ * after acknowledging and this worker can capture the epoch it was meant to wake for, then wait
63
+ * for the one after it — sleeping through the tick, so the join never completes and the pool
64
+ * declares itself dead. Reading first bounds `seen` below the next bump by construction.
65
+ */
66
+ const seen = Atomics.load(control, CTL_EPOCH);
67
+ Atomics.add(control, CTL_ACK, 1);
68
+ Atomics.notify(control, CTL_ACK);
69
+ park(seen);
70
+ }
71
+
72
+ /** Claim islands until there are none, then report what was solved. */
73
+ function work(ctl: Int32Array<ArrayBufferLike>, solve: IslandSolveState): void {
74
+ const count = Atomics.load(ctl, CTL_ISLANDS);
75
+ let solved = 0;
76
+ for (;;) {
77
+ const island = Atomics.add(ctl, CTL_CURSOR, 1);
78
+ if (island >= count) break;
79
+ solveIslandInto(solve, island);
80
+ solved += 1;
81
+ }
82
+ Atomics.add(ctl, CTL_SOLVED + index, solved);
83
+ Atomics.add(ctl, CTL_DONE, 1);
84
+ Atomics.notify(ctl, CTL_DONE);
85
+ }
86
+
87
+ /**
88
+ * Sleep until the epoch moves, then either solve, stop, or go back to the event loop.
89
+ *
90
+ * Returning on `CTL_RELOAD` is the whole reason a re-plan works: a parked worker cannot receive a
91
+ * `postMessage`, so the caller lets it out first and the pending message is delivered the moment
92
+ * this function returns.
93
+ */
94
+ function park(from: number): void {
95
+ const ctl = control;
96
+ if (ctl === null) return;
97
+ let seen = from;
98
+ for (;;) {
99
+ Atomics.wait(ctl, CTL_EPOCH, seen);
100
+ const epoch = Atomics.load(ctl, CTL_EPOCH);
101
+ if (epoch === seen) continue;
102
+ seen = epoch;
103
+ if (Atomics.load(ctl, CTL_STOP) === 1) return;
104
+ if (Atomics.load(ctl, CTL_RELOAD) === 1) return;
105
+ if (state !== null) work(ctl, state);
106
+ }
107
+ }
108
+
109
+ const scope = globalThis as unknown as {
110
+ postMessage?: unknown;
111
+ onmessage?: ((event: { data: StageMessage }) => void) | null;
112
+ };
113
+
114
+ if (typeof scope.postMessage === 'function') {
115
+ scope.onmessage = (event: { data: StageMessage }): void => accept(event.data);
116
+ } else {
117
+ /*
118
+ * **No top-level await here, and that is a consumer's build rather than a preference.**
119
+ *
120
+ * `workerPool.ts` builds this with `new Worker(new URL('./islandWorker.ts', import.meta.url))`,
121
+ * which every bundler detects statically and compiles as a worker whether or not the consumer
122
+ * ever starts a pool. Vite's default worker format is `iife`, and an IIFE cannot carry a
123
+ * top-level await: a consumer's production build answered `Module format "iife" does not support
124
+ * top-level await` on 2026-09-05 and could not build at all. A dev server serves module workers
125
+ * and never sees it, which is why `check:pool` passed over the same code.
126
+ *
127
+ * So the Node branch attaches its listener in a microtask instead. Nothing is missed: a Node
128
+ * `MessagePort` queues messages until `on('message')` attaches, so a `postMessage` issued before
129
+ * this resolves is delivered when it does.
130
+ *
131
+ * A variable specifier so a browser bundler leaves the import alone; this branch never runs in
132
+ * one.
133
+ */
134
+ const nodeWorkers = 'node:worker_threads';
135
+ void import(/* @vite-ignore */ nodeWorkers).then((mod) => {
136
+ const { parentPort } = mod as {
137
+ parentPort: { on(event: 'message', listener: (value: StageMessage) => void): void } | null;
138
+ };
139
+ parentPort?.on('message', accept);
140
+ });
141
+ }