@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/mass.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ import type { ConvexShape } from './shape.ts';
2
+ /**
3
+ * A shape's volume, centre of mass and inertia tensor, at a given density.
4
+ *
5
+ * **Exact for a hull, by tetrahedral decomposition over its face loops** — which is why this sits
6
+ * immediately beside the loops themselves: one build-time addition to `ConvexShape` pays for both
7
+ * contact clipping and this, and neither is a separate piece of work.
8
+ *
9
+ * A rounded hull — `radius > 0` with faces — is the Minkowski sum of a hull and a ball, which has
10
+ * no closed form. Sphere and capsule are handled by the ball case below; anything else with a
11
+ * radius is approximated by the hull's tensor about the same centre. **What that gives up** is
12
+ * accuracy for a heavily rounded hull, where the shell is a large fraction of the volume. **What
13
+ * would make it wrong** is a consumer building such a shape and caring how it tumbles, at which
14
+ * point the answer is a numerical integration over the support function rather than a fudge factor.
15
+ *
16
+ * Build-time only, like everything else in `shape.ts`: nothing here may be called per tick.
17
+ */
18
+ export interface MassProperties {
19
+ volume: number;
20
+ comX: number;
21
+ comY: number;
22
+ comZ: number;
23
+ /** The tensor about the centre of mass. Six independent terms, symmetric. */
24
+ ixx: number;
25
+ iyy: number;
26
+ izz: number;
27
+ ixy: number;
28
+ ixz: number;
29
+ iyz: number;
30
+ }
31
+ /** A target for `shapeMassProperties`, so nothing allocates per call. */
32
+ export declare function createMassProperties(): MassProperties;
33
+ export declare function shapeMassProperties(shape: ConvexShape, density: number, out: MassProperties): MassProperties;
34
+ /**
35
+ * Several shapes as one body: volumes added, centre of mass weighted, tensors moved and summed.
36
+ *
37
+ * **What a compound body weighs, and it is the parallel axis theorem doing the work.** Each part's
38
+ * tensor is about that part's own centre of mass; the body's is about the body's. Moving one costs
39
+ * `m (|d|² δ - d ⊗ d)` where `d` is the offset between the two centres, and skipping that term is
40
+ * not a small error — an L-beam released at rest would rotate, because its inertia would no longer
41
+ * match its shape.
42
+ *
43
+ * **Overlap is counted twice, and that is the decision rather than an oversight.** A decomposition's
44
+ * hulls bulge slightly into each other, so the volumes here sum to a little more than the solid they
45
+ * stand for — measured at 27% against 16% occupied on a torus, and 5% against 2% on a cup. Removing
46
+ * it exactly means a boolean intersection of convex solids, which is a large body of code whose
47
+ * output corrects a number the caller invented when they chose a density.
48
+ * `Decomposition.hullVolume` reports the sum, so a caller who cares can divide it out.
49
+ *
50
+ * The shapes are read twice: once for the centre and once for the tensor about it. That is a bake's
51
+ * cost paid at `addBody`, not a tick's.
52
+ */
53
+ export declare function combineMassProperties(parts: readonly ConvexShape[], density: number, out: MassProperties): MassProperties;
package/dist/mass.js ADDED
@@ -0,0 +1,353 @@
1
+ import { faceCount, faceVertices } from './faces.js';
2
+ /** A target for `shapeMassProperties`, so nothing allocates per call. */
3
+ export function createMassProperties() {
4
+ return { volume: 0, comX: 0, comY: 0, comZ: 0, ixx: 0, iyy: 0, izz: 0, ixy: 0, ixz: 0, iyz: 0 };
5
+ }
6
+ /** Scratch for one face's vertex loop. `MAX_POINTS` caps a hull at 64, so a face cannot exceed it. */
7
+ const FACE = new Uint16Array(64);
8
+ export function shapeMassProperties(shape, density, out) {
9
+ const faces = faceCount(shape);
10
+ /*
11
+ * **A triangle mesh has no mass properties and answers zero rather than guessing.** A surface
12
+ * encloses no volume, and a soup of them may not even be closed; the bounding box's tensor would
13
+ * be a plausible number for a body that `addBody` refuses to make dynamic anyway. Zero volume is
14
+ * what a static body's own inverse mass already is.
15
+ */
16
+ if (shape.triangles !== undefined) {
17
+ out.volume = 0;
18
+ out.comX = 0;
19
+ out.comY = 0;
20
+ out.comZ = 0;
21
+ out.ixx = 0;
22
+ out.iyy = 0;
23
+ out.izz = 0;
24
+ out.ixy = 0;
25
+ out.ixz = 0;
26
+ out.iyz = 0;
27
+ return out;
28
+ }
29
+ // A disc-rounded shape is a cylinder and has its own closed form, which is exact rather than a
30
+ // decomposition — so it is tested before the faceless path, whose arithmetic assumes a ball.
31
+ if (shape.sideRadius > 0)
32
+ return cylinderProperties(shape, density, out);
33
+ if (faces === 0)
34
+ return ballProperties(shape, density, out);
35
+ let volume = 0;
36
+ let centroidX = 0;
37
+ let centroidY = 0;
38
+ let centroidZ = 0;
39
+ // Second moments about the origin, accumulated per tetrahedron and shifted at the end.
40
+ let xx = 0;
41
+ let yy = 0;
42
+ let zz = 0;
43
+ let xy = 0;
44
+ let xz = 0;
45
+ let yz = 0;
46
+ for (let face = 0; face < faces; face++) {
47
+ const n = faceVertices(shape, face, FACE);
48
+ const a = (FACE[0] ?? 0) * 3;
49
+ const ax = shape.vertices[a] ?? 0;
50
+ const ay = shape.vertices[a + 1] ?? 0;
51
+ const az = shape.vertices[a + 2] ?? 0;
52
+ // Fan the face into triangles, each closing a tetrahedron against the origin.
53
+ for (let i = 1; i + 1 < n; i++) {
54
+ const b = (FACE[i] ?? 0) * 3;
55
+ const c = (FACE[i + 1] ?? 0) * 3;
56
+ const bx = shape.vertices[b] ?? 0;
57
+ const by = shape.vertices[b + 1] ?? 0;
58
+ const bz = shape.vertices[b + 2] ?? 0;
59
+ const cx = shape.vertices[c] ?? 0;
60
+ const cy = shape.vertices[c + 1] ?? 0;
61
+ const cz = shape.vertices[c + 2] ?? 0;
62
+ // Six times the signed tetrahedron volume: the scalar triple product.
63
+ const det = ax * (by * cz - bz * cy) - ay * (bx * cz - bz * cx) + az * (bx * cy - by * cx);
64
+ const v = det / 6;
65
+ volume += v;
66
+ // Centroid of a tetrahedron with one vertex at the origin.
67
+ centroidX += (v * (ax + bx + cx)) / 4;
68
+ centroidY += (v * (ay + by + cy)) / 4;
69
+ centroidZ += (v * (az + bz + cz)) / 4;
70
+ /*
71
+ * Second moments of a tetrahedron about the origin, in closed form. The diagonal integral
72
+ * reduces to det/60 times the sum of squares and pairwise products of the three vertex
73
+ * coordinates; the products use det/120 with the mixed terms. Both are exact rather than
74
+ * quadrature, which is what makes the box test below a hand-derived literal rather than a
75
+ * tolerance chosen to fit.
76
+ */
77
+ xx += (det / 60) * (ax * ax + bx * bx + cx * cx + ax * bx + ax * cx + bx * cx);
78
+ yy += (det / 60) * (ay * ay + by * by + cy * cy + ay * by + ay * cy + by * cy);
79
+ zz += (det / 60) * (az * az + bz * bz + cz * cz + az * bz + az * cz + bz * cz);
80
+ xy +=
81
+ (det / 120) *
82
+ (2 * ax * ay +
83
+ 2 * bx * by +
84
+ 2 * cx * cy +
85
+ ax * by +
86
+ ay * bx +
87
+ ax * cy +
88
+ ay * cx +
89
+ bx * cy +
90
+ by * cx);
91
+ xz +=
92
+ (det / 120) *
93
+ (2 * ax * az +
94
+ 2 * bx * bz +
95
+ 2 * cx * cz +
96
+ ax * bz +
97
+ az * bx +
98
+ ax * cz +
99
+ az * cx +
100
+ bx * cz +
101
+ bz * cx);
102
+ yz +=
103
+ (det / 120) *
104
+ (2 * ay * az +
105
+ 2 * by * bz +
106
+ 2 * cy * cz +
107
+ ay * bz +
108
+ az * by +
109
+ ay * cz +
110
+ az * cy +
111
+ by * cz +
112
+ bz * cy);
113
+ }
114
+ }
115
+ const mass = volume * density;
116
+ const comX = volume === 0 ? 0 : centroidX / volume;
117
+ const comY = volume === 0 ? 0 : centroidY / volume;
118
+ const comZ = volume === 0 ? 0 : centroidZ / volume;
119
+ out.volume = volume;
120
+ out.comX = comX;
121
+ out.comY = comY;
122
+ out.comZ = comZ;
123
+ // Parallel-axis shift from the origin to the centre of mass.
124
+ out.ixx = density * (yy + zz) - mass * (comY * comY + comZ * comZ);
125
+ out.iyy = density * (xx + zz) - mass * (comX * comX + comZ * comZ);
126
+ out.izz = density * (xx + yy) - mass * (comX * comX + comY * comY);
127
+ out.ixy = -(density * xy) + mass * comX * comY;
128
+ out.ixz = -(density * xz) + mass * comX * comZ;
129
+ out.iyz = -(density * yz) + mass * comY * comZ;
130
+ return out;
131
+ }
132
+ /**
133
+ * Several shapes as one body: volumes added, centre of mass weighted, tensors moved and summed.
134
+ *
135
+ * **What a compound body weighs, and it is the parallel axis theorem doing the work.** Each part's
136
+ * tensor is about that part's own centre of mass; the body's is about the body's. Moving one costs
137
+ * `m (|d|² δ - d ⊗ d)` where `d` is the offset between the two centres, and skipping that term is
138
+ * not a small error — an L-beam released at rest would rotate, because its inertia would no longer
139
+ * match its shape.
140
+ *
141
+ * **Overlap is counted twice, and that is the decision rather than an oversight.** A decomposition's
142
+ * hulls bulge slightly into each other, so the volumes here sum to a little more than the solid they
143
+ * stand for — measured at 27% against 16% occupied on a torus, and 5% against 2% on a cup. Removing
144
+ * it exactly means a boolean intersection of convex solids, which is a large body of code whose
145
+ * output corrects a number the caller invented when they chose a density.
146
+ * `Decomposition.hullVolume` reports the sum, so a caller who cares can divide it out.
147
+ *
148
+ * The shapes are read twice: once for the centre and once for the tensor about it. That is a bake's
149
+ * cost paid at `addBody`, not a tick's.
150
+ */
151
+ export function combineMassProperties(parts, density, out) {
152
+ let volume = 0;
153
+ let comX = 0;
154
+ let comY = 0;
155
+ let comZ = 0;
156
+ for (const part of parts) {
157
+ const m = shapeMassProperties(part, density, COMBINE_SCRATCH);
158
+ volume += m.volume;
159
+ comX += m.volume * m.comX;
160
+ comY += m.volume * m.comY;
161
+ comZ += m.volume * m.comZ;
162
+ }
163
+ if (volume > 0) {
164
+ comX /= volume;
165
+ comY /= volume;
166
+ comZ /= volume;
167
+ }
168
+ else {
169
+ comX = 0;
170
+ comY = 0;
171
+ comZ = 0;
172
+ }
173
+ let ixx = 0;
174
+ let iyy = 0;
175
+ let izz = 0;
176
+ let ixy = 0;
177
+ let ixz = 0;
178
+ let iyz = 0;
179
+ for (const part of parts) {
180
+ const m = shapeMassProperties(part, density, COMBINE_SCRATCH);
181
+ const mass = m.volume * density;
182
+ const dx = m.comX - comX;
183
+ const dy = m.comY - comY;
184
+ const dz = m.comZ - comZ;
185
+ ixx += m.ixx + mass * (dy * dy + dz * dz);
186
+ iyy += m.iyy + mass * (dx * dx + dz * dz);
187
+ izz += m.izz + mass * (dx * dx + dy * dy);
188
+ ixy += m.ixy - mass * dx * dy;
189
+ ixz += m.ixz - mass * dx * dz;
190
+ iyz += m.iyz - mass * dy * dz;
191
+ }
192
+ out.volume = volume;
193
+ out.comX = comX;
194
+ out.comY = comY;
195
+ out.comZ = comZ;
196
+ out.ixx = ixx;
197
+ out.iyy = iyy;
198
+ out.izz = izz;
199
+ out.ixy = ixy;
200
+ out.ixz = ixz;
201
+ out.iyz = iyz;
202
+ return out;
203
+ }
204
+ /** Reused by `combineMassProperties`, which is called once per body rather than once per tick. */
205
+ const COMBINE_SCRATCH = createMassProperties();
206
+ /**
207
+ * A cylinder: `sideRadius` about the segment between the shape's two points.
208
+ *
209
+ * Exact and in closed form, like the hull path and unlike the capsule's, which has to sum a
210
+ * cylinder and two hemispheres. `m r² / 2` down the axis and `m (3r² + L²) / 12` across it, where
211
+ * `L` is the full length — the two textbook results, hand-derived in the test rather than checked
212
+ * against this code.
213
+ *
214
+ * **The tensor is built about the axis and then rotated into place**, the same last step
215
+ * `capsuleProperties` takes and for the same reason: a cylinder baked into world space by a
216
+ * collider has an arbitrary axis, and a tensor written in the shape's own frame would be wrong
217
+ * for it in a way nothing throws about.
218
+ *
219
+ * A shape carrying both roundings — a cylinder with a filleted rim — is treated as the plain
220
+ * cylinder its two radii describe at the fillet's outer extent. **What that gives up** is the
221
+ * fillet's own volume, a few per cent at a small radius; **what would make it wrong** is a
222
+ * consumer filleting a rim by a large fraction of the shape, where the answer is the numerical
223
+ * integration §4a already names.
224
+ */
225
+ function cylinderProperties(shape, density, out) {
226
+ const r = shape.sideRadius + shape.radius;
227
+ const ax = (shape.vertices[3] ?? 0) - (shape.vertices[0] ?? 0);
228
+ const ay = (shape.vertices[4] ?? 0) - (shape.vertices[1] ?? 0);
229
+ const az = (shape.vertices[5] ?? 0) - (shape.vertices[2] ?? 0);
230
+ const length = Math.sqrt(ax * ax + ay * ay + az * az) + 2 * shape.radius;
231
+ out.comX = ((shape.vertices[0] ?? 0) + (shape.vertices[3] ?? 0)) / 2;
232
+ out.comY = ((shape.vertices[1] ?? 0) + (shape.vertices[4] ?? 0)) / 2;
233
+ out.comZ = ((shape.vertices[2] ?? 0) + (shape.vertices[5] ?? 0)) / 2;
234
+ out.ixy = 0;
235
+ out.ixz = 0;
236
+ out.iyz = 0;
237
+ const volume = Math.PI * r * r * length;
238
+ const mass = volume * density;
239
+ out.volume = volume;
240
+ const along = (mass * r * r) / 2;
241
+ const across = (mass * (3 * r * r + length * length)) / 12;
242
+ if (length === 0) {
243
+ out.ixx = along;
244
+ out.iyy = along;
245
+ out.izz = along;
246
+ return out;
247
+ }
248
+ const segment = Math.sqrt(ax * ax + ay * ay + az * az) || 1;
249
+ const ux = ax / segment;
250
+ const uy = ay / segment;
251
+ const uz = az / segment;
252
+ const d = along - across;
253
+ out.ixx = across + d * ux * ux;
254
+ out.iyy = across + d * uy * uy;
255
+ out.izz = across + d * uz * uz;
256
+ out.ixy = d * ux * uy;
257
+ out.ixz = d * ux * uz;
258
+ out.iyz = d * uy * uz;
259
+ return out;
260
+ }
261
+ /**
262
+ * A shape with no faces is a ball or a capsule: all of its volume is the rounding radius.
263
+ *
264
+ * Which one is decided by how many points survived `hullShape`'s dedupe, and that is the whole
265
+ * distinction: one point is a sphere, two are a capsule along the segment between them.
266
+ *
267
+ * `Math.PI` is a constant rather than a call, so the determinism gate has nothing to say about it.
268
+ */
269
+ function ballProperties(shape, density, out) {
270
+ const count = shape.vertices.length / 3;
271
+ let cx = 0;
272
+ let cy = 0;
273
+ let cz = 0;
274
+ for (let i = 0; i < count; i++) {
275
+ cx += shape.vertices[i * 3] ?? 0;
276
+ cy += shape.vertices[i * 3 + 1] ?? 0;
277
+ cz += shape.vertices[i * 3 + 2] ?? 0;
278
+ }
279
+ const n = count === 0 ? 1 : count;
280
+ out.comX = cx / n;
281
+ out.comY = cy / n;
282
+ out.comZ = cz / n;
283
+ out.ixy = 0;
284
+ out.ixz = 0;
285
+ out.iyz = 0;
286
+ const r = shape.radius;
287
+ if (count !== 2) {
288
+ const volume = (4 / 3) * Math.PI * r * r * r;
289
+ const inertia = 0.4 * volume * density * r * r;
290
+ out.volume = volume;
291
+ out.ixx = inertia;
292
+ out.iyy = inertia;
293
+ out.izz = inertia;
294
+ return out;
295
+ }
296
+ return capsuleProperties(shape, density, out);
297
+ }
298
+ /**
299
+ * A capsule: a cylinder of length 2h capped by two hemispheres of radius r.
300
+ *
301
+ * **This replaces the sphere tensor the previous version returned for any faceless shape**, which
302
+ * was recorded as wrong where it stood: a wrong inertia does not throw, it produces a body that
303
+ * tumbles unconvincingly, and that reads as a solver bug and gets diagnosed as one.
304
+ *
305
+ * The axis is the segment between the two points, and the tensor is built about it and then
306
+ * rotated into place. **What this gives up:** the arithmetic below assumes a capsule, so a
307
+ * two-point cloud with a radius small enough that the points are effectively a line segment is
308
+ * still treated as a capsule, which it is. **What would make it wrong** is a three-point flat
309
+ * cloud with a radius, which has no closed form and falls to the hull path above instead, where
310
+ * §4a's Minkowski caveat applies.
311
+ */
312
+ function capsuleProperties(shape, density, out) {
313
+ const r = shape.radius;
314
+ const ax = (shape.vertices[3] ?? 0) - (shape.vertices[0] ?? 0);
315
+ const ay = (shape.vertices[4] ?? 0) - (shape.vertices[1] ?? 0);
316
+ const az = (shape.vertices[5] ?? 0) - (shape.vertices[2] ?? 0);
317
+ const length = Math.sqrt(ax * ax + ay * ay + az * az);
318
+ const h = length / 2;
319
+ const cylinder = 2 * Math.PI * r * r * h * density;
320
+ const cap = ((2 * Math.PI * r * r * r) / 3) * density;
321
+ out.volume = (cylinder + 2 * cap) / density;
322
+ // About the capsule's own long axis.
323
+ const along = (cylinder * r * r) / 2 + 2 * cap * ((2 * r * r) / 5);
324
+ /*
325
+ * Perpendicular to it. The cylinder term is standard; each hemisphere contributes its own
326
+ * 2mr²/5 about its centre plus the parallel-axis shift to the capsule's centre, whose distance
327
+ * terms reduce to h² + 3hr/4 because a hemisphere's centre of mass sits 3r/8 from its flat face.
328
+ */
329
+ const across = (cylinder * (3 * r * r + 4 * h * h)) / 12 +
330
+ 2 * cap * ((2 * r * r) / 5 + h * h + (3 * h * r) / 4);
331
+ if (length === 0) {
332
+ out.ixx = along;
333
+ out.iyy = along;
334
+ out.izz = along;
335
+ return out;
336
+ }
337
+ /*
338
+ * The tensor is `across` in every direction perpendicular to the axis and `along` down it, which
339
+ * is `across·I + (along − across)·(a ⊗ a)` for the unit axis `a`. Written out rather than looped
340
+ * because six terms unrolled read better than a 3×3 with two of nine entries unused.
341
+ */
342
+ const ux = ax / length;
343
+ const uy = ay / length;
344
+ const uz = az / length;
345
+ const d = along - across;
346
+ out.ixx = across + d * ux * ux;
347
+ out.iyy = across + d * uy * uy;
348
+ out.izz = across + d * uz * uz;
349
+ out.ixy = d * ux * uy;
350
+ out.ixz = d * ux * uz;
351
+ out.iyz = d * uy * uz;
352
+ return out;
353
+ }
@@ -0,0 +1,57 @@
1
+ import type { Manifold, ShapePose } from './manifold.ts';
2
+ import type { ConvexShape } from './shape.ts';
3
+ /**
4
+ * Contacts between a convex shape and a static triangle mesh.
5
+ *
6
+ * **Many manifolds, not one, and that is the whole structural difference.** Every other pair in
7
+ * this engine has a single contact plane; a body in the corner of a mesh room has two or three, and
8
+ * reducing them to one would pick a wall and let the body through the others. So this fills a set,
9
+ * and `PhysicsWorld` prepares a constraint per entry — which the solver already supports, because
10
+ * `prepareContact` appends rather than replaces.
11
+ *
12
+ * **Each triangle goes through `collideShapes` unchanged**, as a three-point polytope written into
13
+ * one scratch shape. That is the reuse that makes this small: the separating-axis test, the
14
+ * clipping, the sphere and capsule paths and the cylinder's own module are all exact against a
15
+ * triangle already, and a second implementation of any of them would be a second thing to be wrong.
16
+ * *What it costs* is that the moving shape is transformed into world space once per candidate
17
+ * triangle rather than once per pair — eight vertices times twenty candidates for a box on a floor.
18
+ * *What would make it wrong* is a consumer resting a 64-point hull on a dense mesh, where the
19
+ * answer is to hoist the transform out of `collideShapes` rather than to write a second narrow
20
+ * phase.
21
+ *
22
+ * **Two sources of triangles and one set of rules.** A level's triangles are stored and found
23
+ * through a tree; a heightfield's are generated from four samples and found by index. Everything
24
+ * below is written once for both — the one-sided rule, the many manifolds, the interior-edge
25
+ * filter — and the only thing that differs is where a triangle's corners, plane and edge
26
+ * classification come from. `heightfieldShape.ts` is the other source, and the split is the
27
+ * 2026-08-13 rule applied to the one file here it would be most expensive to have two of.
28
+ *
29
+ * **One-sided.** A contact whose normal points into the back of a triangle is dropped. A level mesh
30
+ * is a surface rather than a solid, so "which side" is the only thing that distinguishes standing
31
+ * on a floor from being pushed up through it, and a two-sided triangle catches anything that has
32
+ * already sunk below it and fires it out the wrong way.
33
+ *
34
+ * **And the interior-edge filter, which is the part that decides whether it feels right.** A box
35
+ * sliding across two coplanar triangles meets their shared edge, where the narrow phase finds an
36
+ * edge normal rather than the face's and shoves the box backwards along it — a character stumbling
37
+ * on a flat floor once a metre. `meshShape` classifies every edge once as convex or not; a normal
38
+ * that came off a non-convex edge is replaced with the face's, and the separation is recomputed
39
+ * from the face plane. *What that gives up:* a genuinely convex ridge still produces its edge
40
+ * normal, which is correct, so a consumer who wants a ridge softened has to bevel it.
41
+ */
42
+ /** How many contact planes one pair may produce. A body in a corner needs three; eight is generous. */
43
+ export declare const MAX_MESH_MANIFOLDS = 8;
44
+ /**
45
+ * Whether a shape is a static triangle mesh, which is the one question the dispatch asks.
46
+ *
47
+ * Takes an absent shape as *not* a mesh, because a compound body has no single shape and the
48
+ * dispatch asks about both sides before it knows which kind of pair it has.
49
+ */
50
+ export declare function isMesh(shape: ConvexShape | undefined): boolean;
51
+ /**
52
+ * Fill `out` with up to `out.length` manifolds and return how many.
53
+ *
54
+ * `out` is the caller's, allocated once — this runs in the tick. The normal of each manifold points
55
+ * from `shapeA` to `shapeB`, exactly as `collideShapes` promises, whichever of the two is the mesh.
56
+ */
57
+ export declare function collideMesh(shapeA: ConvexShape, poseA: ShapePose, shapeB: ConvexShape, poseB: ShapePose, margin: number, out: readonly Manifold[]): number;