@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/bodies.js ADDED
@@ -0,0 +1,298 @@
1
+ import { combineMassProperties, createMassProperties, shapeMassProperties } from './mass.js';
2
+ /**
3
+ * Rigid body state, structure-of-arrays, indexed by a dense body index.
4
+ *
5
+ * **`Float32Array` for state and doubles for every intermediate.** A store rounds through
6
+ * `Math.fround`, which ECMAScript specifies exactly, while the arithmetic between stores stays in
7
+ * doubles — so the solver computes at higher precision than it keeps and halves its memory
8
+ * bandwidth. Both halves of that are deliberate, and JavaScript has no SIMD to spend the bandwidth
9
+ * on instead.
10
+ *
11
+ * **Indices are dense and removal swaps the last body in**, so a caller holding an index must be
12
+ * told when one moves. `remove` returns the index that moved, or −1, and every structure keyed by
13
+ * body index has to act on it.
14
+ */
15
+ export const BODY_STATIC = 0;
16
+ export const BODY_KINEMATIC = 1;
17
+ export const BODY_DYNAMIC = 2;
18
+ /** Set on a body whose island is asleep. Woken bodies clear it together. */
19
+ export const BODY_SLEEPING = 1;
20
+ /**
21
+ * A sensor reports overlap and resolves none of it.
22
+ *
23
+ * Narrowphase still runs — an overlap that is never computed cannot be reported — and the solver is
24
+ * what skips it. **What this costs** is a sensor being as dear as a solid body in the broadphase and
25
+ * narrowphase, which is where most of the cost is anyway.
26
+ */
27
+ export const BODY_SENSOR = 2;
28
+ const SCRATCH_MASS = createMassProperties();
29
+ export class BodySet {
30
+ count = 0;
31
+ posX;
32
+ posY;
33
+ posZ;
34
+ rotX;
35
+ rotY;
36
+ rotZ;
37
+ rotW;
38
+ velX;
39
+ velY;
40
+ velZ;
41
+ angX;
42
+ angY;
43
+ angZ;
44
+ invMass;
45
+ /** Body-frame inverse inertia, six symmetric terms per body: xx, yy, zz, xy, xz, yz. */
46
+ invInertia;
47
+ /** Centre of mass in body frame, which a shape built off-centre has. */
48
+ comX;
49
+ comY;
50
+ comZ;
51
+ friction;
52
+ restitution;
53
+ type;
54
+ flags;
55
+ sleepTicks;
56
+ layer;
57
+ mask;
58
+ shape = [];
59
+ /** The parts of a compound body, or undefined where it has a single `shape`. */
60
+ shapes = [];
61
+ capacity;
62
+ constructor(capacity = 64) {
63
+ this.capacity = Math.max(1, capacity | 0);
64
+ const f = (n = 1) => new Float32Array(this.capacity * n);
65
+ this.posX = f();
66
+ this.posY = f();
67
+ this.posZ = f();
68
+ this.rotX = f();
69
+ this.rotY = f();
70
+ this.rotZ = f();
71
+ this.rotW = f();
72
+ this.velX = f();
73
+ this.velY = f();
74
+ this.velZ = f();
75
+ this.angX = f();
76
+ this.angY = f();
77
+ this.angZ = f();
78
+ this.invMass = f();
79
+ this.invInertia = f(6);
80
+ this.comX = f();
81
+ this.comY = f();
82
+ this.comZ = f();
83
+ this.friction = f();
84
+ this.restitution = f();
85
+ this.type = new Uint8Array(this.capacity);
86
+ this.flags = new Uint8Array(this.capacity);
87
+ this.sleepTicks = new Uint16Array(this.capacity);
88
+ this.layer = new Uint32Array(this.capacity);
89
+ this.mask = new Uint32Array(this.capacity);
90
+ }
91
+ add(desc) {
92
+ if (this.count === this.capacity)
93
+ this.grow();
94
+ const i = this.count++;
95
+ this.posX[i] = desc.x ?? 0;
96
+ this.posY[i] = desc.y ?? 0;
97
+ this.posZ[i] = desc.z ?? 0;
98
+ this.rotX[i] = desc.qx ?? 0;
99
+ this.rotY[i] = desc.qy ?? 0;
100
+ this.rotZ[i] = desc.qz ?? 0;
101
+ this.rotW[i] = desc.qw ?? 1;
102
+ this.velX[i] = 0;
103
+ this.velY[i] = 0;
104
+ this.velZ[i] = 0;
105
+ this.angX[i] = 0;
106
+ this.angY[i] = 0;
107
+ this.angZ[i] = 0;
108
+ this.type[i] = desc.type;
109
+ this.flags[i] = desc.sensor ? BODY_SENSOR : 0;
110
+ this.sleepTicks[i] = 0;
111
+ this.friction[i] = desc.friction ?? 0.5;
112
+ this.restitution[i] = desc.restitution ?? 0;
113
+ this.layer[i] = desc.layer ?? 1;
114
+ this.mask[i] = desc.mask ?? 0xffffffff;
115
+ this.shape[i] = desc.shape;
116
+ this.shapes[i] = desc.shapes;
117
+ /*
118
+ * A static or kinematic body has zero inverse mass and zero inverse inertia, which is what
119
+ * makes it immovable *arithmetically* rather than by a branch in the solver. Every constraint
120
+ * then treats it uniformly and the hot loop has one fewer test in it.
121
+ */
122
+ if (desc.type === BODY_DYNAMIC) {
123
+ const density = desc.density ?? 1000;
124
+ const m = desc.shapes
125
+ ? combineMassProperties(desc.shapes, density, SCRATCH_MASS)
126
+ : shapeMassProperties(desc.shape, density, SCRATCH_MASS);
127
+ const mass = m.volume * density;
128
+ this.invMass[i] = mass > 0 ? 1 / mass : 0;
129
+ this.comX[i] = m.comX;
130
+ this.comY[i] = m.comY;
131
+ this.comZ[i] = m.comZ;
132
+ invertSymmetric(m.ixx, m.iyy, m.izz, m.ixy, m.ixz, m.iyz, this.invInertia, i * 6);
133
+ }
134
+ else {
135
+ this.invMass[i] = 0;
136
+ this.comX[i] = 0;
137
+ this.comY[i] = 0;
138
+ this.comZ[i] = 0;
139
+ for (let k = 0; k < 6; k++)
140
+ this.invInertia[i * 6 + k] = 0;
141
+ }
142
+ return i;
143
+ }
144
+ /** Swap-remove. Returns the index that moved into `index`, or −1 if it was the last. */
145
+ remove(index) {
146
+ const last = --this.count;
147
+ if (index === last) {
148
+ this.shape[index] = undefined;
149
+ this.shapes[index] = undefined;
150
+ return -1;
151
+ }
152
+ const copy = (a, stride = 1) => {
153
+ for (let k = 0; k < stride; k++)
154
+ a[index * stride + k] = a[last * stride + k] ?? 0;
155
+ };
156
+ for (const a of [
157
+ this.posX,
158
+ this.posY,
159
+ this.posZ,
160
+ this.rotX,
161
+ this.rotY,
162
+ this.rotZ,
163
+ this.rotW,
164
+ this.velX,
165
+ this.velY,
166
+ this.velZ,
167
+ this.angX,
168
+ this.angY,
169
+ this.angZ,
170
+ this.invMass,
171
+ this.comX,
172
+ this.comY,
173
+ this.comZ,
174
+ this.friction,
175
+ this.restitution,
176
+ ])
177
+ copy(a);
178
+ copy(this.invInertia, 6);
179
+ copy(this.type);
180
+ copy(this.flags);
181
+ copy(this.sleepTicks);
182
+ copy(this.layer);
183
+ copy(this.mask);
184
+ this.shape[index] = this.shape[last];
185
+ this.shape[last] = undefined;
186
+ this.shapes[index] = this.shapes[last];
187
+ this.shapes[last] = undefined;
188
+ return last;
189
+ }
190
+ /**
191
+ * Fill `out` with the world-frame inverse inertia, six symmetric terms.
192
+ *
193
+ * `R · I⁻¹ · Rᵀ`, a congruence, rather than a diagonalisation. Storing three principal moments
194
+ * would need an eigen decomposition, and Jacobi is iterative — a fixed sweep count would be
195
+ * deterministic but is one more thing to get exactly right for no gain. **What this gives up** is
196
+ * six floats per body instead of three and a congruence per substep. **What would make it wrong**
197
+ * is a measurement showing that congruence dominates the tick.
198
+ */
199
+ worldInverseInertia(index, out, at = 0) {
200
+ const x = this.rotX[index] ?? 0;
201
+ const y = this.rotY[index] ?? 0;
202
+ const z = this.rotZ[index] ?? 0;
203
+ const w = this.rotW[index] ?? 1;
204
+ // Rotation matrix, row-major.
205
+ const r00 = 1 - 2 * (y * y + z * z);
206
+ const r01 = 2 * (x * y - z * w);
207
+ const r02 = 2 * (x * z + y * w);
208
+ const r10 = 2 * (x * y + z * w);
209
+ const r11 = 1 - 2 * (x * x + z * z);
210
+ const r12 = 2 * (y * z - x * w);
211
+ const r20 = 2 * (x * z - y * w);
212
+ const r21 = 2 * (y * z + x * w);
213
+ const r22 = 1 - 2 * (x * x + y * y);
214
+ const b = index * 6;
215
+ const mxx = this.invInertia[b] ?? 0;
216
+ const myy = this.invInertia[b + 1] ?? 0;
217
+ const mzz = this.invInertia[b + 2] ?? 0;
218
+ const mxy = this.invInertia[b + 3] ?? 0;
219
+ const mxz = this.invInertia[b + 4] ?? 0;
220
+ const myz = this.invInertia[b + 5] ?? 0;
221
+ // T = R · M, then out = T · Rᵀ, keeping only the six symmetric terms.
222
+ const t00 = r00 * mxx + r01 * mxy + r02 * mxz;
223
+ const t01 = r00 * mxy + r01 * myy + r02 * myz;
224
+ const t02 = r00 * mxz + r01 * myz + r02 * mzz;
225
+ const t10 = r10 * mxx + r11 * mxy + r12 * mxz;
226
+ const t11 = r10 * mxy + r11 * myy + r12 * myz;
227
+ const t12 = r10 * mxz + r11 * myz + r12 * mzz;
228
+ const t20 = r20 * mxx + r21 * mxy + r22 * mxz;
229
+ const t21 = r20 * mxy + r21 * myy + r22 * myz;
230
+ const t22 = r20 * mxz + r21 * myz + r22 * mzz;
231
+ out[at] = t00 * r00 + t01 * r01 + t02 * r02;
232
+ out[at + 1] = t10 * r10 + t11 * r11 + t12 * r12;
233
+ out[at + 2] = t20 * r20 + t21 * r21 + t22 * r22;
234
+ out[at + 3] = t00 * r10 + t01 * r11 + t02 * r12;
235
+ out[at + 4] = t00 * r20 + t01 * r21 + t02 * r22;
236
+ out[at + 5] = t10 * r20 + t11 * r21 + t12 * r22;
237
+ }
238
+ grow() {
239
+ const next = this.capacity * 2;
240
+ const wider = (a, stride = 1) => {
241
+ const out = new a.constructor(next * stride);
242
+ out.set(a);
243
+ return out;
244
+ };
245
+ this.posX = wider(this.posX);
246
+ this.posY = wider(this.posY);
247
+ this.posZ = wider(this.posZ);
248
+ this.rotX = wider(this.rotX);
249
+ this.rotY = wider(this.rotY);
250
+ this.rotZ = wider(this.rotZ);
251
+ this.rotW = wider(this.rotW);
252
+ this.velX = wider(this.velX);
253
+ this.velY = wider(this.velY);
254
+ this.velZ = wider(this.velZ);
255
+ this.angX = wider(this.angX);
256
+ this.angY = wider(this.angY);
257
+ this.angZ = wider(this.angZ);
258
+ this.invMass = wider(this.invMass);
259
+ this.invInertia = wider(this.invInertia, 6);
260
+ this.comX = wider(this.comX);
261
+ this.comY = wider(this.comY);
262
+ this.comZ = wider(this.comZ);
263
+ this.friction = wider(this.friction);
264
+ this.restitution = wider(this.restitution);
265
+ this.type = wider(this.type);
266
+ this.flags = wider(this.flags);
267
+ this.sleepTicks = wider(this.sleepTicks);
268
+ this.layer = wider(this.layer);
269
+ this.mask = wider(this.mask);
270
+ this.capacity = next;
271
+ }
272
+ }
273
+ /**
274
+ * Invert a symmetric 3×3 in closed form, writing six terms.
275
+ *
276
+ * A singular tensor — a shape with no volume — inverts to zero rather than to infinity, which makes
277
+ * it behave as a point mass instead of producing NaN in every constraint that touches it. An
278
+ * unwritten uniform is zero and zero is a real value, which is the failure this engine has recorded
279
+ * twice; here zero is the *correct* value and the comment is what says so.
280
+ */
281
+ function invertSymmetric(xx, yy, zz, xy, xz, yz, out, at) {
282
+ const c00 = yy * zz - yz * yz;
283
+ const c01 = xz * yz - xy * zz;
284
+ const c02 = xy * yz - xz * yy;
285
+ const det = xx * c00 + xy * c01 + xz * c02;
286
+ if (det === 0) {
287
+ for (let k = 0; k < 6; k++)
288
+ out[at + k] = 0;
289
+ return;
290
+ }
291
+ const inv = 1 / det;
292
+ out[at] = c00 * inv;
293
+ out[at + 1] = (xx * zz - xz * xz) * inv;
294
+ out[at + 2] = (xx * yy - xy * xy) * inv;
295
+ out[at + 3] = c01 * inv;
296
+ out[at + 4] = c02 * inv;
297
+ out[at + 5] = (xy * xz - xx * yz) * inv;
298
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * What water does to a body that falls in.
3
+ *
4
+ * **Two things, and both are about momentum rather than about floating.** A kinematic body has no
5
+ * mass to displace, so there is no buoyant force to compute — what a consumer actually wants is
6
+ * that falling in *costs* something, and that the drop settles instead of accelerating into the
7
+ * seabed.
8
+ *
9
+ * **Drag on every axis, not only the vertical one.** Killing horizontal momentum is the whole
10
+ * reason falling in is a setback rather than a shortcut: a body that kept its speed underwater
11
+ * would be taking a faster line.
12
+ *
13
+ * Zero game nouns, which is why it is here. What stays with a consumer is where the water is.
14
+ */
15
+ export interface BuoyancyOptions {
16
+ /**
17
+ * The surface height. Default −Infinity, which is a world with no water in it.
18
+ *
19
+ * A consumer that has not asked reaches none of this and pays for none of it.
20
+ */
21
+ level?: number;
22
+ /** How much of a body's speed a second of water takes, as a fraction. */
23
+ drag?: number;
24
+ /** The fastest a submerged body sinks, metres a second. Negative is downward. */
25
+ sinkSpeed?: number;
26
+ }
27
+ export interface Buoyancy {
28
+ readonly level: number;
29
+ readonly drag: number;
30
+ readonly sinkSpeed: number;
31
+ }
32
+ /** Anything with a velocity. Declared structurally so nothing is imported in either direction. */
33
+ export interface BuoyantState {
34
+ velX: number;
35
+ velY: number;
36
+ velZ: number;
37
+ }
38
+ export declare function createBuoyancy(options?: BuoyancyOptions): Buoyancy;
39
+ /**
40
+ * Apply it, and report whether the body is submerged.
41
+ *
42
+ * @param y Where the body is. Under `level` is submerged.
43
+ * @returns Whether it was, so a consumer can count the time or drown somebody — which is a game's
44
+ * decision and not this function's.
45
+ */
46
+ export declare function applyBuoyancy(water: Buoyancy, y: number, state: BuoyantState, dt: number): boolean;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * What water does to a body that falls in.
3
+ *
4
+ * **Two things, and both are about momentum rather than about floating.** A kinematic body has no
5
+ * mass to displace, so there is no buoyant force to compute — what a consumer actually wants is
6
+ * that falling in *costs* something, and that the drop settles instead of accelerating into the
7
+ * seabed.
8
+ *
9
+ * **Drag on every axis, not only the vertical one.** Killing horizontal momentum is the whole
10
+ * reason falling in is a setback rather than a shortcut: a body that kept its speed underwater
11
+ * would be taking a faster line.
12
+ *
13
+ * Zero game nouns, which is why it is here. What stays with a consumer is where the water is.
14
+ */
15
+ export function createBuoyancy(options = {}) {
16
+ return {
17
+ level: options.level ?? -Infinity,
18
+ drag: options.drag ?? 0,
19
+ sinkSpeed: options.sinkSpeed ?? 0,
20
+ };
21
+ }
22
+ /**
23
+ * Apply it, and report whether the body is submerged.
24
+ *
25
+ * @param y Where the body is. Under `level` is submerged.
26
+ * @returns Whether it was, so a consumer can count the time or drown somebody — which is a game's
27
+ * decision and not this function's.
28
+ */
29
+ export function applyBuoyancy(water, y, state, dt) {
30
+ if (y >= water.level)
31
+ return false;
32
+ /*
33
+ * Clamped at zero: `1 − drag·dt` goes negative past `dt = 1/drag`, and a negative factor does
34
+ * not slow a body, it throws it backwards. The worst a long step can do is stop it dead.
35
+ */
36
+ const kept = Math.max(0, 1 - water.drag * dt);
37
+ state.velX *= kept;
38
+ state.velY *= kept;
39
+ state.velZ *= kept;
40
+ /* A floor on the fall rather than a speed it is driven to: a body drifting down slower than
41
+ terminal is left alone, and one falling faster is settled to it. */
42
+ if (state.velY < water.sinkSpeed)
43
+ state.velY = water.sinkSpeed;
44
+ return true;
45
+ }
@@ -0,0 +1,226 @@
1
+ import type { PhysicsWorld } from './world.ts';
2
+ /**
3
+ * Cloth, as position-based dynamics with compliance.
4
+ *
5
+ * **A separate solver from the rigid bodies, deliberately, and it is §5's decision in a different
6
+ * place.** Thousands of rotationless particles are the wrong data layout for a rigid constraint
7
+ * graph: every particle would carry an orientation it never uses and an inertia tensor that is
8
+ * always zero. Two solvers with two jobs, not two implementations of one decision.
9
+ *
10
+ * **Compliance rather than a stiffness constant**, for the same reason §7 states contact stiffness
11
+ * as a frequency: `α = compliance / h²` makes a constraint's stiffness independent of the step
12
+ * length, so the iteration count is a quality dial rather than a behaviour change. A raw
13
+ * position-based solver's stiffness is a function of how many times you iterated, which means
14
+ * tuning it is tuning the solver rather than the cloth.
15
+ *
16
+ * **Two-way coupled since 2026-08-27, by an impulse exchange rather than by a merged island.** The
17
+ * refusal this replaces was right about the price and wrong about the only way to pay it. Putting
18
+ * particles into the rigid solver's constraint graph *would* merge islands — a character in a cloak
19
+ * becomes one island with everything the cloak brushes, and merged islands are what §8a's
20
+ * parallelism is made of. So that is still refused. What ships instead is the exchange the two
21
+ * solvers can make **without either learning about the other's structure**: the collision pass
22
+ * already moves a particle out of a body, and a particle moved by `d` in a tick of `h` has had its
23
+ * momentum changed by `m·d/h`; the body receives the opposite of that at the contact point, through
24
+ * the same `applyImpulse` any consumer would call.
25
+ *
26
+ * **What that costs, said plainly.** The exchange is *explicit* — the body responds on the next
27
+ * tick rather than in the same solve — so it is a spring with a one-tick delay in it, and a very
28
+ * heavy sheet landing on a very light body can oscillate. `coupling` is a scale from 0 to 1 and
29
+ * **0 is the default**, so no existing consumer's world moves by a millimetre until it asks. *What
30
+ * would reverse the remaining refusal:* a consumer who needs the sheet and the body to converge
31
+ * together in one solve, at which point the price is the island structure and it is a track rather
32
+ * than a field.
33
+ */
34
+ export interface ClothOptions {
35
+ /** Metres of stretch per newton. Zero is inextensible; larger is stretchier. */
36
+ stretchCompliance?: number;
37
+ /** The same, for the constraints that resist folding. */
38
+ bendCompliance?: number;
39
+ /** Solver passes per tick. A quality dial: compliance keeps stiffness meaningful across it. */
40
+ iterations?: number;
41
+ /**
42
+ * How close two unconnected particles may come before they push each other apart, in metres.
43
+ *
44
+ * **0 is off and is the default**, because self-collision is the half of cloth whose *tuning*
45
+ * decides whether it ships: a sheet that resists itself at a distance larger than its own
46
+ * spacing cannot lie flat, and one that resists at a distance far smaller than a fold's
47
+ * thickness passes through itself anyway. A consumer sets this from the sheet they built, and
48
+ * the honest starting point is **a little under the grid spacing** — `makeClothGrid` at 5 cm
49
+ * wants about 3 to 4 cm.
50
+ *
51
+ * **Particles joined by a constraint are exempt**, and that is not an optimisation. A stretch
52
+ * link's rest length is the spacing, so a self-collision distance anywhere near it would have
53
+ * every neighbour pair fighting its own link at every step — the sheet would inflate rather than
54
+ * drape, which is the failure that reads as a solver bug. `ClothBody` builds the adjacency once
55
+ * at construction for exactly this.
56
+ */
57
+ selfDistance?: number;
58
+ /** Per-second velocity retained, as a fraction. */
59
+ damping?: number;
60
+ /** How far a particle is held off a surface it meets. */
61
+ thickness?: number;
62
+ /**
63
+ * How much of the momentum a pushed particle gave up is handed back to the body, 0 to 1.
64
+ *
65
+ * **0 by default, which is one-way coupling and is what every consumer had before this existed.**
66
+ * 1 is the whole exchange; anything between is a sheet that pushes less than its mass says it
67
+ * should, which is the dial a consumer reaches for when the delay in the exchange makes a light
68
+ * body jitter.
69
+ */
70
+ coupling?: number;
71
+ /**
72
+ * What one particle weighs, in kilograms. Only the coupling reads it.
73
+ *
74
+ * **Separate from `invMass`, and the separation is deliberate.** That array is a *relative
75
+ * weight* in the position solve — every free particle is 1 and a pinned one is 0 — and the
76
+ * compliance is tuned against it, so redefining it as an inverse mass in kilograms would move
77
+ * every sheet any consumer has ever tuned. The coupling needs kilograms and nothing else does,
78
+ * so it takes them here.
79
+ *
80
+ * The default is fifty grams: a square metre of ordinary cloth is two to three hundred grams,
81
+ * and `makeClothGrid` at a five-centimetre spacing puts four hundred particles in that square
82
+ * metre. A consumer building a tarpaulin or a chainmail cape sets it.
83
+ */
84
+ particleMass?: number;
85
+ gravityX?: number;
86
+ gravityY?: number;
87
+ gravityZ?: number;
88
+ }
89
+ export declare class ClothBody {
90
+ /** xyz-packed particle positions. */
91
+ readonly position: Float32Array;
92
+ /** xyz-packed velocities. */
93
+ readonly velocity: Float32Array;
94
+ /** Inverse mass per particle. Zero pins it. */
95
+ readonly invMass: Float32Array;
96
+ readonly count: number;
97
+ /** Two particle indices per constraint. */
98
+ private readonly link;
99
+ private readonly rest;
100
+ /** Compliance per constraint, so stretch and bend can differ. */
101
+ private readonly compliance;
102
+ private readonly previous;
103
+ private readonly lambda;
104
+ private readonly linkCount;
105
+ readonly iterations: number;
106
+ readonly damping: number;
107
+ readonly thickness: number;
108
+ /** See `ClothOptions.coupling`. 0 is one-way, which is the default. */
109
+ readonly coupling: number;
110
+ /** See `ClothOptions.particleMass`. Kilograms, read only by the coupling. */
111
+ readonly particleMass: number;
112
+ /** See `ClothOptions.selfDistance`. 0 is off, which is the default. */
113
+ readonly selfDistance: number;
114
+ /**
115
+ * Who is linked to whom, as CSR: `neighbourStart[i]` to `neighbourStart[i + 1]` indexes
116
+ * `neighbour`.
117
+ *
118
+ * **Built once, because the link set never changes** and the self-collision pass asks "are these
119
+ * two linked" once per candidate pair per tick. A `Set` of packed pairs would answer the same
120
+ * question with a hash probe and an allocation at construction the size of the link list; a
121
+ * sorted adjacency answers it with a scan over the eight or so neighbours a grid particle has,
122
+ * which is shorter than the probe.
123
+ *
124
+ * Empty when `selfDistance` is 0, so a sheet that does not collide with itself pays neither the
125
+ * memory nor the build.
126
+ */
127
+ private readonly neighbourStart;
128
+ private readonly neighbour;
129
+ /**
130
+ * The spatial hash the self-collision pass rebuilds each tick: a cell list over the particles.
131
+ *
132
+ * **A hash rather than a grid, because a sheet has no bounds worth allocating for.** A curtain
133
+ * hanging in a room occupies a sliver of it, and a dense grid over the room's extent would be
134
+ * mostly empty cells rebuilt every tick. The table is sized once from the particle count and
135
+ * reused; `cellHead` and `cellNext` are the standard two arrays, so a rebuild is one pass that
136
+ * writes and allocates nothing.
137
+ */
138
+ private readonly cellHead;
139
+ private readonly cellNext;
140
+ gravityX: number;
141
+ gravityY: number;
142
+ gravityZ: number;
143
+ constructor(positions: Float32Array, links: Int32Array, bendLinks: Int32Array, options?: ClothOptions);
144
+ /** Pin a particle where it is, or release it. */
145
+ pin(index: number, pinned?: boolean): void;
146
+ /**
147
+ * Advance one fixed tick.
148
+ *
149
+ * Integrate, project every constraint `iterations` times, then derive velocity from how far each
150
+ * particle actually moved — which is what makes it position-based: a constraint that moves a
151
+ * particle also, by construction, changes its velocity, so nothing has to reconcile the two.
152
+ */
153
+ step(world: PhysicsWorld | null, dt: number): void;
154
+ /**
155
+ * One distance constraint, with its compliance.
156
+ *
157
+ * `α = compliance / h²` is what makes the stiffness independent of the step and of the iteration
158
+ * count. At zero compliance this reduces exactly to an ordinary position-based projection, which
159
+ * is why an inextensible sheet costs nothing extra.
160
+ */
161
+ private project;
162
+ /**
163
+ * Keep the sheet out of itself: a spatial hash over the particles, and a symmetric push apart.
164
+ *
165
+ * **The hash is rebuilt every tick and that is the cheap half.** One pass over the particles
166
+ * writing two integers each, against a structure that tried to be incremental and would have to
167
+ * know which particles moved — which in a cloth solver is all of them.
168
+ *
169
+ * The cell size is the self-collision distance, so a particle's neighbours are in its own cell
170
+ * and the twenty-six around it. **The 3x3x3 scan is what makes the answer complete**: a pair at
171
+ * the distance exactly cannot be in cells more than one apart, and a scan of the own cell alone
172
+ * would miss every pair straddling a boundary — which is most of them, since a folded sheet lies
173
+ * along one.
174
+ *
175
+ * **Each pair is resolved once, by index order.** Visiting both halves would apply the
176
+ * correction twice and turn a resting fold into a buzzing one; the `other > i` test is what
177
+ * makes the pass a sweep over pairs rather than over particles.
178
+ *
179
+ * A projection rather than a constraint with compliance, unlike the distance links: a contact is
180
+ * an inequality — it does nothing until it is violated — and XPBD's accumulated multiplier is
181
+ * for a constraint that holds all tick. *What that gives up* is that the stiffness of a fold is
182
+ * the iteration count's again, for this one term. *What would make it wrong* is a consumer
183
+ * wanting a fold to have a measurable resistance, at which point it needs its own multiplier
184
+ * array and a sign clamp on it.
185
+ */
186
+ private collideSelf;
187
+ /** Whether a link joins these two. A scan over the eight or so a grid particle has. */
188
+ private linked;
189
+ /** Push two particles apart to exactly `radius`, split by their inverse masses. */
190
+ private separate;
191
+ /**
192
+ * Push every particle out of anything it has entered, and hand the body back what it took.
193
+ *
194
+ * **The impulse is derived from the correction rather than measured**, which is what makes the
195
+ * exchange cost one multiply: the constraint solve has already decided where the particle would
196
+ * be, the push says where it must be instead, and the difference over the tick is the velocity
197
+ * the collision removed from it. `m·Δ/h` is that momentum, and the body gets its negative at the
198
+ * point of contact — a push straight down on a plank's end tips the plank, because
199
+ * `applyImpulse` takes the point.
200
+ *
201
+ * A static body is skipped for the impulse rather than for the push: scenery does not move, and
202
+ * `applyImpulse` would return without doing anything anyway.
203
+ */
204
+ private collideParticles;
205
+ }
206
+ /**
207
+ * A rectangular sheet: positions, its stretch links, and the bend links across them.
208
+ *
209
+ * Bend links skip a particle rather than joining diagonals, because a diagonal resists *shear* and a
210
+ * skip resists *bowing*, and a sheet that cannot shear drapes like cardboard.
211
+ *
212
+ * **A skip-one distance constraint resists curvature and not orientation**, which is worth knowing
213
+ * before tuning it: three collinear particles satisfy it at any angle, so a flat sheet and a sheet
214
+ * hanging straight down are equally happy. Measured — a cantilever pinned along one edge settles at
215
+ * exactly vertical whether the bend compliance is 1e-8 or 1e6. What it *does* buy is 12.5
216
+ * millimetres less bow on a strip pinned at both ends with slack.
217
+ *
218
+ * **What would make it wrong** is a consumer wanting a cloak to stand out from a shoulder, and the
219
+ * answer then is a dihedral-angle constraint over the two triangles either side of an edge, which
220
+ * knows a rest angle and therefore knows the difference between flat and folded.
221
+ */
222
+ export declare function makeClothGrid(columns: number, rows: number, spacing: number): {
223
+ positions: Float32Array;
224
+ links: Int32Array;
225
+ bendLinks: Int32Array;
226
+ };