@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/shape.js ADDED
@@ -0,0 +1,533 @@
1
+ /** What a shape with no faces carries: a sphere, a capsule, or any cloud too flat to bound one. */
2
+ const EMPTY_FACES = {
3
+ facePlanes: new Float32Array(0),
4
+ faceVertexStart: new Uint16Array(0),
5
+ faceVertexIndices: new Uint16Array(0),
6
+ };
7
+ /**
8
+ * Hard cap on hull input size. The builder is O(n^4); at 64 points that is a
9
+ * few million build-time operations, and anything larger is a sign the caller
10
+ * is feeding it a render mesh instead of an element's defining points.
11
+ */
12
+ const MAX_POINTS = 64;
13
+ /** Points closer than this are the same point. */
14
+ const POINT_EPS = 1e-6;
15
+ /** Distance within which a point counts as lying on a plane, metres. */
16
+ const COPLANAR_M = 1e-4;
17
+ /** Dot product above which two directions are the same line. */
18
+ const DIR_DEDUP = 1 - 1e-7;
19
+ /** The exact shape `MeshBuilder.addBox` draws: eight corners, three axes. */
20
+ export function boxShape(hx, hy, hz) {
21
+ const vertices = new Float32Array(24);
22
+ let v = 0;
23
+ for (let i = 0; i < 8; i++) {
24
+ vertices[v++] = i & 1 ? hx : -hx;
25
+ vertices[v++] = i & 2 ? hy : -hy;
26
+ vertices[v++] = i & 4 ? hz : -hz;
27
+ }
28
+ const axes = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
29
+ /*
30
+ * Six faces, where `axes` above is three directions. Corner `i` has bit 0 for +x, bit 1 for +y
31
+ * and bit 2 for +z, so each face is the four corners sharing one bit, listed counter-clockwise
32
+ * seen from outside. Written out rather than derived because the loop that derives it is longer
33
+ * than the table and this is build-time code that runs once per shape.
34
+ */
35
+ const facePlanes = new Float32Array([
36
+ 1,
37
+ 0,
38
+ 0,
39
+ hx,
40
+ -1,
41
+ 0,
42
+ 0,
43
+ hx,
44
+ 0,
45
+ 1,
46
+ 0,
47
+ hy,
48
+ 0,
49
+ -1,
50
+ 0,
51
+ hy,
52
+ 0,
53
+ 0,
54
+ 1,
55
+ hz,
56
+ 0,
57
+ 0,
58
+ -1,
59
+ hz,
60
+ ]);
61
+ const faceVertexIndices = new Uint16Array([
62
+ 1, 3, 7, 5, 4, 6, 2, 0, 2, 6, 7, 3, 0, 1, 5, 4, 4, 5, 7, 6, 0, 2, 3, 1,
63
+ ]);
64
+ const faceVertexStart = new Uint16Array([0, 4, 8, 12, 16, 20, 24]);
65
+ return {
66
+ vertices,
67
+ faceNormals: axes,
68
+ edgeDirs: axes,
69
+ radius: 0,
70
+ sideRadius: 0,
71
+ boundRadius: Math.sqrt(hx * hx + hy * hy + hz * hz),
72
+ facePlanes,
73
+ faceVertexStart,
74
+ faceVertexIndices,
75
+ };
76
+ }
77
+ /** A ball: one point, all radius. It separates on other shapes' features. */
78
+ export function sphereShape(radius) {
79
+ return {
80
+ vertices: new Float32Array(3),
81
+ faceNormals: new Float32Array(0),
82
+ edgeDirs: new Float32Array(0),
83
+ radius,
84
+ sideRadius: 0,
85
+ boundRadius: radius,
86
+ ...EMPTY_FACES,
87
+ };
88
+ }
89
+ /**
90
+ * A capsule: a segment along y with a rounding radius.
91
+ *
92
+ * **No new representation.** Point-cloud-plus-radius already expresses a capsule exactly, so this
93
+ * is a named constructor over `hullShape` rather than a shape type. A named thing is discoverable
94
+ * and an idiom is not. It stands along y because that is the axis a character capsule uses, and a
95
+ * rotation nobody has to apply is a rotation nobody gets wrong.
96
+ *
97
+ * At `halfHeight` zero the two points dedupe to one and the result *is* a sphere, which is the
98
+ * right degenerate case rather than an error.
99
+ *
100
+ * **A cylinder is a different shape and has its own constructor**, `cylinderShape` below. The
101
+ * rounding radius here grows uniformly around every feature, so applying it to a segment can only
102
+ * ever give a capsule: it would round a cylinder's rim as well as its side. `sideRadius` is the
103
+ * field that expresses the difference, and it was added on 2026-08-27 with the second condition
104
+ * this file had been recording — a wheel rim, a rolling barrel — as its reason.
105
+ *
106
+ * The advice until then was "build one as an n-gon prism with `hullShape` and accept the facets",
107
+ * and it had a measured ceiling: `hullShape` refuses more than 64 points, a prism spends two a
108
+ * side, so thirty-two sides was the wall, and a thirty-two-sided wheel of radius 1 rolling at
109
+ * 6 m/s bobbed 16 mm that no side count could reduce. `prismRoll.test.ts` still measures that,
110
+ * beside the same wheel built as a cylinder, because the comparison is the reason the shape kind
111
+ * exists.
112
+ */
113
+ export function capsuleShape(radius, halfHeight) {
114
+ if (!(radius > 0))
115
+ throw new Error(`capsuleShape: radius must be positive, got ${radius}`);
116
+ if (!(halfHeight >= 0)) {
117
+ throw new Error(`capsuleShape: halfHeight must not be negative, got ${halfHeight}`);
118
+ }
119
+ return hullShape([0, -halfHeight, 0, 0, halfHeight, 0], radius);
120
+ }
121
+ /**
122
+ * A cylinder: a segment along y, grown by a disc rather than by a ball.
123
+ *
124
+ * **The one shape a point cloud plus a rounding radius cannot express**, and the reason it needed
125
+ * a second kind of rounding rather than a second shape type. `radius` grows a shape uniformly, so
126
+ * applying it to a segment gives a capsule; `sideRadius` grows it only perpendicular to that
127
+ * segment, which gives two flat cap discs, a curved side, and a sharp rim between them. That rim
128
+ * is the whole difference, and it is what a wheel rolls on.
129
+ *
130
+ * **What this replaces is an n-gon prism with a measured floor.** `prismRoll.test.ts` had the
131
+ * numbers: `hullShape` refuses more than 64 points, a prism spends two a side, so thirty-two sides
132
+ * was the wall, and a thirty-two-sided wheel of radius 1 rolling at 6 m/s bobbed **16 mm** — which
133
+ * no side count could reduce. A cylinder's support along any direction perpendicular to its axis
134
+ * is exactly `sideRadius`, at every angle, so the same wheel bobs by nothing at all. The same test
135
+ * file now measures both and compares them.
136
+ *
137
+ * It stands along y for `capsuleShape`'s reason: a rotation nobody has to apply is a rotation
138
+ * nobody gets wrong. A wheel wants its axis across the vehicle, and the body carries a quaternion
139
+ * for exactly that. A static collider baked into world space carries its axis in its own two
140
+ * points, so nothing stores a direction that could disagree with them.
141
+ *
142
+ * **`halfHeight` must be positive**, unlike `capsuleShape`, whose zero case degenerates into a
143
+ * sphere and is right. A cylinder of zero height is a disc: a shape with no interior, whose mass
144
+ * properties are zero and whose contact normal is undefined on its rim. Refused rather than
145
+ * silently produced.
146
+ */
147
+ export function cylinderShape(radius, halfHeight) {
148
+ if (!(radius > 0))
149
+ throw new Error(`cylinderShape: radius must be positive, got ${radius}`);
150
+ if (!(halfHeight > 0)) {
151
+ throw new Error(`cylinderShape: halfHeight must be positive, got ${halfHeight}`);
152
+ }
153
+ /*
154
+ * One axis in `faceNormals` and in `edgeDirs`, which is the honest answer for both: the caps are
155
+ * the only planar faces a cylinder has, and they share one direction, while the axis is the only
156
+ * straight edge direction on it. The curved side has neither, and the contact code reaches it
157
+ * through `sideRadius` rather than through an enumerated axis — which is the point of the field.
158
+ */
159
+ const axis = new Float32Array([0, 1, 0]);
160
+ return {
161
+ vertices: new Float32Array([0, -halfHeight, 0, 0, halfHeight, 0]),
162
+ faceNormals: axis,
163
+ edgeDirs: axis,
164
+ radius: 0,
165
+ sideRadius: radius,
166
+ boundRadius: Math.sqrt(halfHeight * halfHeight + radius * radius),
167
+ ...EMPTY_FACES,
168
+ };
169
+ }
170
+ /**
171
+ * The convex hull of a point cloud, with its separating features enumerated.
172
+ *
173
+ * Brute force by design: every point triple proposes a plane, and a plane with
174
+ * every point on one side is a face. Edges are point pairs lying on two
175
+ * distinct faces. No incremental hull, no floating-point pivoting — the same
176
+ * input yields the same features in the same order on every machine, which the
177
+ * replay contract needs from anything that feeds collision.
178
+ *
179
+ * **Every length here is `Math.sqrt` of a sum of squares rather than `Math.hypot`**, and that is
180
+ * what makes the sentence above true rather than merely intended. ECMAScript declines to specify
181
+ * `hypot` precisely, so two engines may differ by an ulp — and an ulp here is not a slightly
182
+ * different axis. `proposePlane`'s normal classifies every point against `COPLANAR_M`, so it
183
+ * decides whether a plane becomes a face at all; `appendDirection`'s is compared against
184
+ * `DIR_DEDUP`, so it decides how many separating axes exist and in what order. SAT then takes a
185
+ * minimum over that list.
186
+ *
187
+ * `hypot` is the more accurate of the two, guarding against intermediate overflow at magnitudes a
188
+ * collision shape does not reach. **What would make this wrong** is a shape built at astronomical
189
+ * or atomic scale, where that guard is worth more than the reproducibility; the answer then is an
190
+ * explicit rescale rather than the call back. Asserted by `scripts/determinism.test.mjs`.
191
+ */
192
+ export function hullShape(points, radius = 0) {
193
+ const pts = dedupePoints(points);
194
+ const n = pts.length / 3;
195
+ if (n === 0)
196
+ throw new Error('hullShape: no points');
197
+ if (n > MAX_POINTS) {
198
+ throw new Error(`hullShape: ${n} points exceeds ${MAX_POINTS} — derive shapes from defining points, not meshes`);
199
+ }
200
+ let boundRadius = 0;
201
+ for (let i = 0; i < n; i++) {
202
+ const px = pts[i * 3] ?? 0;
203
+ const py = pts[i * 3 + 1] ?? 0;
204
+ const pz = pts[i * 3 + 2] ?? 0;
205
+ const d = Math.sqrt(px * px + py * py + pz * pz);
206
+ if (d > boundRadius)
207
+ boundRadius = d;
208
+ }
209
+ boundRadius += radius;
210
+ // Every plane that supports the hull: (nx, ny, nz, offset), outward.
211
+ const planes = [];
212
+ for (let i = 0; i < n - 2; i++) {
213
+ for (let j = i + 1; j < n - 1; j++) {
214
+ for (let k = j + 1; k < n; k++) {
215
+ proposePlane(pts, n, i, j, k, planes);
216
+ }
217
+ }
218
+ }
219
+ const faceNormals = dedupeDirections(planes, 4);
220
+ const edgeDirs = planes.length >= 8
221
+ ? hullEdges(pts, n, planes)
222
+ : // Flat or degenerate cloud (one plane or none): there are no two faces
223
+ // to intersect, so every pairwise direction stands in for the rim.
224
+ pairwiseDirections(pts, n);
225
+ /*
226
+ * The planes are what `proposePlane` already found; only their offsets were being discarded. A
227
+ * face is one plane plus the points lying on it, ordered around the normal — which is what
228
+ * clipping and an inertia tensor both need, and what `faceNormals` cannot give, being deduped
229
+ * directions rather than faces.
230
+ */
231
+ const faces = planes.length >= 4 ? buildFaces(pts, n, planes) : EMPTY_FACES;
232
+ return { vertices: pts, faceNormals, edgeDirs, radius, sideRadius: 0, boundRadius, ...faces };
233
+ }
234
+ /**
235
+ * World-axis bounds of a shape as placed (its points are its placement).
236
+ *
237
+ * **`radius` and `sideRadius` grow the box differently, and the second one is not `sideRadius` on
238
+ * every axis.** A ball grows a box by its radius along all three; a disc perpendicular to a unit
239
+ * axis `a` grows it by `sideRadius * sqrt(1 - a[i]^2)` along axis `i` — the disc's own extent seen
240
+ * down that axis, which is the full radius across the axis and exactly nothing along it. Using
241
+ * `sideRadius` on all three instead would bound a lying cylinder as if it were a sphere, which is
242
+ * conservative and therefore not wrong, and would cost the broad phase the difference on every
243
+ * cylinder in the world.
244
+ */
245
+ export function shapeBounds(shape, out) {
246
+ const v = shape.vertices;
247
+ let minX = Infinity;
248
+ let minY = Infinity;
249
+ let minZ = Infinity;
250
+ let maxX = -Infinity;
251
+ let maxY = -Infinity;
252
+ let maxZ = -Infinity;
253
+ for (let i = 0; i < v.length; i += 3) {
254
+ const x = v[i] ?? 0;
255
+ const y = v[i + 1] ?? 0;
256
+ const z = v[i + 2] ?? 0;
257
+ if (x < minX)
258
+ minX = x;
259
+ if (x > maxX)
260
+ maxX = x;
261
+ if (y < minY)
262
+ minY = y;
263
+ if (y > maxY)
264
+ maxY = y;
265
+ if (z < minZ)
266
+ minZ = z;
267
+ if (z > maxZ)
268
+ maxZ = z;
269
+ }
270
+ let growX = shape.radius;
271
+ let growY = shape.radius;
272
+ let growZ = shape.radius;
273
+ if (shape.sideRadius > 0) {
274
+ const ax = (v[3] ?? 0) - (v[0] ?? 0);
275
+ const ay = (v[4] ?? 0) - (v[1] ?? 0);
276
+ const az = (v[5] ?? 0) - (v[2] ?? 0);
277
+ const length = Math.sqrt(ax * ax + ay * ay + az * az) || 1;
278
+ const ux = ax / length;
279
+ const uy = ay / length;
280
+ const uz = az / length;
281
+ growX += shape.sideRadius * Math.sqrt(Math.max(0, 1 - ux * ux));
282
+ growY += shape.sideRadius * Math.sqrt(Math.max(0, 1 - uy * uy));
283
+ growZ += shape.sideRadius * Math.sqrt(Math.max(0, 1 - uz * uz));
284
+ }
285
+ out.minX = minX - growX;
286
+ out.minY = minY - growY;
287
+ out.minZ = minZ - growZ;
288
+ out.maxX = maxX + growX;
289
+ out.maxY = maxY + growY;
290
+ out.maxZ = maxZ + growZ;
291
+ return out;
292
+ }
293
+ function dedupePoints(points) {
294
+ const kept = [];
295
+ for (let i = 0; i + 2 < points.length; i += 3) {
296
+ const x = points[i] ?? 0;
297
+ const y = points[i + 1] ?? 0;
298
+ const z = points[i + 2] ?? 0;
299
+ let duplicate = false;
300
+ for (let k = 0; k < kept.length; k += 3) {
301
+ if (Math.abs((kept[k] ?? 0) - x) < POINT_EPS &&
302
+ Math.abs((kept[k + 1] ?? 0) - y) < POINT_EPS &&
303
+ Math.abs((kept[k + 2] ?? 0) - z) < POINT_EPS) {
304
+ duplicate = true;
305
+ break;
306
+ }
307
+ }
308
+ if (!duplicate)
309
+ kept.push(x, y, z);
310
+ }
311
+ return new Float32Array(kept);
312
+ }
313
+ /** If the triple (i, j, k) spans a supporting plane, record it outward in `planes`. */
314
+ function proposePlane(pts, n, i, j, k, planes) {
315
+ const ax = pts[i * 3] ?? 0;
316
+ const ay = pts[i * 3 + 1] ?? 0;
317
+ const az = pts[i * 3 + 2] ?? 0;
318
+ const ux = (pts[j * 3] ?? 0) - ax;
319
+ const uy = (pts[j * 3 + 1] ?? 0) - ay;
320
+ const uz = (pts[j * 3 + 2] ?? 0) - az;
321
+ const vx = (pts[k * 3] ?? 0) - ax;
322
+ const vy = (pts[k * 3 + 1] ?? 0) - ay;
323
+ const vz = (pts[k * 3 + 2] ?? 0) - az;
324
+ let nx = uy * vz - uz * vy;
325
+ let ny = uz * vx - ux * vz;
326
+ let nz = ux * vy - uy * vx;
327
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
328
+ if (len < 1e-9)
329
+ return;
330
+ nx /= len;
331
+ ny /= len;
332
+ nz /= len;
333
+ let above = 0;
334
+ let below = 0;
335
+ for (let p = 0; p < n; p++) {
336
+ const d = ((pts[p * 3] ?? 0) - ax) * nx +
337
+ ((pts[p * 3 + 1] ?? 0) - ay) * ny +
338
+ ((pts[p * 3 + 2] ?? 0) - az) * nz;
339
+ if (d > COPLANAR_M)
340
+ above++;
341
+ else if (d < -COPLANAR_M)
342
+ below++;
343
+ if (above > 0 && below > 0)
344
+ return;
345
+ }
346
+ // Outward means every other point sits behind the plane.
347
+ if (above === 0)
348
+ recordPlane(planes, nx, ny, nz, nx * ax + ny * ay + nz * az);
349
+ if (below === 0)
350
+ recordPlane(planes, -nx, -ny, -nz, -(nx * ax + ny * ay + nz * az));
351
+ }
352
+ function recordPlane(planes, nx, ny, nz, d) {
353
+ for (let p = 0; p < planes.length; p += 4) {
354
+ const dot = (planes[p] ?? 0) * nx + (planes[p + 1] ?? 0) * ny + (planes[p + 2] ?? 0) * nz;
355
+ if (dot > DIR_DEDUP && Math.abs((planes[p + 3] ?? 0) - d) < COPLANAR_M)
356
+ return;
357
+ }
358
+ planes.push(nx, ny, nz, d);
359
+ }
360
+ /** Unique directions (as lines) from a packed list with the given stride. */
361
+ function dedupeDirections(packed, stride) {
362
+ const dirs = [];
363
+ for (let p = 0; p < packed.length; p += stride) {
364
+ const x = packed[p] ?? 0;
365
+ const y = packed[p + 1] ?? 0;
366
+ const z = packed[p + 2] ?? 0;
367
+ appendDirection(dirs, x, y, z);
368
+ }
369
+ return new Float32Array(dirs);
370
+ }
371
+ function appendDirection(dirs, x, y, z) {
372
+ const len = Math.sqrt(x * x + y * y + z * z);
373
+ if (len < 1e-9)
374
+ return;
375
+ const nx = x / len;
376
+ const ny = y / len;
377
+ const nz = z / len;
378
+ for (let d = 0; d < dirs.length; d += 3) {
379
+ const dot = (dirs[d] ?? 0) * nx + (dirs[d + 1] ?? 0) * ny + (dirs[d + 2] ?? 0) * nz;
380
+ if (Math.abs(dot) > DIR_DEDUP)
381
+ return;
382
+ }
383
+ dirs.push(nx, ny, nz);
384
+ }
385
+ /** Edge directions: point pairs lying on at least two distinct supporting planes. */
386
+ function hullEdges(pts, n, planes) {
387
+ const dirs = [];
388
+ for (let i = 0; i < n - 1; i++) {
389
+ for (let j = i + 1; j < n; j++) {
390
+ let shared = 0;
391
+ for (let p = 0; p < planes.length && shared < 2; p += 4) {
392
+ const nx = planes[p] ?? 0;
393
+ const ny = planes[p + 1] ?? 0;
394
+ const nz = planes[p + 2] ?? 0;
395
+ const d = planes[p + 3] ?? 0;
396
+ const di = (pts[i * 3] ?? 0) * nx + (pts[i * 3 + 1] ?? 0) * ny + (pts[i * 3 + 2] ?? 0) * nz - d;
397
+ if (Math.abs(di) > COPLANAR_M)
398
+ continue;
399
+ const dj = (pts[j * 3] ?? 0) * nx + (pts[j * 3 + 1] ?? 0) * ny + (pts[j * 3 + 2] ?? 0) * nz - d;
400
+ if (Math.abs(dj) > COPLANAR_M)
401
+ continue;
402
+ shared++;
403
+ }
404
+ if (shared < 2)
405
+ continue;
406
+ appendDirection(dirs, (pts[j * 3] ?? 0) - (pts[i * 3] ?? 0), (pts[j * 3 + 1] ?? 0) - (pts[i * 3 + 1] ?? 0), (pts[j * 3 + 2] ?? 0) - (pts[i * 3 + 2] ?? 0));
407
+ }
408
+ }
409
+ return new Float32Array(dirs);
410
+ }
411
+ function pairwiseDirections(pts, n) {
412
+ const dirs = [];
413
+ for (let i = 0; i < n - 1; i++) {
414
+ for (let j = i + 1; j < n; j++) {
415
+ appendDirection(dirs, (pts[j * 3] ?? 0) - (pts[i * 3] ?? 0), (pts[j * 3 + 1] ?? 0) - (pts[i * 3 + 1] ?? 0), (pts[j * 3 + 2] ?? 0) - (pts[i * 3 + 2] ?? 0));
416
+ }
417
+ }
418
+ return new Float32Array(dirs);
419
+ }
420
+ /**
421
+ * Group the supporting planes into faces and order each face's points around its normal.
422
+ *
423
+ * `proposePlane` records one plane per point triple, so a square face arrives four times over. The
424
+ * dedupe here is by *direction and offset together*, unlike `appendDirection`, which folds a plane
425
+ * into its opposite because a separating axis has no sign. A face does.
426
+ *
427
+ * **What this gives up:** a face with fewer than three points on it is dropped, so a shape flat
428
+ * enough to have no interior yields no faces and falls back to the empty set. **What would make it
429
+ * wrong** is a caller expecting a degenerate cloud to clip, which it cannot: there is no volume to
430
+ * clip against.
431
+ */
432
+ function buildFaces(pts, n, planes) {
433
+ const kept = [];
434
+ const loops = [];
435
+ for (let p = 0; p + 3 < planes.length; p += 4) {
436
+ const nx = planes[p] ?? 0;
437
+ const ny = planes[p + 1] ?? 0;
438
+ const nz = planes[p + 2] ?? 0;
439
+ const d = planes[p + 3] ?? 0;
440
+ let seen = false;
441
+ for (let k = 0; k + 3 < kept.length; k += 4) {
442
+ const dot = (kept[k] ?? 0) * nx + (kept[k + 1] ?? 0) * ny + (kept[k + 2] ?? 0) * nz;
443
+ if (dot > DIR_DEDUP && Math.abs((kept[k + 3] ?? 0) - d) < COPLANAR_M) {
444
+ seen = true;
445
+ break;
446
+ }
447
+ }
448
+ if (seen)
449
+ continue;
450
+ const on = [];
451
+ for (let i = 0; i < n; i++) {
452
+ const x = pts[i * 3] ?? 0;
453
+ const y = pts[i * 3 + 1] ?? 0;
454
+ const z = pts[i * 3 + 2] ?? 0;
455
+ if (Math.abs(nx * x + ny * y + nz * z - d) < COPLANAR_M)
456
+ on.push(i);
457
+ }
458
+ if (on.length < 3)
459
+ continue;
460
+ kept.push(nx, ny, nz, d);
461
+ loops.push(orderAroundNormal(pts, on, nx, ny, nz));
462
+ }
463
+ if (loops.length === 0)
464
+ return EMPTY_FACES;
465
+ const starts = new Uint16Array(loops.length + 1);
466
+ let total = 0;
467
+ for (let i = 0; i < loops.length; i++) {
468
+ starts[i] = total;
469
+ total += loops[i]?.length ?? 0;
470
+ }
471
+ starts[loops.length] = total;
472
+ const indices = new Uint16Array(total);
473
+ let at = 0;
474
+ for (const loop of loops)
475
+ for (const index of loop)
476
+ indices[at++] = index;
477
+ return {
478
+ facePlanes: new Float32Array(kept),
479
+ faceVertexStart: starts,
480
+ faceVertexIndices: indices,
481
+ };
482
+ }
483
+ /**
484
+ * Sort a face's points counter-clockwise about its outward normal.
485
+ *
486
+ * A comparison sort on a key computed from the two in-plane axes, **not `Math.atan2`** — the
487
+ * determinism gate bans that outright, and the ordering only has to be *consistent*, which a
488
+ * pseudo-angle gives for free. `Array.prototype.sort` is stable by specification, and `on` is built
489
+ * in ascending index order, so two coincident points keep a defined order.
490
+ */
491
+ function orderAroundNormal(pts, on, nx, ny, nz) {
492
+ // Any axis not parallel to the normal gives an in-plane basis.
493
+ const ax = Math.abs(nx) < 0.5 ? 1 : 0;
494
+ const ay = Math.abs(nx) < 0.5 ? 0 : 1;
495
+ let ux = ay * nz;
496
+ let uy = -ax * nz;
497
+ let uz = ax * ny - ay * nx;
498
+ const ulen = Math.sqrt(ux * ux + uy * uy + uz * uz) || 1;
499
+ ux /= ulen;
500
+ uy /= ulen;
501
+ uz /= ulen;
502
+ const vx = ny * uz - nz * uy;
503
+ const vy = nz * ux - nx * uz;
504
+ const vz = nx * uy - ny * ux;
505
+ let cx = 0;
506
+ let cy = 0;
507
+ let cz = 0;
508
+ for (const i of on) {
509
+ cx += pts[i * 3] ?? 0;
510
+ cy += pts[i * 3 + 1] ?? 0;
511
+ cz += pts[i * 3 + 2] ?? 0;
512
+ }
513
+ cx /= on.length;
514
+ cy /= on.length;
515
+ cz /= on.length;
516
+ const key = new Map();
517
+ for (const i of on) {
518
+ const dx = (pts[i * 3] ?? 0) - cx;
519
+ const dy = (pts[i * 3 + 1] ?? 0) - cy;
520
+ const dz = (pts[i * 3 + 2] ?? 0) - cz;
521
+ key.set(i, pseudoAngle(dx * ux + dy * uy + dz * uz, dx * vx + dy * vy + dz * vz));
522
+ }
523
+ // A copy, because `on` is the caller's and sorting in place would surprise it.
524
+ return [...on].sort((p, q) => (key.get(p) ?? 0) - (key.get(q) ?? 0));
525
+ }
526
+ /** A monotonic stand-in for `atan2`, in [0, 4), costing one divide and no transcendental. */
527
+ function pseudoAngle(x, y) {
528
+ const sum = Math.abs(x) + Math.abs(y);
529
+ if (sum === 0)
530
+ return 0;
531
+ const t = y / sum;
532
+ return x < 0 ? 2 - t : t < 0 ? 4 + t : t;
533
+ }
@@ -0,0 +1,136 @@
1
+ import type { BodySet } from './bodies.ts';
2
+ import type { Manifold } from './manifold.ts';
3
+ /**
4
+ * Contact constraints, and the substepped soft solve over them.
5
+ *
6
+ * **Soft rather than Baumgarte, and the difference is a property rather than a preference.** Every
7
+ * constraint carries a *hertz* and a damping ratio, from which the bias rate, mass scale and
8
+ * impulse scale are derived using the substep length. Stating stiffness as a frequency is what
9
+ * makes the substep count a quality dial: turn it up and nothing moves differently. A Baumgarte
10
+ * bias is a fraction of penetration per step, so it means something different at every step length,
11
+ * and the mitigations for that are all tuning constants — which `AGENTS.md` says never to assert.
12
+ *
13
+ * **Nothing here recomputes a manifold.** Anchors are stored in body-local frame at prepare time
14
+ * along with a separation offset, so the current separation is exact at any point inside the tick
15
+ * without re-running narrowphase per substep. `s = sBase + dot((pB + rB') − (pA + rA'), n)` is that
16
+ * identity, and it holds because `sBase` is defined to make it hold at prepare.
17
+ *
18
+ * **Every entry point takes an order array rather than a contiguous range.** Islands are disjoint
19
+ * by construction, so they can be solved in any order or simultaneously — but a constraint list
20
+ * built in pair order is not grouped by island. Indirecting through an order array groups them
21
+ * without permuting eleven parallel arrays, and it is what lets one `solveIsland` serve both
22
+ * executors.
23
+ *
24
+ * **Friction is solved before the normal**, against the normal impulse accumulated so far. Solving
25
+ * it after means the first iteration has no normal impulse to bound against and applies none, so a
26
+ * resting body slides for one iteration every substep.
27
+ */
28
+ /** Contacts start with this much slack allowed before the bias fights them, in metres. */
29
+ export declare const LINEAR_SLOP = 0.005;
30
+ /** How fast the bias may push overlap out, in metres per second. */
31
+ export declare const MAX_BIAS_VELOCITY = 4;
32
+ /** Below this approach speed a bounce is not worth having, and jitters if it is applied. */
33
+ export declare const RESTITUTION_THRESHOLD = 1;
34
+ export declare class ContactConstraints {
35
+ /** One entry per manifold. */
36
+ count: number;
37
+ bodyA: Int32Array;
38
+ bodyB: Int32Array;
39
+ nx: Float32Array;
40
+ ny: Float32Array;
41
+ nz: Float32Array;
42
+ t1x: Float32Array;
43
+ t1y: Float32Array;
44
+ t1z: Float32Array;
45
+ t2x: Float32Array;
46
+ t2y: Float32Array;
47
+ t2z: Float32Array;
48
+ friction: Float32Array;
49
+ restitution: Float32Array;
50
+ pointStart: Int32Array;
51
+ pointCount: Int32Array;
52
+ /** One entry per contact point. */
53
+ points: number;
54
+ localA: Float32Array;
55
+ localB: Float32Array;
56
+ sBase: Float32Array;
57
+ normalMass: Float32Array;
58
+ tangent1Mass: Float32Array;
59
+ tangent2Mass: Float32Array;
60
+ normalImpulse: Float32Array;
61
+ tangent1Impulse: Float32Array;
62
+ tangent2Impulse: Float32Array;
63
+ /** Approach speed when the constraint was prepared, which is what restitution answers to. */
64
+ approach: Float32Array;
65
+ featureId: Int32Array;
66
+ /**
67
+ * Whether each side's anchor sits on a curved surface, one byte per point.
68
+ *
69
+ * A curved anchor is held in world frame rather than in the body's, for the reason `Manifold`
70
+ * states at length: the point where a ball touches the floor does not turn with the ball, and an
71
+ * anchor that turns anyway climbs its side and reports a gap that is not there. Per point rather
72
+ * than per manifold so `worldAnchors` needs only the point index, which is all its callers have.
73
+ */
74
+ curvedA: Uint8Array;
75
+ curvedB: Uint8Array;
76
+ private cap;
77
+ private pcap;
78
+ constructor(capacity?: number);
79
+ clear(): void;
80
+ reserve(pointsNeeded: number): void;
81
+ private growConstraints;
82
+ private growPoints;
83
+ }
84
+ /**
85
+ * Turn one manifold into a constraint, with its effective masses and its local anchors.
86
+ *
87
+ * `warm` supplies the previous tick's impulses by feature id, or −1 where there is none. Warm
88
+ * starting is what lets a stack converge in a handful of iterations rather than a hundred.
89
+ */
90
+ export declare function prepareContact(bodies: BodySet, a: number, b: number, m: Manifold, c: ContactConstraints, warmNormal: Float32Array | null, warmT1: Float32Array | null, warmT2: Float32Array | null): void;
91
+ /** Apply the impulses carried over from the previous tick, before the first iteration. */
92
+ export declare function warmStart(bodies: BodySet, c: ContactConstraints, order: Int32Array, from: number, to: number): void;
93
+ /**
94
+ * One iteration over a range of constraints.
95
+ *
96
+ * `useBias` is false on the relax pass, which is what removes the energy the bias added and is why
97
+ * a stack stops breathing.
98
+ */
99
+ /**
100
+ * How the two tangent rows are bounded against each other.
101
+ *
102
+ * **`box` is two independent clamps and is what this solver has always done.** Each axis is bounded
103
+ * at `mu * N` on its own, so along the diagonal between them the pair reaches `sqrt(2) * mu * N` —
104
+ * forty-one per cent more friction than the same surface offers along an axis. The tangent basis is
105
+ * constructed from the contact normal, so *which* headings are cheap is an artefact of that
106
+ * construction rather than a fact about the world, and a box measurably slides further in some
107
+ * directions than others.
108
+ *
109
+ * **`elliptical` couples them and friction becomes isotropic**, which is what it physically is. It
110
+ * is a correctness fix and it is **not the default**, because every stacking result moves under it:
111
+ * `fingerprintBodies`, both baselines, and any golden fingerprint or stored replay a consumer has
112
+ * kept. A production engine does not silently invalidate those. *What would reverse
113
+ * that:* a major version, where the honest thing is to make the ellipse the default and say so in
114
+ * the porting guide.
115
+ *
116
+ * **One coefficient, not two, and that is a decision rather than an omission.** Anisotropic
117
+ * friction is the natural API for an ellipse — different `mu` along each tangent — and it needs a
118
+ * tangent frame anchored to a *material*, which this solver does not have: `prepareContact` builds
119
+ * the tangents from the normal, so they rotate arbitrarily as a contact moves and a per-axis
120
+ * coefficient would mean something different from one tick to the next. That is a silent wrong
121
+ * answer, so the ellipse here is a circle. *What would reverse it:* a tangent frame carried by the
122
+ * body, which is a larger change than the cone; `vehicle.ts` already needs that and answers it its
123
+ * own way, with a grip table rather than a coefficient.
124
+ */
125
+ export type FrictionModel = 'box' | 'elliptical';
126
+ export declare function solveContacts(bodies: BodySet, c: ContactConstraints, order: Int32Array, from: number, to: number, h: number, useBias: boolean, hertz: number, damping: number, friction?: FrictionModel): void;
127
+ /**
128
+ * One pass after every substep, giving back the bounce the solve suppressed.
129
+ *
130
+ * Applied once against the approach speed captured at prepare, so a drop returns to the same height
131
+ * whatever the substep count. A bounce driven from the *current* velocity instead compounds with
132
+ * every substep and turns the coefficient into a function of the dial.
133
+ */
134
+ export declare function applyRestitution(bodies: BodySet, c: ContactConstraints, order: Int32Array, from: number, to: number): void;
135
+ /** Whether a body is one the solver may move. */
136
+ export declare function isDynamicBody(bodies: BodySet, i: number): boolean;