@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/cloth.ts ADDED
@@ -0,0 +1,734 @@
1
+ import { BODY_STATIC } from './bodies.ts';
2
+ import { LOCAL_POSE, closestOnCylinder, isCylinder, readCylinder } from './cylinder.ts';
3
+ import { faceCount } from './faces.ts';
4
+ import type { ShapePose } from './manifold.ts';
5
+ import type { ConvexShape } from './shape.ts';
6
+ import type { PhysicsWorld } from './world.ts';
7
+
8
+ /**
9
+ * Cloth, as position-based dynamics with compliance.
10
+ *
11
+ * **A separate solver from the rigid bodies, deliberately, and it is §5's decision in a different
12
+ * place.** Thousands of rotationless particles are the wrong data layout for a rigid constraint
13
+ * graph: every particle would carry an orientation it never uses and an inertia tensor that is
14
+ * always zero. Two solvers with two jobs, not two implementations of one decision.
15
+ *
16
+ * **Compliance rather than a stiffness constant**, for the same reason §7 states contact stiffness
17
+ * as a frequency: `α = compliance / h²` makes a constraint's stiffness independent of the step
18
+ * length, so the iteration count is a quality dial rather than a behaviour change. A raw
19
+ * position-based solver's stiffness is a function of how many times you iterated, which means
20
+ * tuning it is tuning the solver rather than the cloth.
21
+ *
22
+ * **Two-way coupled since 2026-08-27, by an impulse exchange rather than by a merged island.** The
23
+ * refusal this replaces was right about the price and wrong about the only way to pay it. Putting
24
+ * particles into the rigid solver's constraint graph *would* merge islands — a character in a cloak
25
+ * becomes one island with everything the cloak brushes, and merged islands are what §8a's
26
+ * parallelism is made of. So that is still refused. What ships instead is the exchange the two
27
+ * solvers can make **without either learning about the other's structure**: the collision pass
28
+ * already moves a particle out of a body, and a particle moved by `d` in a tick of `h` has had its
29
+ * momentum changed by `m·d/h`; the body receives the opposite of that at the contact point, through
30
+ * the same `applyImpulse` any consumer would call.
31
+ *
32
+ * **What that costs, said plainly.** The exchange is *explicit* — the body responds on the next
33
+ * tick rather than in the same solve — so it is a spring with a one-tick delay in it, and a very
34
+ * heavy sheet landing on a very light body can oscillate. `coupling` is a scale from 0 to 1 and
35
+ * **0 is the default**, so no existing consumer's world moves by a millimetre until it asks. *What
36
+ * would reverse the remaining refusal:* a consumer who needs the sheet and the body to converge
37
+ * together in one solve, at which point the price is the island structure and it is a track rather
38
+ * than a field.
39
+ */
40
+
41
+ export interface ClothOptions {
42
+ /** Metres of stretch per newton. Zero is inextensible; larger is stretchier. */
43
+ stretchCompliance?: number;
44
+ /** The same, for the constraints that resist folding. */
45
+ bendCompliance?: number;
46
+ /** Solver passes per tick. A quality dial: compliance keeps stiffness meaningful across it. */
47
+ iterations?: number;
48
+ /**
49
+ * How close two unconnected particles may come before they push each other apart, in metres.
50
+ *
51
+ * **0 is off and is the default**, because self-collision is the half of cloth whose *tuning*
52
+ * decides whether it ships: a sheet that resists itself at a distance larger than its own
53
+ * spacing cannot lie flat, and one that resists at a distance far smaller than a fold's
54
+ * thickness passes through itself anyway. A consumer sets this from the sheet they built, and
55
+ * the honest starting point is **a little under the grid spacing** — `makeClothGrid` at 5 cm
56
+ * wants about 3 to 4 cm.
57
+ *
58
+ * **Particles joined by a constraint are exempt**, and that is not an optimisation. A stretch
59
+ * link's rest length is the spacing, so a self-collision distance anywhere near it would have
60
+ * every neighbour pair fighting its own link at every step — the sheet would inflate rather than
61
+ * drape, which is the failure that reads as a solver bug. `ClothBody` builds the adjacency once
62
+ * at construction for exactly this.
63
+ */
64
+ selfDistance?: number;
65
+ /** Per-second velocity retained, as a fraction. */
66
+ damping?: number;
67
+ /** How far a particle is held off a surface it meets. */
68
+ thickness?: number;
69
+ /**
70
+ * How much of the momentum a pushed particle gave up is handed back to the body, 0 to 1.
71
+ *
72
+ * **0 by default, which is one-way coupling and is what every consumer had before this existed.**
73
+ * 1 is the whole exchange; anything between is a sheet that pushes less than its mass says it
74
+ * should, which is the dial a consumer reaches for when the delay in the exchange makes a light
75
+ * body jitter.
76
+ */
77
+ coupling?: number;
78
+ /**
79
+ * What one particle weighs, in kilograms. Only the coupling reads it.
80
+ *
81
+ * **Separate from `invMass`, and the separation is deliberate.** That array is a *relative
82
+ * weight* in the position solve — every free particle is 1 and a pinned one is 0 — and the
83
+ * compliance is tuned against it, so redefining it as an inverse mass in kilograms would move
84
+ * every sheet any consumer has ever tuned. The coupling needs kilograms and nothing else does,
85
+ * so it takes them here.
86
+ *
87
+ * The default is fifty grams: a square metre of ordinary cloth is two to three hundred grams,
88
+ * and `makeClothGrid` at a five-centimetre spacing puts four hundred particles in that square
89
+ * metre. A consumer building a tarpaulin or a chainmail cape sets it.
90
+ */
91
+ particleMass?: number;
92
+ gravityX?: number;
93
+ gravityY?: number;
94
+ gravityZ?: number;
95
+ }
96
+
97
+ export class ClothBody {
98
+ /** xyz-packed particle positions. */
99
+ readonly position: Float32Array;
100
+ /** xyz-packed velocities. */
101
+ readonly velocity: Float32Array;
102
+ /** Inverse mass per particle. Zero pins it. */
103
+ readonly invMass: Float32Array;
104
+ readonly count: number;
105
+
106
+ /** Two particle indices per constraint. */
107
+ private readonly link: Int32Array;
108
+ private readonly rest: Float32Array;
109
+ /** Compliance per constraint, so stretch and bend can differ. */
110
+ private readonly compliance: Float32Array;
111
+ private readonly previous: Float32Array;
112
+ private readonly lambda: Float32Array;
113
+ private readonly linkCount: number;
114
+
115
+ readonly iterations: number;
116
+ readonly damping: number;
117
+ readonly thickness: number;
118
+ /** See `ClothOptions.coupling`. 0 is one-way, which is the default. */
119
+ readonly coupling: number;
120
+ /** See `ClothOptions.particleMass`. Kilograms, read only by the coupling. */
121
+ readonly particleMass: number;
122
+ /** See `ClothOptions.selfDistance`. 0 is off, which is the default. */
123
+ readonly selfDistance: number;
124
+
125
+ /**
126
+ * Who is linked to whom, as CSR: `neighbourStart[i]` to `neighbourStart[i + 1]` indexes
127
+ * `neighbour`.
128
+ *
129
+ * **Built once, because the link set never changes** and the self-collision pass asks "are these
130
+ * two linked" once per candidate pair per tick. A `Set` of packed pairs would answer the same
131
+ * question with a hash probe and an allocation at construction the size of the link list; a
132
+ * sorted adjacency answers it with a scan over the eight or so neighbours a grid particle has,
133
+ * which is shorter than the probe.
134
+ *
135
+ * Empty when `selfDistance` is 0, so a sheet that does not collide with itself pays neither the
136
+ * memory nor the build.
137
+ */
138
+ private readonly neighbourStart: Int32Array;
139
+ private readonly neighbour: Int32Array;
140
+
141
+ /**
142
+ * The spatial hash the self-collision pass rebuilds each tick: a cell list over the particles.
143
+ *
144
+ * **A hash rather than a grid, because a sheet has no bounds worth allocating for.** A curtain
145
+ * hanging in a room occupies a sliver of it, and a dense grid over the room's extent would be
146
+ * mostly empty cells rebuilt every tick. The table is sized once from the particle count and
147
+ * reused; `cellHead` and `cellNext` are the standard two arrays, so a rebuild is one pass that
148
+ * writes and allocates nothing.
149
+ */
150
+ private readonly cellHead: Int32Array;
151
+ private readonly cellNext: Int32Array;
152
+ gravityX: number;
153
+ gravityY: number;
154
+ gravityZ: number;
155
+
156
+ constructor(
157
+ positions: Float32Array,
158
+ links: Int32Array,
159
+ bendLinks: Int32Array,
160
+ options: ClothOptions = {},
161
+ ) {
162
+ this.count = positions.length / 3;
163
+ this.position = new Float32Array(positions);
164
+ this.velocity = new Float32Array(positions.length);
165
+ this.invMass = new Float32Array(this.count).fill(1);
166
+ this.previous = new Float32Array(positions.length);
167
+ this.iterations = Math.max(1, options.iterations ?? 8);
168
+ this.damping = options.damping ?? 0.02;
169
+ this.thickness = options.thickness ?? 0.02;
170
+ this.coupling = Math.min(1, Math.max(0, options.coupling ?? 0));
171
+ this.particleMass = Math.max(0, options.particleMass ?? 0.05);
172
+ this.selfDistance = Math.max(0, options.selfDistance ?? 0);
173
+ this.gravityX = options.gravityX ?? 0;
174
+ this.gravityY = options.gravityY ?? -9.81;
175
+ this.gravityZ = options.gravityZ ?? 0;
176
+
177
+ const stretch = options.stretchCompliance ?? 0;
178
+ const bend = options.bendCompliance ?? 1e-4;
179
+ const total = links.length / 2 + bendLinks.length / 2;
180
+ this.linkCount = total;
181
+ this.link = new Int32Array(total * 2);
182
+ this.rest = new Float32Array(total);
183
+ this.compliance = new Float32Array(total);
184
+ this.lambda = new Float32Array(total);
185
+ let at = 0;
186
+ for (const [set, value] of [
187
+ [links, stretch],
188
+ [bendLinks, bend],
189
+ ] as const) {
190
+ for (let i = 0; i + 1 < set.length; i += 2) {
191
+ const a = set[i] ?? 0;
192
+ const b = set[i + 1] ?? 0;
193
+ this.link[at * 2] = a;
194
+ this.link[at * 2 + 1] = b;
195
+ this.rest[at] = distance(this.position, a, b);
196
+ this.compliance[at] = value;
197
+ at++;
198
+ }
199
+ }
200
+
201
+ /*
202
+ * The adjacency and the hash, built only where self-collision is on.
203
+ *
204
+ * Two counting passes rather than an array of arrays: the first counts each particle's links
205
+ * and the second fills them, which is the standard CSR build and allocates two typed arrays
206
+ * instead of one per particle.
207
+ *
208
+ * The table has one bucket a particle. A hash with as many buckets as things in it averages
209
+ * one occupant a bucket, and the cost of a collision is a longer scan rather than a wrong
210
+ * answer — the cell key is part of the comparison below, so two particles that hash together
211
+ * from different cells are separated by the distance test they were going to take anyway.
212
+ */
213
+ if (this.selfDistance > 0) {
214
+ this.neighbourStart = new Int32Array(this.count + 1);
215
+ for (let k = 0; k < total; k++) {
216
+ this.neighbourStart[(this.link[k * 2] ?? 0) + 1]++;
217
+ this.neighbourStart[(this.link[k * 2 + 1] ?? 0) + 1]++;
218
+ }
219
+ for (let i = 0; i < this.count; i++) {
220
+ this.neighbourStart[i + 1] =
221
+ (this.neighbourStart[i + 1] ?? 0) + (this.neighbourStart[i] ?? 0);
222
+ }
223
+ this.neighbour = new Int32Array(total * 2);
224
+ const cursor = new Int32Array(this.neighbourStart.subarray(0, this.count));
225
+ for (let k = 0; k < total; k++) {
226
+ const a = this.link[k * 2] ?? 0;
227
+ const b = this.link[k * 2 + 1] ?? 0;
228
+ this.neighbour[cursor[a]++] = b;
229
+ this.neighbour[cursor[b]++] = a;
230
+ }
231
+ this.cellHead = new Int32Array(Math.max(1, this.count)).fill(-1);
232
+ this.cellNext = new Int32Array(this.count);
233
+ } else {
234
+ this.neighbourStart = EMPTY_INDEX;
235
+ this.neighbour = EMPTY_INDEX;
236
+ this.cellHead = EMPTY_INDEX;
237
+ this.cellNext = EMPTY_INDEX;
238
+ }
239
+ }
240
+
241
+ /** Pin a particle where it is, or release it. */
242
+ pin(index: number, pinned = true): void {
243
+ this.invMass[index] = pinned ? 0 : 1;
244
+ }
245
+
246
+ /**
247
+ * Advance one fixed tick.
248
+ *
249
+ * Integrate, project every constraint `iterations` times, then derive velocity from how far each
250
+ * particle actually moved — which is what makes it position-based: a constraint that moves a
251
+ * particle also, by construction, changes its velocity, so nothing has to reconcile the two.
252
+ */
253
+ step(world: PhysicsWorld | null, dt: number): void {
254
+ const n = this.count;
255
+ const retain = 1 / (1 + this.damping * dt);
256
+ for (let i = 0; i < n; i++) {
257
+ const im = this.invMass[i] ?? 0;
258
+ this.previous[i * 3] = this.position[i * 3] ?? 0;
259
+ this.previous[i * 3 + 1] = this.position[i * 3 + 1] ?? 0;
260
+ this.previous[i * 3 + 2] = this.position[i * 3 + 2] ?? 0;
261
+ if (im === 0) continue;
262
+ const vx = ((this.velocity[i * 3] ?? 0) + this.gravityX * dt) * retain;
263
+ const vy = ((this.velocity[i * 3 + 1] ?? 0) + this.gravityY * dt) * retain;
264
+ const vz = ((this.velocity[i * 3 + 2] ?? 0) + this.gravityZ * dt) * retain;
265
+ this.position[i * 3] = (this.position[i * 3] ?? 0) + vx * dt;
266
+ this.position[i * 3 + 1] = (this.position[i * 3 + 1] ?? 0) + vy * dt;
267
+ this.position[i * 3 + 2] = (this.position[i * 3 + 2] ?? 0) + vz * dt;
268
+ }
269
+
270
+ this.lambda.fill(0);
271
+ const invH2 = 1 / (dt * dt);
272
+ for (let pass = 0; pass < this.iterations; pass++) {
273
+ for (let k = 0; k < this.linkCount; k++) this.project(k, invH2);
274
+ }
275
+ /*
276
+ * Self-collision after the constraints and before the bodies, which is the order that makes
277
+ * both of them mean what they say. A sheet resolved against itself *first* would then be
278
+ * stretched back into its own fold by the distance constraints; one resolved against the world
279
+ * first would be pushed back into a body by its own self-collision. The world is what the
280
+ * sheet may not enter under any circumstances, so it goes last.
281
+ */
282
+ if (this.selfDistance > 0) this.collideSelf();
283
+ if (world) this.collideParticles(world, dt);
284
+
285
+ const invDt = 1 / dt;
286
+ for (let i = 0; i < n; i++) {
287
+ if ((this.invMass[i] ?? 0) === 0) {
288
+ this.velocity[i * 3] = 0;
289
+ this.velocity[i * 3 + 1] = 0;
290
+ this.velocity[i * 3 + 2] = 0;
291
+ continue;
292
+ }
293
+ this.velocity[i * 3] = ((this.position[i * 3] ?? 0) - (this.previous[i * 3] ?? 0)) * invDt;
294
+ this.velocity[i * 3 + 1] =
295
+ ((this.position[i * 3 + 1] ?? 0) - (this.previous[i * 3 + 1] ?? 0)) * invDt;
296
+ this.velocity[i * 3 + 2] =
297
+ ((this.position[i * 3 + 2] ?? 0) - (this.previous[i * 3 + 2] ?? 0)) * invDt;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * One distance constraint, with its compliance.
303
+ *
304
+ * `α = compliance / h²` is what makes the stiffness independent of the step and of the iteration
305
+ * count. At zero compliance this reduces exactly to an ordinary position-based projection, which
306
+ * is why an inextensible sheet costs nothing extra.
307
+ */
308
+ private project(k: number, invH2: number): void {
309
+ const a = this.link[k * 2] ?? 0;
310
+ const b = this.link[k * 2 + 1] ?? 0;
311
+ const wa = this.invMass[a] ?? 0;
312
+ const wb = this.invMass[b] ?? 0;
313
+ const w = wa + wb;
314
+ if (w === 0) return;
315
+ const dx = (this.position[b * 3] ?? 0) - (this.position[a * 3] ?? 0);
316
+ const dy = (this.position[b * 3 + 1] ?? 0) - (this.position[a * 3 + 1] ?? 0);
317
+ const dz = (this.position[b * 3 + 2] ?? 0) - (this.position[a * 3 + 2] ?? 0);
318
+ const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
319
+ if (length < 1e-9) return;
320
+ const c = length - (this.rest[k] ?? 0);
321
+ const alpha = (this.compliance[k] ?? 0) * invH2;
322
+ const old = this.lambda[k] ?? 0;
323
+ const delta = (-c - alpha * old) / (w + alpha);
324
+ this.lambda[k] = old + delta;
325
+ const nx = (dx / length) * delta;
326
+ const ny = (dy / length) * delta;
327
+ const nz = (dz / length) * delta;
328
+ this.position[a * 3] = (this.position[a * 3] ?? 0) - nx * wa;
329
+ this.position[a * 3 + 1] = (this.position[a * 3 + 1] ?? 0) - ny * wa;
330
+ this.position[a * 3 + 2] = (this.position[a * 3 + 2] ?? 0) - nz * wa;
331
+ this.position[b * 3] = (this.position[b * 3] ?? 0) + nx * wb;
332
+ this.position[b * 3 + 1] = (this.position[b * 3 + 1] ?? 0) + ny * wb;
333
+ this.position[b * 3 + 2] = (this.position[b * 3 + 2] ?? 0) + nz * wb;
334
+ }
335
+
336
+ /**
337
+ * Keep the sheet out of itself: a spatial hash over the particles, and a symmetric push apart.
338
+ *
339
+ * **The hash is rebuilt every tick and that is the cheap half.** One pass over the particles
340
+ * writing two integers each, against a structure that tried to be incremental and would have to
341
+ * know which particles moved — which in a cloth solver is all of them.
342
+ *
343
+ * The cell size is the self-collision distance, so a particle's neighbours are in its own cell
344
+ * and the twenty-six around it. **The 3x3x3 scan is what makes the answer complete**: a pair at
345
+ * the distance exactly cannot be in cells more than one apart, and a scan of the own cell alone
346
+ * would miss every pair straddling a boundary — which is most of them, since a folded sheet lies
347
+ * along one.
348
+ *
349
+ * **Each pair is resolved once, by index order.** Visiting both halves would apply the
350
+ * correction twice and turn a resting fold into a buzzing one; the `other > i` test is what
351
+ * makes the pass a sweep over pairs rather than over particles.
352
+ *
353
+ * A projection rather than a constraint with compliance, unlike the distance links: a contact is
354
+ * an inequality — it does nothing until it is violated — and XPBD's accumulated multiplier is
355
+ * for a constraint that holds all tick. *What that gives up* is that the stiffness of a fold is
356
+ * the iteration count's again, for this one term. *What would make it wrong* is a consumer
357
+ * wanting a fold to have a measurable resistance, at which point it needs its own multiplier
358
+ * array and a sign clamp on it.
359
+ */
360
+ private collideSelf(): void {
361
+ const n = this.count;
362
+ const radius = this.selfDistance;
363
+ const inverseCell = 1 / radius;
364
+ this.cellHead.fill(-1);
365
+ const buckets = this.cellHead.length;
366
+ for (let i = 0; i < n; i++) {
367
+ const key = cellKey(
368
+ Math.floor((this.position[i * 3] ?? 0) * inverseCell),
369
+ Math.floor((this.position[i * 3 + 1] ?? 0) * inverseCell),
370
+ Math.floor((this.position[i * 3 + 2] ?? 0) * inverseCell),
371
+ buckets,
372
+ );
373
+ this.cellNext[i] = this.cellHead[key] ?? -1;
374
+ this.cellHead[key] = i;
375
+ }
376
+
377
+ for (let i = 0; i < n; i++) {
378
+ const wi = this.invMass[i] ?? 0;
379
+ const px = this.position[i * 3] ?? 0;
380
+ const py = this.position[i * 3 + 1] ?? 0;
381
+ const pz = this.position[i * 3 + 2] ?? 0;
382
+ const cx = Math.floor(px * inverseCell);
383
+ const cy = Math.floor(py * inverseCell);
384
+ const cz = Math.floor(pz * inverseCell);
385
+ for (let dx = -1; dx <= 1; dx++) {
386
+ for (let dy = -1; dy <= 1; dy++) {
387
+ for (let dz = -1; dz <= 1; dz++) {
388
+ let other = this.cellHead[cellKey(cx + dx, cy + dy, cz + dz, buckets)] ?? -1;
389
+ while (other >= 0) {
390
+ /* Index order, so a pair is met once however many cells they share a bucket with. */
391
+ if (other > i && !this.linked(i, other)) {
392
+ this.separate(i, other, wi, px, py, pz, radius);
393
+ }
394
+ other = this.cellNext[other] ?? -1;
395
+ }
396
+ }
397
+ }
398
+ }
399
+ }
400
+ }
401
+
402
+ /** Whether a link joins these two. A scan over the eight or so a grid particle has. */
403
+ private linked(a: number, b: number): boolean {
404
+ const from = this.neighbourStart[a] ?? 0;
405
+ const to = this.neighbourStart[a + 1] ?? from;
406
+ for (let at = from; at < to; at++) {
407
+ if (this.neighbour[at] === b) return true;
408
+ }
409
+ return false;
410
+ }
411
+
412
+ /** Push two particles apart to exactly `radius`, split by their inverse masses. */
413
+ private separate(
414
+ a: number,
415
+ b: number,
416
+ wa: number,
417
+ ax: number,
418
+ ay: number,
419
+ az: number,
420
+ radius: number,
421
+ ): void {
422
+ const wb = this.invMass[b] ?? 0;
423
+ const w = wa + wb;
424
+ if (w === 0) return;
425
+ const dx = (this.position[b * 3] ?? 0) - ax;
426
+ const dy = (this.position[b * 3 + 1] ?? 0) - ay;
427
+ const dz = (this.position[b * 3 + 2] ?? 0) - az;
428
+ const squared = dx * dx + dy * dy + dz * dz;
429
+ if (squared >= radius * radius) return;
430
+ const length = Math.sqrt(squared);
431
+ /*
432
+ * Two particles at exactly the same point have no direction to separate along, and picking one
433
+ * arbitrarily would pick the same one for every such pair — so a whole collapsed fold would
434
+ * open along one axis. Left alone instead: the next tick's integration moves them apart, and a
435
+ * pair genuinely coincident is a sheet that has already failed.
436
+ */
437
+ if (length < 1e-9) return;
438
+ const push = (radius - length) / length / w;
439
+ const mx = dx * push;
440
+ const my = dy * push;
441
+ const mz = dz * push;
442
+ this.position[a * 3] = ax - mx * wa;
443
+ this.position[a * 3 + 1] = ay - my * wa;
444
+ this.position[a * 3 + 2] = az - mz * wa;
445
+ this.position[b * 3] = (this.position[b * 3] ?? 0) + mx * wb;
446
+ this.position[b * 3 + 1] = (this.position[b * 3 + 1] ?? 0) + my * wb;
447
+ this.position[b * 3 + 2] = (this.position[b * 3 + 2] ?? 0) + mz * wb;
448
+ }
449
+
450
+ /**
451
+ * Push every particle out of anything it has entered, and hand the body back what it took.
452
+ *
453
+ * **The impulse is derived from the correction rather than measured**, which is what makes the
454
+ * exchange cost one multiply: the constraint solve has already decided where the particle would
455
+ * be, the push says where it must be instead, and the difference over the tick is the velocity
456
+ * the collision removed from it. `m·Δ/h` is that momentum, and the body gets its negative at the
457
+ * point of contact — a push straight down on a plank's end tips the plank, because
458
+ * `applyImpulse` takes the point.
459
+ *
460
+ * A static body is skipped for the impulse rather than for the push: scenery does not move, and
461
+ * `applyImpulse` would return without doing anything anyway.
462
+ */
463
+ private collideParticles(world: PhysicsWorld, dt: number): void {
464
+ const bodies = world.bodies;
465
+ const exchange = this.coupling > 0 ? (this.particleMass * this.coupling) / dt : 0;
466
+ for (let i = 0; i < this.count; i++) {
467
+ if ((this.invMass[i] ?? 0) === 0) continue;
468
+ const px = this.position[i * 3] ?? 0;
469
+ const py = this.position[i * 3 + 1] ?? 0;
470
+ const pz = this.position[i * 3 + 2] ?? 0;
471
+ for (let b = 0; b < bodies.count; b++) {
472
+ const shape = bodies.shape[b];
473
+ if (!shape) continue;
474
+ // A cheap reject before anything expensive: the shape's own bounding sphere.
475
+ const bx = bodies.posX[b] ?? 0;
476
+ const by = bodies.posY[b] ?? 0;
477
+ const bz = bodies.posZ[b] ?? 0;
478
+ const dx = px - bx;
479
+ const dy = py - by;
480
+ const dz = pz - bz;
481
+ const reach = shape.boundRadius + this.thickness;
482
+ if (dx * dx + dy * dy + dz * dz > reach * reach) continue;
483
+
484
+ POSE.x = bx;
485
+ POSE.y = by;
486
+ POSE.z = bz;
487
+ POSE.qx = bodies.rotX[b] ?? 0;
488
+ POSE.qy = bodies.rotY[b] ?? 0;
489
+ POSE.qz = bodies.rotZ[b] ?? 0;
490
+ POSE.qw = bodies.rotW[b] ?? 1;
491
+ if (!pushOut(shape, POSE, px, py, pz, this.thickness, OUT)) continue;
492
+ this.position[i * 3] = OUT[0];
493
+ this.position[i * 3 + 1] = OUT[1];
494
+ this.position[i * 3 + 2] = OUT[2];
495
+ /*
496
+ * The other half of the exchange. Negated, because the push moved the particle *away*
497
+ * from the body and the body is what pushed it — Newton's third law with the tick length
498
+ * folded in. At the contact point rather than at the centre, so a sheet landing on one end
499
+ * of a plank tips it.
500
+ */
501
+ if (exchange > 0 && bodies.type[b] !== BODY_STATIC) {
502
+ world.applyImpulse(
503
+ b,
504
+ -(OUT[0] - px) * exchange,
505
+ -(OUT[1] - py) * exchange,
506
+ -(OUT[2] - pz) * exchange,
507
+ px,
508
+ py,
509
+ pz,
510
+ );
511
+ }
512
+ // A static body is scenery, so one push settles it; a moving one is resolved next tick.
513
+ if (bodies.type[b] === BODY_STATIC) break;
514
+ }
515
+ }
516
+ }
517
+ }
518
+
519
+ /** What a sheet with no self-collision carries instead of four arrays it will never read. */
520
+ const EMPTY_INDEX = new Int32Array(0);
521
+
522
+ const POSE: ShapePose = { x: 0, y: 0, z: 0, qx: 0, qy: 0, qz: 0, qw: 1 };
523
+ const OUT = new Float64Array(3);
524
+ const LOCAL = new Float64Array(3);
525
+ const CYLINDER = new Float64Array(11);
526
+ const SURFACE = new Float64Array(7);
527
+
528
+ /**
529
+ * Move a point to the outside of a shape, if it is inside. Returns whether it moved.
530
+ *
531
+ * Works in the shape's own frame so a rotated body costs one rotation rather than a transformed
532
+ * copy of its geometry. A faceless shape is a sphere or a capsule and is handled by its radius; a
533
+ * polytope is pushed out along its least-penetrating face.
534
+ */
535
+ function pushOut(
536
+ shape: ConvexShape,
537
+ pose: ShapePose,
538
+ px: number,
539
+ py: number,
540
+ pz: number,
541
+ thickness: number,
542
+ out: Float64Array,
543
+ ): boolean {
544
+ unrotate(pose, px - pose.x, py - pose.y, pz - pose.z, LOCAL);
545
+ const lx = LOCAL[0];
546
+ const ly = LOCAL[1];
547
+ const lz = LOCAL[2];
548
+ const faces = faceCount(shape);
549
+
550
+ if (isCylinder(shape)) {
551
+ /*
552
+ * A cylinder in its own coordinates, which is where this function already is. `closestOnCylinder`
553
+ * answers all four regions — beside the side, past a cap, past a rim, inside — so a particle
554
+ * that has entered near the rim comes out at the rim rather than being pushed through the solid
555
+ * along whichever of the two surfaces the code happened to test first.
556
+ */
557
+ readCylinder(shape, LOCAL_POSE, CYLINDER);
558
+ closestOnCylinder(CYLINDER, lx, ly, lz, SURFACE);
559
+ const want = shape.radius + thickness;
560
+ if (SURFACE[6] >= want) return false;
561
+ rotate(
562
+ pose,
563
+ SURFACE[0] + SURFACE[3] * want,
564
+ SURFACE[1] + SURFACE[4] * want,
565
+ SURFACE[2] + SURFACE[5] * want,
566
+ out,
567
+ );
568
+ out[0] += pose.x;
569
+ out[1] += pose.y;
570
+ out[2] += pose.z;
571
+ return true;
572
+ }
573
+
574
+ if (faces === 0) {
575
+ // The segment the shape is built on, then its radius.
576
+ const count = shape.vertices.length / 3;
577
+ const ax = shape.vertices[0] ?? 0;
578
+ const ay = shape.vertices[1] ?? 0;
579
+ const az = shape.vertices[2] ?? 0;
580
+ const bx = count > 1 ? (shape.vertices[3] ?? 0) : ax;
581
+ const by = count > 1 ? (shape.vertices[4] ?? 0) : ay;
582
+ const bz = count > 1 ? (shape.vertices[5] ?? 0) : az;
583
+ const ex = bx - ax;
584
+ const ey = by - ay;
585
+ const ez = bz - az;
586
+ const ee = ex * ex + ey * ey + ez * ez;
587
+ let t = 0;
588
+ if (ee > 1e-12) {
589
+ t = ((lx - ax) * ex + (ly - ay) * ey + (lz - az) * ez) / ee;
590
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
591
+ }
592
+ const cx = ax + ex * t;
593
+ const cy = ay + ey * t;
594
+ const cz = az + ez * t;
595
+ let nx = lx - cx;
596
+ let ny = ly - cy;
597
+ let nz = lz - cz;
598
+ const d = Math.sqrt(nx * nx + ny * ny + nz * nz);
599
+ const want = shape.radius + thickness;
600
+ if (d >= want) return false;
601
+ if (d < 1e-9) {
602
+ nx = 0;
603
+ ny = 1;
604
+ nz = 0;
605
+ } else {
606
+ nx /= d;
607
+ ny /= d;
608
+ nz /= d;
609
+ }
610
+ rotate(pose, cx + nx * want, cy + ny * want, cz + nz * want, out);
611
+ out[0] += pose.x;
612
+ out[1] += pose.y;
613
+ out[2] += pose.z;
614
+ return true;
615
+ }
616
+
617
+ let bestFace = 0;
618
+ let best = -Infinity;
619
+ for (let f = 0; f < faces; f++) {
620
+ const d =
621
+ lx * (shape.facePlanes[f * 4] ?? 0) +
622
+ ly * (shape.facePlanes[f * 4 + 1] ?? 0) +
623
+ lz * (shape.facePlanes[f * 4 + 2] ?? 0) -
624
+ (shape.facePlanes[f * 4 + 3] ?? 0) -
625
+ shape.radius;
626
+ if (d > best) {
627
+ best = d;
628
+ bestFace = f;
629
+ }
630
+ }
631
+ if (best >= thickness) return false;
632
+ const move = thickness - best;
633
+ rotate(
634
+ pose,
635
+ lx + (shape.facePlanes[bestFace * 4] ?? 0) * move,
636
+ ly + (shape.facePlanes[bestFace * 4 + 1] ?? 0) * move,
637
+ lz + (shape.facePlanes[bestFace * 4 + 2] ?? 0) * move,
638
+ out,
639
+ );
640
+ out[0] += pose.x;
641
+ out[1] += pose.y;
642
+ out[2] += pose.z;
643
+ return true;
644
+ }
645
+
646
+ function rotate(pose: ShapePose, x: number, y: number, z: number, out: Float64Array): void {
647
+ const tx = 2 * (pose.qy * z - pose.qz * y);
648
+ const ty = 2 * (pose.qz * x - pose.qx * z);
649
+ const tz = 2 * (pose.qx * y - pose.qy * x);
650
+ out[0] = x + pose.qw * tx + (pose.qy * tz - pose.qz * ty);
651
+ out[1] = y + pose.qw * ty + (pose.qz * tx - pose.qx * tz);
652
+ out[2] = z + pose.qw * tz + (pose.qx * ty - pose.qy * tx);
653
+ }
654
+
655
+ function unrotate(pose: ShapePose, x: number, y: number, z: number, out: Float64Array): void {
656
+ const qx = -pose.qx;
657
+ const qy = -pose.qy;
658
+ const qz = -pose.qz;
659
+ const qw = pose.qw;
660
+ const tx = 2 * (qy * z - qz * y);
661
+ const ty = 2 * (qz * x - qx * z);
662
+ const tz = 2 * (qx * y - qy * x);
663
+ out[0] = x + qw * tx + (qy * tz - qz * ty);
664
+ out[1] = y + qw * ty + (qz * tx - qx * tz);
665
+ out[2] = z + qw * tz + (qx * ty - qy * tx);
666
+ }
667
+
668
+ /**
669
+ * Three cell coordinates into one bucket, by the standard three-prime mix.
670
+ *
671
+ * The primes are the ones every published spatial hash for particles uses, and the reason to take
672
+ * them rather than invent three is that a poor choice shows up as a *distribution* rather than as
673
+ * an error: buckets that fill unevenly, a scan that lengthens, and a sheet that costs more the
674
+ * further it is from the origin. `>>> 0` before the modulo, because a negative cell coordinate is
675
+ * ordinary — a sheet hanging below the origin has them — and a negative bucket index would read
676
+ * `undefined` out of the table and start every chain empty.
677
+ */
678
+ function cellKey(x: number, y: number, z: number, buckets: number): number {
679
+ return (((x * 73856093) ^ (y * 19349663) ^ (z * 83492791)) >>> 0) % buckets;
680
+ }
681
+
682
+ function distance(p: Float32Array, a: number, b: number): number {
683
+ const dx = (p[b * 3] ?? 0) - (p[a * 3] ?? 0);
684
+ const dy = (p[b * 3 + 1] ?? 0) - (p[a * 3 + 1] ?? 0);
685
+ const dz = (p[b * 3 + 2] ?? 0) - (p[a * 3 + 2] ?? 0);
686
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
687
+ }
688
+
689
+ /**
690
+ * A rectangular sheet: positions, its stretch links, and the bend links across them.
691
+ *
692
+ * Bend links skip a particle rather than joining diagonals, because a diagonal resists *shear* and a
693
+ * skip resists *bowing*, and a sheet that cannot shear drapes like cardboard.
694
+ *
695
+ * **A skip-one distance constraint resists curvature and not orientation**, which is worth knowing
696
+ * before tuning it: three collinear particles satisfy it at any angle, so a flat sheet and a sheet
697
+ * hanging straight down are equally happy. Measured — a cantilever pinned along one edge settles at
698
+ * exactly vertical whether the bend compliance is 1e-8 or 1e6. What it *does* buy is 12.5
699
+ * millimetres less bow on a strip pinned at both ends with slack.
700
+ *
701
+ * **What would make it wrong** is a consumer wanting a cloak to stand out from a shoulder, and the
702
+ * answer then is a dihedral-angle constraint over the two triangles either side of an edge, which
703
+ * knows a rest angle and therefore knows the difference between flat and folded.
704
+ */
705
+ export function makeClothGrid(
706
+ columns: number,
707
+ rows: number,
708
+ spacing: number,
709
+ ): { positions: Float32Array; links: Int32Array; bendLinks: Int32Array } {
710
+ const positions = new Float32Array(columns * rows * 3);
711
+ const links: number[] = [];
712
+ const bend: number[] = [];
713
+ const at = (c: number, r: number): number => r * columns + c;
714
+ for (let r = 0; r < rows; r++) {
715
+ for (let c = 0; c < columns; c++) {
716
+ const i = at(c, r);
717
+ positions[i * 3] = c * spacing;
718
+ positions[i * 3 + 1] = 0;
719
+ positions[i * 3 + 2] = r * spacing;
720
+ if (c + 1 < columns) links.push(i, at(c + 1, r));
721
+ if (r + 1 < rows) links.push(i, at(c, r + 1));
722
+ // Shear, which a grid needs or it folds flat along a diagonal.
723
+ if (c + 1 < columns && r + 1 < rows) links.push(i, at(c + 1, r + 1));
724
+ if (c + 1 < columns && r + 1 < rows) links.push(at(c + 1, r), at(c, r + 1));
725
+ if (c + 2 < columns) bend.push(i, at(c + 2, r));
726
+ if (r + 2 < rows) bend.push(i, at(c, r + 2));
727
+ }
728
+ }
729
+ return {
730
+ positions,
731
+ links: new Int32Array(links),
732
+ bendLinks: new Int32Array(bend),
733
+ };
734
+ }