@driftengine/physics 3.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +503 -0
  4. package/dist/bodies.d.ts +121 -0
  5. package/dist/bodies.js +298 -0
  6. package/dist/buoyancy.d.ts +46 -0
  7. package/dist/buoyancy.js +45 -0
  8. package/dist/cloth.d.ts +226 -0
  9. package/dist/cloth.js +587 -0
  10. package/dist/collide/aabb.d.ts +10 -0
  11. package/dist/collide/aabb.js +11 -0
  12. package/dist/collide/body.d.ts +202 -0
  13. package/dist/collide/body.js +142 -0
  14. package/dist/collide/eject.d.ts +39 -0
  15. package/dist/collide/eject.js +142 -0
  16. package/dist/collide/index.d.ts +18 -0
  17. package/dist/collide/index.js +5 -0
  18. package/dist/collide/scratch.d.ts +8 -0
  19. package/dist/collide/scratch.js +9 -0
  20. package/dist/collide/segment.d.ts +12 -0
  21. package/dist/collide/segment.js +109 -0
  22. package/dist/collide/sweep.d.ts +3 -0
  23. package/dist/collide/sweep.js +1036 -0
  24. package/dist/colliderSet.d.ts +239 -0
  25. package/dist/colliderSet.js +546 -0
  26. package/dist/compoundContact.d.ts +70 -0
  27. package/dist/compoundContact.js +221 -0
  28. package/dist/controller.d.ts +351 -0
  29. package/dist/controller.js +641 -0
  30. package/dist/cylinder.d.ts +78 -0
  31. package/dist/cylinder.js +294 -0
  32. package/dist/cylinderContact.d.ts +7 -0
  33. package/dist/cylinderContact.js +704 -0
  34. package/dist/decompose.d.ts +105 -0
  35. package/dist/decompose.js +979 -0
  36. package/dist/events.d.ts +43 -0
  37. package/dist/events.js +95 -0
  38. package/dist/executor.d.ts +147 -0
  39. package/dist/executor.js +173 -0
  40. package/dist/faces.d.ts +26 -0
  41. package/dist/faces.js +34 -0
  42. package/dist/fingerprint.d.ts +61 -0
  43. package/dist/fingerprint.js +139 -0
  44. package/dist/groundMotor.d.ts +34 -0
  45. package/dist/groundMotor.js +41 -0
  46. package/dist/heightfieldShape.d.ts +128 -0
  47. package/dist/heightfieldShape.js +328 -0
  48. package/dist/index.d.ts +73 -0
  49. package/dist/index.js +47 -0
  50. package/dist/island.d.ts +55 -0
  51. package/dist/island.js +227 -0
  52. package/dist/islandSolve.d.ts +70 -0
  53. package/dist/islandSolve.js +171 -0
  54. package/dist/islandWorker.d.ts +18 -0
  55. package/dist/islandWorker.js +125 -0
  56. package/dist/joints.d.ts +180 -0
  57. package/dist/joints.js +1094 -0
  58. package/dist/manifold.d.ts +89 -0
  59. package/dist/manifold.js +608 -0
  60. package/dist/mass.d.ts +53 -0
  61. package/dist/mass.js +353 -0
  62. package/dist/meshContact.d.ts +57 -0
  63. package/dist/meshContact.js +393 -0
  64. package/dist/meshShape.d.ts +76 -0
  65. package/dist/meshShape.js +175 -0
  66. package/dist/pairs.d.ts +62 -0
  67. package/dist/pairs.js +154 -0
  68. package/dist/query.d.ts +71 -0
  69. package/dist/query.js +683 -0
  70. package/dist/ragdoll.d.ts +135 -0
  71. package/dist/ragdoll.js +489 -0
  72. package/dist/shape.d.ts +177 -0
  73. package/dist/shape.js +533 -0
  74. package/dist/solver.d.ts +136 -0
  75. package/dist/solver.js +601 -0
  76. package/dist/stage.d.ts +167 -0
  77. package/dist/stage.js +350 -0
  78. package/dist/stallEscape.d.ts +141 -0
  79. package/dist/stallEscape.js +254 -0
  80. package/dist/sweptStep.d.ts +173 -0
  81. package/dist/sweptStep.js +472 -0
  82. package/dist/tree.d.ts +61 -0
  83. package/dist/tree.js +348 -0
  84. package/dist/vehicle.d.ts +154 -0
  85. package/dist/vehicle.js +343 -0
  86. package/dist/workerPool.d.ts +107 -0
  87. package/dist/workerPool.js +239 -0
  88. package/dist/workers.d.ts +37 -0
  89. package/dist/workers.js +35 -0
  90. package/dist/world.d.ts +298 -0
  91. package/dist/world.js +712 -0
  92. package/package.json +55 -0
  93. package/src/bodies.ts +369 -0
  94. package/src/buoyancy.ts +78 -0
  95. package/src/cloth.ts +734 -0
  96. package/src/collide/aabb.ts +28 -0
  97. package/src/collide/body.ts +336 -0
  98. package/src/collide/eject.ts +156 -0
  99. package/src/collide/index.ts +26 -0
  100. package/src/collide/scratch.ts +10 -0
  101. package/src/collide/segment.ts +127 -0
  102. package/src/collide/sweep.ts +1132 -0
  103. package/src/colliderSet.ts +637 -0
  104. package/src/compoundContact.ts +284 -0
  105. package/src/controller.ts +817 -0
  106. package/src/cylinder.ts +340 -0
  107. package/src/cylinderContact.ts +825 -0
  108. package/src/decompose.ts +1198 -0
  109. package/src/events.ts +108 -0
  110. package/src/executor.ts +224 -0
  111. package/src/faces.ts +37 -0
  112. package/src/fingerprint.ts +146 -0
  113. package/src/groundMotor.ts +53 -0
  114. package/src/heightfieldShape.ts +395 -0
  115. package/src/index.ts +164 -0
  116. package/src/island.ts +219 -0
  117. package/src/islandSolve.ts +218 -0
  118. package/src/islandWorker.ts +141 -0
  119. package/src/joints.ts +1367 -0
  120. package/src/manifold.ts +798 -0
  121. package/src/mass.ts +424 -0
  122. package/src/meshContact.ts +443 -0
  123. package/src/meshShape.ts +261 -0
  124. package/src/pairs.ts +169 -0
  125. package/src/query.ts +844 -0
  126. package/src/ragdoll.ts +662 -0
  127. package/src/shape.ts +656 -0
  128. package/src/solver.ts +873 -0
  129. package/src/stage.ts +406 -0
  130. package/src/stallEscape.ts +362 -0
  131. package/src/sweptStep.ts +609 -0
  132. package/src/tree.ts +366 -0
  133. package/src/vehicle.ts +489 -0
  134. package/src/workerPool.ts +283 -0
  135. package/src/workers.ts +38 -0
  136. package/src/world.ts +961 -0
package/dist/cloth.js ADDED
@@ -0,0 +1,587 @@
1
+ import { BODY_STATIC } from './bodies.js';
2
+ import { LOCAL_POSE, closestOnCylinder, isCylinder, readCylinder } from './cylinder.js';
3
+ import { faceCount } from './faces.js';
4
+ export class ClothBody {
5
+ /** xyz-packed particle positions. */
6
+ position;
7
+ /** xyz-packed velocities. */
8
+ velocity;
9
+ /** Inverse mass per particle. Zero pins it. */
10
+ invMass;
11
+ count;
12
+ /** Two particle indices per constraint. */
13
+ link;
14
+ rest;
15
+ /** Compliance per constraint, so stretch and bend can differ. */
16
+ compliance;
17
+ previous;
18
+ lambda;
19
+ linkCount;
20
+ iterations;
21
+ damping;
22
+ thickness;
23
+ /** See `ClothOptions.coupling`. 0 is one-way, which is the default. */
24
+ coupling;
25
+ /** See `ClothOptions.particleMass`. Kilograms, read only by the coupling. */
26
+ particleMass;
27
+ /** See `ClothOptions.selfDistance`. 0 is off, which is the default. */
28
+ selfDistance;
29
+ /**
30
+ * Who is linked to whom, as CSR: `neighbourStart[i]` to `neighbourStart[i + 1]` indexes
31
+ * `neighbour`.
32
+ *
33
+ * **Built once, because the link set never changes** and the self-collision pass asks "are these
34
+ * two linked" once per candidate pair per tick. A `Set` of packed pairs would answer the same
35
+ * question with a hash probe and an allocation at construction the size of the link list; a
36
+ * sorted adjacency answers it with a scan over the eight or so neighbours a grid particle has,
37
+ * which is shorter than the probe.
38
+ *
39
+ * Empty when `selfDistance` is 0, so a sheet that does not collide with itself pays neither the
40
+ * memory nor the build.
41
+ */
42
+ neighbourStart;
43
+ neighbour;
44
+ /**
45
+ * The spatial hash the self-collision pass rebuilds each tick: a cell list over the particles.
46
+ *
47
+ * **A hash rather than a grid, because a sheet has no bounds worth allocating for.** A curtain
48
+ * hanging in a room occupies a sliver of it, and a dense grid over the room's extent would be
49
+ * mostly empty cells rebuilt every tick. The table is sized once from the particle count and
50
+ * reused; `cellHead` and `cellNext` are the standard two arrays, so a rebuild is one pass that
51
+ * writes and allocates nothing.
52
+ */
53
+ cellHead;
54
+ cellNext;
55
+ gravityX;
56
+ gravityY;
57
+ gravityZ;
58
+ constructor(positions, links, bendLinks, options = {}) {
59
+ this.count = positions.length / 3;
60
+ this.position = new Float32Array(positions);
61
+ this.velocity = new Float32Array(positions.length);
62
+ this.invMass = new Float32Array(this.count).fill(1);
63
+ this.previous = new Float32Array(positions.length);
64
+ this.iterations = Math.max(1, options.iterations ?? 8);
65
+ this.damping = options.damping ?? 0.02;
66
+ this.thickness = options.thickness ?? 0.02;
67
+ this.coupling = Math.min(1, Math.max(0, options.coupling ?? 0));
68
+ this.particleMass = Math.max(0, options.particleMass ?? 0.05);
69
+ this.selfDistance = Math.max(0, options.selfDistance ?? 0);
70
+ this.gravityX = options.gravityX ?? 0;
71
+ this.gravityY = options.gravityY ?? -9.81;
72
+ this.gravityZ = options.gravityZ ?? 0;
73
+ const stretch = options.stretchCompliance ?? 0;
74
+ const bend = options.bendCompliance ?? 1e-4;
75
+ const total = links.length / 2 + bendLinks.length / 2;
76
+ this.linkCount = total;
77
+ this.link = new Int32Array(total * 2);
78
+ this.rest = new Float32Array(total);
79
+ this.compliance = new Float32Array(total);
80
+ this.lambda = new Float32Array(total);
81
+ let at = 0;
82
+ for (const [set, value] of [
83
+ [links, stretch],
84
+ [bendLinks, bend],
85
+ ]) {
86
+ for (let i = 0; i + 1 < set.length; i += 2) {
87
+ const a = set[i] ?? 0;
88
+ const b = set[i + 1] ?? 0;
89
+ this.link[at * 2] = a;
90
+ this.link[at * 2 + 1] = b;
91
+ this.rest[at] = distance(this.position, a, b);
92
+ this.compliance[at] = value;
93
+ at++;
94
+ }
95
+ }
96
+ /*
97
+ * The adjacency and the hash, built only where self-collision is on.
98
+ *
99
+ * Two counting passes rather than an array of arrays: the first counts each particle's links
100
+ * and the second fills them, which is the standard CSR build and allocates two typed arrays
101
+ * instead of one per particle.
102
+ *
103
+ * The table has one bucket a particle. A hash with as many buckets as things in it averages
104
+ * one occupant a bucket, and the cost of a collision is a longer scan rather than a wrong
105
+ * answer — the cell key is part of the comparison below, so two particles that hash together
106
+ * from different cells are separated by the distance test they were going to take anyway.
107
+ */
108
+ if (this.selfDistance > 0) {
109
+ this.neighbourStart = new Int32Array(this.count + 1);
110
+ for (let k = 0; k < total; k++) {
111
+ this.neighbourStart[(this.link[k * 2] ?? 0) + 1]++;
112
+ this.neighbourStart[(this.link[k * 2 + 1] ?? 0) + 1]++;
113
+ }
114
+ for (let i = 0; i < this.count; i++) {
115
+ this.neighbourStart[i + 1] =
116
+ (this.neighbourStart[i + 1] ?? 0) + (this.neighbourStart[i] ?? 0);
117
+ }
118
+ this.neighbour = new Int32Array(total * 2);
119
+ const cursor = new Int32Array(this.neighbourStart.subarray(0, this.count));
120
+ for (let k = 0; k < total; k++) {
121
+ const a = this.link[k * 2] ?? 0;
122
+ const b = this.link[k * 2 + 1] ?? 0;
123
+ this.neighbour[cursor[a]++] = b;
124
+ this.neighbour[cursor[b]++] = a;
125
+ }
126
+ this.cellHead = new Int32Array(Math.max(1, this.count)).fill(-1);
127
+ this.cellNext = new Int32Array(this.count);
128
+ }
129
+ else {
130
+ this.neighbourStart = EMPTY_INDEX;
131
+ this.neighbour = EMPTY_INDEX;
132
+ this.cellHead = EMPTY_INDEX;
133
+ this.cellNext = EMPTY_INDEX;
134
+ }
135
+ }
136
+ /** Pin a particle where it is, or release it. */
137
+ pin(index, pinned = true) {
138
+ this.invMass[index] = pinned ? 0 : 1;
139
+ }
140
+ /**
141
+ * Advance one fixed tick.
142
+ *
143
+ * Integrate, project every constraint `iterations` times, then derive velocity from how far each
144
+ * particle actually moved — which is what makes it position-based: a constraint that moves a
145
+ * particle also, by construction, changes its velocity, so nothing has to reconcile the two.
146
+ */
147
+ step(world, dt) {
148
+ const n = this.count;
149
+ const retain = 1 / (1 + this.damping * dt);
150
+ for (let i = 0; i < n; i++) {
151
+ const im = this.invMass[i] ?? 0;
152
+ this.previous[i * 3] = this.position[i * 3] ?? 0;
153
+ this.previous[i * 3 + 1] = this.position[i * 3 + 1] ?? 0;
154
+ this.previous[i * 3 + 2] = this.position[i * 3 + 2] ?? 0;
155
+ if (im === 0)
156
+ continue;
157
+ const vx = ((this.velocity[i * 3] ?? 0) + this.gravityX * dt) * retain;
158
+ const vy = ((this.velocity[i * 3 + 1] ?? 0) + this.gravityY * dt) * retain;
159
+ const vz = ((this.velocity[i * 3 + 2] ?? 0) + this.gravityZ * dt) * retain;
160
+ this.position[i * 3] = (this.position[i * 3] ?? 0) + vx * dt;
161
+ this.position[i * 3 + 1] = (this.position[i * 3 + 1] ?? 0) + vy * dt;
162
+ this.position[i * 3 + 2] = (this.position[i * 3 + 2] ?? 0) + vz * dt;
163
+ }
164
+ this.lambda.fill(0);
165
+ const invH2 = 1 / (dt * dt);
166
+ for (let pass = 0; pass < this.iterations; pass++) {
167
+ for (let k = 0; k < this.linkCount; k++)
168
+ this.project(k, invH2);
169
+ }
170
+ /*
171
+ * Self-collision after the constraints and before the bodies, which is the order that makes
172
+ * both of them mean what they say. A sheet resolved against itself *first* would then be
173
+ * stretched back into its own fold by the distance constraints; one resolved against the world
174
+ * first would be pushed back into a body by its own self-collision. The world is what the
175
+ * sheet may not enter under any circumstances, so it goes last.
176
+ */
177
+ if (this.selfDistance > 0)
178
+ this.collideSelf();
179
+ if (world)
180
+ this.collideParticles(world, dt);
181
+ const invDt = 1 / dt;
182
+ for (let i = 0; i < n; i++) {
183
+ if ((this.invMass[i] ?? 0) === 0) {
184
+ this.velocity[i * 3] = 0;
185
+ this.velocity[i * 3 + 1] = 0;
186
+ this.velocity[i * 3 + 2] = 0;
187
+ continue;
188
+ }
189
+ this.velocity[i * 3] = ((this.position[i * 3] ?? 0) - (this.previous[i * 3] ?? 0)) * invDt;
190
+ this.velocity[i * 3 + 1] =
191
+ ((this.position[i * 3 + 1] ?? 0) - (this.previous[i * 3 + 1] ?? 0)) * invDt;
192
+ this.velocity[i * 3 + 2] =
193
+ ((this.position[i * 3 + 2] ?? 0) - (this.previous[i * 3 + 2] ?? 0)) * invDt;
194
+ }
195
+ }
196
+ /**
197
+ * One distance constraint, with its compliance.
198
+ *
199
+ * `α = compliance / h²` is what makes the stiffness independent of the step and of the iteration
200
+ * count. At zero compliance this reduces exactly to an ordinary position-based projection, which
201
+ * is why an inextensible sheet costs nothing extra.
202
+ */
203
+ project(k, invH2) {
204
+ const a = this.link[k * 2] ?? 0;
205
+ const b = this.link[k * 2 + 1] ?? 0;
206
+ const wa = this.invMass[a] ?? 0;
207
+ const wb = this.invMass[b] ?? 0;
208
+ const w = wa + wb;
209
+ if (w === 0)
210
+ return;
211
+ const dx = (this.position[b * 3] ?? 0) - (this.position[a * 3] ?? 0);
212
+ const dy = (this.position[b * 3 + 1] ?? 0) - (this.position[a * 3 + 1] ?? 0);
213
+ const dz = (this.position[b * 3 + 2] ?? 0) - (this.position[a * 3 + 2] ?? 0);
214
+ const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
215
+ if (length < 1e-9)
216
+ return;
217
+ const c = length - (this.rest[k] ?? 0);
218
+ const alpha = (this.compliance[k] ?? 0) * invH2;
219
+ const old = this.lambda[k] ?? 0;
220
+ const delta = (-c - alpha * old) / (w + alpha);
221
+ this.lambda[k] = old + delta;
222
+ const nx = (dx / length) * delta;
223
+ const ny = (dy / length) * delta;
224
+ const nz = (dz / length) * delta;
225
+ this.position[a * 3] = (this.position[a * 3] ?? 0) - nx * wa;
226
+ this.position[a * 3 + 1] = (this.position[a * 3 + 1] ?? 0) - ny * wa;
227
+ this.position[a * 3 + 2] = (this.position[a * 3 + 2] ?? 0) - nz * wa;
228
+ this.position[b * 3] = (this.position[b * 3] ?? 0) + nx * wb;
229
+ this.position[b * 3 + 1] = (this.position[b * 3 + 1] ?? 0) + ny * wb;
230
+ this.position[b * 3 + 2] = (this.position[b * 3 + 2] ?? 0) + nz * wb;
231
+ }
232
+ /**
233
+ * Keep the sheet out of itself: a spatial hash over the particles, and a symmetric push apart.
234
+ *
235
+ * **The hash is rebuilt every tick and that is the cheap half.** One pass over the particles
236
+ * writing two integers each, against a structure that tried to be incremental and would have to
237
+ * know which particles moved — which in a cloth solver is all of them.
238
+ *
239
+ * The cell size is the self-collision distance, so a particle's neighbours are in its own cell
240
+ * and the twenty-six around it. **The 3x3x3 scan is what makes the answer complete**: a pair at
241
+ * the distance exactly cannot be in cells more than one apart, and a scan of the own cell alone
242
+ * would miss every pair straddling a boundary — which is most of them, since a folded sheet lies
243
+ * along one.
244
+ *
245
+ * **Each pair is resolved once, by index order.** Visiting both halves would apply the
246
+ * correction twice and turn a resting fold into a buzzing one; the `other > i` test is what
247
+ * makes the pass a sweep over pairs rather than over particles.
248
+ *
249
+ * A projection rather than a constraint with compliance, unlike the distance links: a contact is
250
+ * an inequality — it does nothing until it is violated — and XPBD's accumulated multiplier is
251
+ * for a constraint that holds all tick. *What that gives up* is that the stiffness of a fold is
252
+ * the iteration count's again, for this one term. *What would make it wrong* is a consumer
253
+ * wanting a fold to have a measurable resistance, at which point it needs its own multiplier
254
+ * array and a sign clamp on it.
255
+ */
256
+ collideSelf() {
257
+ const n = this.count;
258
+ const radius = this.selfDistance;
259
+ const inverseCell = 1 / radius;
260
+ this.cellHead.fill(-1);
261
+ const buckets = this.cellHead.length;
262
+ for (let i = 0; i < n; i++) {
263
+ const key = cellKey(Math.floor((this.position[i * 3] ?? 0) * inverseCell), Math.floor((this.position[i * 3 + 1] ?? 0) * inverseCell), Math.floor((this.position[i * 3 + 2] ?? 0) * inverseCell), buckets);
264
+ this.cellNext[i] = this.cellHead[key] ?? -1;
265
+ this.cellHead[key] = i;
266
+ }
267
+ for (let i = 0; i < n; i++) {
268
+ const wi = this.invMass[i] ?? 0;
269
+ const px = this.position[i * 3] ?? 0;
270
+ const py = this.position[i * 3 + 1] ?? 0;
271
+ const pz = this.position[i * 3 + 2] ?? 0;
272
+ const cx = Math.floor(px * inverseCell);
273
+ const cy = Math.floor(py * inverseCell);
274
+ const cz = Math.floor(pz * inverseCell);
275
+ for (let dx = -1; dx <= 1; dx++) {
276
+ for (let dy = -1; dy <= 1; dy++) {
277
+ for (let dz = -1; dz <= 1; dz++) {
278
+ let other = this.cellHead[cellKey(cx + dx, cy + dy, cz + dz, buckets)] ?? -1;
279
+ while (other >= 0) {
280
+ /* Index order, so a pair is met once however many cells they share a bucket with. */
281
+ if (other > i && !this.linked(i, other)) {
282
+ this.separate(i, other, wi, px, py, pz, radius);
283
+ }
284
+ other = this.cellNext[other] ?? -1;
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
290
+ }
291
+ /** Whether a link joins these two. A scan over the eight or so a grid particle has. */
292
+ linked(a, b) {
293
+ const from = this.neighbourStart[a] ?? 0;
294
+ const to = this.neighbourStart[a + 1] ?? from;
295
+ for (let at = from; at < to; at++) {
296
+ if (this.neighbour[at] === b)
297
+ return true;
298
+ }
299
+ return false;
300
+ }
301
+ /** Push two particles apart to exactly `radius`, split by their inverse masses. */
302
+ separate(a, b, wa, ax, ay, az, radius) {
303
+ const wb = this.invMass[b] ?? 0;
304
+ const w = wa + wb;
305
+ if (w === 0)
306
+ return;
307
+ const dx = (this.position[b * 3] ?? 0) - ax;
308
+ const dy = (this.position[b * 3 + 1] ?? 0) - ay;
309
+ const dz = (this.position[b * 3 + 2] ?? 0) - az;
310
+ const squared = dx * dx + dy * dy + dz * dz;
311
+ if (squared >= radius * radius)
312
+ return;
313
+ const length = Math.sqrt(squared);
314
+ /*
315
+ * Two particles at exactly the same point have no direction to separate along, and picking one
316
+ * arbitrarily would pick the same one for every such pair — so a whole collapsed fold would
317
+ * open along one axis. Left alone instead: the next tick's integration moves them apart, and a
318
+ * pair genuinely coincident is a sheet that has already failed.
319
+ */
320
+ if (length < 1e-9)
321
+ return;
322
+ const push = (radius - length) / length / w;
323
+ const mx = dx * push;
324
+ const my = dy * push;
325
+ const mz = dz * push;
326
+ this.position[a * 3] = ax - mx * wa;
327
+ this.position[a * 3 + 1] = ay - my * wa;
328
+ this.position[a * 3 + 2] = az - mz * wa;
329
+ this.position[b * 3] = (this.position[b * 3] ?? 0) + mx * wb;
330
+ this.position[b * 3 + 1] = (this.position[b * 3 + 1] ?? 0) + my * wb;
331
+ this.position[b * 3 + 2] = (this.position[b * 3 + 2] ?? 0) + mz * wb;
332
+ }
333
+ /**
334
+ * Push every particle out of anything it has entered, and hand the body back what it took.
335
+ *
336
+ * **The impulse is derived from the correction rather than measured**, which is what makes the
337
+ * exchange cost one multiply: the constraint solve has already decided where the particle would
338
+ * be, the push says where it must be instead, and the difference over the tick is the velocity
339
+ * the collision removed from it. `m·Δ/h` is that momentum, and the body gets its negative at the
340
+ * point of contact — a push straight down on a plank's end tips the plank, because
341
+ * `applyImpulse` takes the point.
342
+ *
343
+ * A static body is skipped for the impulse rather than for the push: scenery does not move, and
344
+ * `applyImpulse` would return without doing anything anyway.
345
+ */
346
+ collideParticles(world, dt) {
347
+ const bodies = world.bodies;
348
+ const exchange = this.coupling > 0 ? (this.particleMass * this.coupling) / dt : 0;
349
+ for (let i = 0; i < this.count; i++) {
350
+ if ((this.invMass[i] ?? 0) === 0)
351
+ continue;
352
+ const px = this.position[i * 3] ?? 0;
353
+ const py = this.position[i * 3 + 1] ?? 0;
354
+ const pz = this.position[i * 3 + 2] ?? 0;
355
+ for (let b = 0; b < bodies.count; b++) {
356
+ const shape = bodies.shape[b];
357
+ if (!shape)
358
+ continue;
359
+ // A cheap reject before anything expensive: the shape's own bounding sphere.
360
+ const bx = bodies.posX[b] ?? 0;
361
+ const by = bodies.posY[b] ?? 0;
362
+ const bz = bodies.posZ[b] ?? 0;
363
+ const dx = px - bx;
364
+ const dy = py - by;
365
+ const dz = pz - bz;
366
+ const reach = shape.boundRadius + this.thickness;
367
+ if (dx * dx + dy * dy + dz * dz > reach * reach)
368
+ continue;
369
+ POSE.x = bx;
370
+ POSE.y = by;
371
+ POSE.z = bz;
372
+ POSE.qx = bodies.rotX[b] ?? 0;
373
+ POSE.qy = bodies.rotY[b] ?? 0;
374
+ POSE.qz = bodies.rotZ[b] ?? 0;
375
+ POSE.qw = bodies.rotW[b] ?? 1;
376
+ if (!pushOut(shape, POSE, px, py, pz, this.thickness, OUT))
377
+ continue;
378
+ this.position[i * 3] = OUT[0];
379
+ this.position[i * 3 + 1] = OUT[1];
380
+ this.position[i * 3 + 2] = OUT[2];
381
+ /*
382
+ * The other half of the exchange. Negated, because the push moved the particle *away*
383
+ * from the body and the body is what pushed it — Newton's third law with the tick length
384
+ * folded in. At the contact point rather than at the centre, so a sheet landing on one end
385
+ * of a plank tips it.
386
+ */
387
+ if (exchange > 0 && bodies.type[b] !== BODY_STATIC) {
388
+ world.applyImpulse(b, -(OUT[0] - px) * exchange, -(OUT[1] - py) * exchange, -(OUT[2] - pz) * exchange, px, py, pz);
389
+ }
390
+ // A static body is scenery, so one push settles it; a moving one is resolved next tick.
391
+ if (bodies.type[b] === BODY_STATIC)
392
+ break;
393
+ }
394
+ }
395
+ }
396
+ }
397
+ /** What a sheet with no self-collision carries instead of four arrays it will never read. */
398
+ const EMPTY_INDEX = new Int32Array(0);
399
+ const POSE = { x: 0, y: 0, z: 0, qx: 0, qy: 0, qz: 0, qw: 1 };
400
+ const OUT = new Float64Array(3);
401
+ const LOCAL = new Float64Array(3);
402
+ const CYLINDER = new Float64Array(11);
403
+ const SURFACE = new Float64Array(7);
404
+ /**
405
+ * Move a point to the outside of a shape, if it is inside. Returns whether it moved.
406
+ *
407
+ * Works in the shape's own frame so a rotated body costs one rotation rather than a transformed
408
+ * copy of its geometry. A faceless shape is a sphere or a capsule and is handled by its radius; a
409
+ * polytope is pushed out along its least-penetrating face.
410
+ */
411
+ function pushOut(shape, pose, px, py, pz, thickness, out) {
412
+ unrotate(pose, px - pose.x, py - pose.y, pz - pose.z, LOCAL);
413
+ const lx = LOCAL[0];
414
+ const ly = LOCAL[1];
415
+ const lz = LOCAL[2];
416
+ const faces = faceCount(shape);
417
+ if (isCylinder(shape)) {
418
+ /*
419
+ * A cylinder in its own coordinates, which is where this function already is. `closestOnCylinder`
420
+ * answers all four regions — beside the side, past a cap, past a rim, inside — so a particle
421
+ * that has entered near the rim comes out at the rim rather than being pushed through the solid
422
+ * along whichever of the two surfaces the code happened to test first.
423
+ */
424
+ readCylinder(shape, LOCAL_POSE, CYLINDER);
425
+ closestOnCylinder(CYLINDER, lx, ly, lz, SURFACE);
426
+ const want = shape.radius + thickness;
427
+ if (SURFACE[6] >= want)
428
+ return false;
429
+ rotate(pose, SURFACE[0] + SURFACE[3] * want, SURFACE[1] + SURFACE[4] * want, SURFACE[2] + SURFACE[5] * want, out);
430
+ out[0] += pose.x;
431
+ out[1] += pose.y;
432
+ out[2] += pose.z;
433
+ return true;
434
+ }
435
+ if (faces === 0) {
436
+ // The segment the shape is built on, then its radius.
437
+ const count = shape.vertices.length / 3;
438
+ const ax = shape.vertices[0] ?? 0;
439
+ const ay = shape.vertices[1] ?? 0;
440
+ const az = shape.vertices[2] ?? 0;
441
+ const bx = count > 1 ? (shape.vertices[3] ?? 0) : ax;
442
+ const by = count > 1 ? (shape.vertices[4] ?? 0) : ay;
443
+ const bz = count > 1 ? (shape.vertices[5] ?? 0) : az;
444
+ const ex = bx - ax;
445
+ const ey = by - ay;
446
+ const ez = bz - az;
447
+ const ee = ex * ex + ey * ey + ez * ez;
448
+ let t = 0;
449
+ if (ee > 1e-12) {
450
+ t = ((lx - ax) * ex + (ly - ay) * ey + (lz - az) * ez) / ee;
451
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
452
+ }
453
+ const cx = ax + ex * t;
454
+ const cy = ay + ey * t;
455
+ const cz = az + ez * t;
456
+ let nx = lx - cx;
457
+ let ny = ly - cy;
458
+ let nz = lz - cz;
459
+ const d = Math.sqrt(nx * nx + ny * ny + nz * nz);
460
+ const want = shape.radius + thickness;
461
+ if (d >= want)
462
+ return false;
463
+ if (d < 1e-9) {
464
+ nx = 0;
465
+ ny = 1;
466
+ nz = 0;
467
+ }
468
+ else {
469
+ nx /= d;
470
+ ny /= d;
471
+ nz /= d;
472
+ }
473
+ rotate(pose, cx + nx * want, cy + ny * want, cz + nz * want, out);
474
+ out[0] += pose.x;
475
+ out[1] += pose.y;
476
+ out[2] += pose.z;
477
+ return true;
478
+ }
479
+ let bestFace = 0;
480
+ let best = -Infinity;
481
+ for (let f = 0; f < faces; f++) {
482
+ const d = lx * (shape.facePlanes[f * 4] ?? 0) +
483
+ ly * (shape.facePlanes[f * 4 + 1] ?? 0) +
484
+ lz * (shape.facePlanes[f * 4 + 2] ?? 0) -
485
+ (shape.facePlanes[f * 4 + 3] ?? 0) -
486
+ shape.radius;
487
+ if (d > best) {
488
+ best = d;
489
+ bestFace = f;
490
+ }
491
+ }
492
+ if (best >= thickness)
493
+ return false;
494
+ const move = thickness - best;
495
+ rotate(pose, lx + (shape.facePlanes[bestFace * 4] ?? 0) * move, ly + (shape.facePlanes[bestFace * 4 + 1] ?? 0) * move, lz + (shape.facePlanes[bestFace * 4 + 2] ?? 0) * move, out);
496
+ out[0] += pose.x;
497
+ out[1] += pose.y;
498
+ out[2] += pose.z;
499
+ return true;
500
+ }
501
+ function rotate(pose, x, y, z, out) {
502
+ const tx = 2 * (pose.qy * z - pose.qz * y);
503
+ const ty = 2 * (pose.qz * x - pose.qx * z);
504
+ const tz = 2 * (pose.qx * y - pose.qy * x);
505
+ out[0] = x + pose.qw * tx + (pose.qy * tz - pose.qz * ty);
506
+ out[1] = y + pose.qw * ty + (pose.qz * tx - pose.qx * tz);
507
+ out[2] = z + pose.qw * tz + (pose.qx * ty - pose.qy * tx);
508
+ }
509
+ function unrotate(pose, x, y, z, out) {
510
+ const qx = -pose.qx;
511
+ const qy = -pose.qy;
512
+ const qz = -pose.qz;
513
+ const qw = pose.qw;
514
+ const tx = 2 * (qy * z - qz * y);
515
+ const ty = 2 * (qz * x - qx * z);
516
+ const tz = 2 * (qx * y - qy * x);
517
+ out[0] = x + qw * tx + (qy * tz - qz * ty);
518
+ out[1] = y + qw * ty + (qz * tx - qx * tz);
519
+ out[2] = z + qw * tz + (qx * ty - qy * tx);
520
+ }
521
+ /**
522
+ * Three cell coordinates into one bucket, by the standard three-prime mix.
523
+ *
524
+ * The primes are the ones every published spatial hash for particles uses, and the reason to take
525
+ * them rather than invent three is that a poor choice shows up as a *distribution* rather than as
526
+ * an error: buckets that fill unevenly, a scan that lengthens, and a sheet that costs more the
527
+ * further it is from the origin. `>>> 0` before the modulo, because a negative cell coordinate is
528
+ * ordinary — a sheet hanging below the origin has them — and a negative bucket index would read
529
+ * `undefined` out of the table and start every chain empty.
530
+ */
531
+ function cellKey(x, y, z, buckets) {
532
+ return (((x * 73856093) ^ (y * 19349663) ^ (z * 83492791)) >>> 0) % buckets;
533
+ }
534
+ function distance(p, a, b) {
535
+ const dx = (p[b * 3] ?? 0) - (p[a * 3] ?? 0);
536
+ const dy = (p[b * 3 + 1] ?? 0) - (p[a * 3 + 1] ?? 0);
537
+ const dz = (p[b * 3 + 2] ?? 0) - (p[a * 3 + 2] ?? 0);
538
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
539
+ }
540
+ /**
541
+ * A rectangular sheet: positions, its stretch links, and the bend links across them.
542
+ *
543
+ * Bend links skip a particle rather than joining diagonals, because a diagonal resists *shear* and a
544
+ * skip resists *bowing*, and a sheet that cannot shear drapes like cardboard.
545
+ *
546
+ * **A skip-one distance constraint resists curvature and not orientation**, which is worth knowing
547
+ * before tuning it: three collinear particles satisfy it at any angle, so a flat sheet and a sheet
548
+ * hanging straight down are equally happy. Measured — a cantilever pinned along one edge settles at
549
+ * exactly vertical whether the bend compliance is 1e-8 or 1e6. What it *does* buy is 12.5
550
+ * millimetres less bow on a strip pinned at both ends with slack.
551
+ *
552
+ * **What would make it wrong** is a consumer wanting a cloak to stand out from a shoulder, and the
553
+ * answer then is a dihedral-angle constraint over the two triangles either side of an edge, which
554
+ * knows a rest angle and therefore knows the difference between flat and folded.
555
+ */
556
+ export function makeClothGrid(columns, rows, spacing) {
557
+ const positions = new Float32Array(columns * rows * 3);
558
+ const links = [];
559
+ const bend = [];
560
+ const at = (c, r) => r * columns + c;
561
+ for (let r = 0; r < rows; r++) {
562
+ for (let c = 0; c < columns; c++) {
563
+ const i = at(c, r);
564
+ positions[i * 3] = c * spacing;
565
+ positions[i * 3 + 1] = 0;
566
+ positions[i * 3 + 2] = r * spacing;
567
+ if (c + 1 < columns)
568
+ links.push(i, at(c + 1, r));
569
+ if (r + 1 < rows)
570
+ links.push(i, at(c, r + 1));
571
+ // Shear, which a grid needs or it folds flat along a diagonal.
572
+ if (c + 1 < columns && r + 1 < rows)
573
+ links.push(i, at(c + 1, r + 1));
574
+ if (c + 1 < columns && r + 1 < rows)
575
+ links.push(at(c + 1, r), at(c, r + 1));
576
+ if (c + 2 < columns)
577
+ bend.push(i, at(c + 2, r));
578
+ if (r + 2 < rows)
579
+ bend.push(i, at(c, r + 2));
580
+ }
581
+ }
582
+ return {
583
+ positions,
584
+ links: new Int32Array(links),
585
+ bendLinks: new Int32Array(bend),
586
+ };
587
+ }
@@ -0,0 +1,10 @@
1
+ /** An axis-aligned box, and the one operation that builds one. */
2
+ export interface Aabb {
3
+ minX: number;
4
+ minY: number;
5
+ minZ: number;
6
+ maxX: number;
7
+ maxY: number;
8
+ maxZ: number;
9
+ }
10
+ export declare function aabbFromCenter(cx: number, cy: number, cz: number, hx: number, hy: number, hz: number): Aabb;
@@ -0,0 +1,11 @@
1
+ /** An axis-aligned box, and the one operation that builds one. */
2
+ export function aabbFromCenter(cx, cy, cz, hx, hy, hz) {
3
+ return {
4
+ minX: cx - hx,
5
+ minY: cy - hy,
6
+ minZ: cz - hz,
7
+ maxX: cx + hx,
8
+ maxY: cy + hy,
9
+ maxZ: cz + hz,
10
+ };
11
+ }