@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
@@ -0,0 +1,393 @@
1
+ import { fieldCandidates, fieldConvexEdges, fieldCorners, fieldPlane } from './heightfieldShape.js';
2
+ import { collideShapes, createManifold } from './manifold.js';
3
+ import { shapeBounds } from './shape.js';
4
+ /**
5
+ * Contacts between a convex shape and a static triangle mesh.
6
+ *
7
+ * **Many manifolds, not one, and that is the whole structural difference.** Every other pair in
8
+ * this engine has a single contact plane; a body in the corner of a mesh room has two or three, and
9
+ * reducing them to one would pick a wall and let the body through the others. So this fills a set,
10
+ * and `PhysicsWorld` prepares a constraint per entry — which the solver already supports, because
11
+ * `prepareContact` appends rather than replaces.
12
+ *
13
+ * **Each triangle goes through `collideShapes` unchanged**, as a three-point polytope written into
14
+ * one scratch shape. That is the reuse that makes this small: the separating-axis test, the
15
+ * clipping, the sphere and capsule paths and the cylinder's own module are all exact against a
16
+ * triangle already, and a second implementation of any of them would be a second thing to be wrong.
17
+ * *What it costs* is that the moving shape is transformed into world space once per candidate
18
+ * triangle rather than once per pair — eight vertices times twenty candidates for a box on a floor.
19
+ * *What would make it wrong* is a consumer resting a 64-point hull on a dense mesh, where the
20
+ * answer is to hoist the transform out of `collideShapes` rather than to write a second narrow
21
+ * phase.
22
+ *
23
+ * **Two sources of triangles and one set of rules.** A level's triangles are stored and found
24
+ * through a tree; a heightfield's are generated from four samples and found by index. Everything
25
+ * below is written once for both — the one-sided rule, the many manifolds, the interior-edge
26
+ * filter — and the only thing that differs is where a triangle's corners, plane and edge
27
+ * classification come from. `heightfieldShape.ts` is the other source, and the split is the
28
+ * 2026-08-13 rule applied to the one file here it would be most expensive to have two of.
29
+ *
30
+ * **One-sided.** A contact whose normal points into the back of a triangle is dropped. A level mesh
31
+ * is a surface rather than a solid, so "which side" is the only thing that distinguishes standing
32
+ * on a floor from being pushed up through it, and a two-sided triangle catches anything that has
33
+ * already sunk below it and fires it out the wrong way.
34
+ *
35
+ * **And the interior-edge filter, which is the part that decides whether it feels right.** A box
36
+ * sliding across two coplanar triangles meets their shared edge, where the narrow phase finds an
37
+ * edge normal rather than the face's and shoves the box backwards along it — a character stumbling
38
+ * on a flat floor once a metre. `meshShape` classifies every edge once as convex or not; a normal
39
+ * that came off a non-convex edge is replaced with the face's, and the separation is recomputed
40
+ * from the face plane. *What that gives up:* a genuinely convex ridge still produces its edge
41
+ * normal, which is correct, so a consumer who wants a ridge softened has to bevel it.
42
+ */
43
+ /** How many contact planes one pair may produce. A body in a corner needs three; eight is generous. */
44
+ export const MAX_MESH_MANIFOLDS = 8;
45
+ /** How parallel a contact normal must be to the face before it counts as coming off the face. */
46
+ const FACE_DOT = 0.999;
47
+ /** The candidate list from the tree, grown rather than capped. */
48
+ let candidates = new Int32Array(256);
49
+ const region = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
50
+ const local = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
51
+ const scratch = createManifold();
52
+ const rot = new Float32Array(9);
53
+ /**
54
+ * A triangle as a polytope, rewritten per candidate.
55
+ *
56
+ * One face rather than two, which is what makes the pair one-sided at the level below as well:
57
+ * `collideShapes` looks for a reference face and there is only the front one to find.
58
+ */
59
+ const TRIANGLE = {
60
+ vertices: new Float32Array(9),
61
+ faceNormals: new Float32Array(3),
62
+ edgeDirs: new Float32Array(9),
63
+ radius: 0,
64
+ sideRadius: 0,
65
+ boundRadius: 0,
66
+ facePlanes: new Float32Array(4),
67
+ faceVertexStart: new Uint16Array([0, 3]),
68
+ faceVertexIndices: new Uint16Array([0, 1, 2]),
69
+ };
70
+ /**
71
+ * Whether a shape is a static triangle mesh, which is the one question the dispatch asks.
72
+ *
73
+ * Takes an absent shape as *not* a mesh, because a compound body has no single shape and the
74
+ * dispatch asks about both sides before it knows which kind of pair it has.
75
+ */
76
+ export function isMesh(shape) {
77
+ return shape?.triangles !== undefined;
78
+ }
79
+ /**
80
+ * Fill `out` with up to `out.length` manifolds and return how many.
81
+ *
82
+ * `out` is the caller's, allocated once — this runs in the tick. The normal of each manifold points
83
+ * from `shapeA` to `shapeB`, exactly as `collideShapes` promises, whichever of the two is the mesh.
84
+ */
85
+ export function collideMesh(shapeA, poseA, shapeB, poseB, margin, out) {
86
+ const meshIsA = isMesh(shapeA);
87
+ const mesh = (meshIsA ? shapeA : shapeB).triangles;
88
+ if (mesh === undefined)
89
+ return 0;
90
+ const meshPose = meshIsA ? poseA : poseB;
91
+ const other = meshIsA ? shapeB : shapeA;
92
+ const otherPose = meshIsA ? poseB : poseA;
93
+ /* Two meshes never touch: both are static, so nothing would move if they did. */
94
+ if (isMesh(other))
95
+ return 0;
96
+ /*
97
+ * The moving shape's bounds, carried into the mesh's own frame — because the tree is in that
98
+ * frame and rebuilding it per frame is exactly what static means it never has to do. The box is
99
+ * transformed rather than the tree: a rotated box's axis-aligned bounds are a little larger than
100
+ * the shape, which costs a few extra candidates and nothing else.
101
+ */
102
+ shapeBounds(other, region);
103
+ worldRegion(region, otherPose, meshPose, margin, local);
104
+ let found = candidatesFor(mesh, local);
105
+ while (found === candidates.length) {
106
+ candidates = new Int32Array(candidates.length * 2);
107
+ found = candidatesFor(mesh, local);
108
+ }
109
+ let count = 0;
110
+ for (let i = 0; i < found; i++) {
111
+ const triangle = candidates[i] ?? 0;
112
+ if (!writeTriangle(mesh, triangle))
113
+ continue;
114
+ /* A to B, with the triangle standing in for the mesh wherever the mesh stood. */
115
+ const hit = meshIsA
116
+ ? collideShapes(TRIANGLE, meshPose, other, otherPose, margin, scratch)
117
+ : collideShapes(other, otherPose, TRIANGLE, meshPose, margin, scratch);
118
+ if (!hit)
119
+ continue;
120
+ /* The face normal in world, pointing away from the front of the triangle. */
121
+ planeOf(mesh, triangle);
122
+ rotateInto(meshPose, PLANE, 0, FACE);
123
+ /* Mesh-to-body, whichever way the manifold was asked for. */
124
+ const sign = meshIsA ? 1 : -1;
125
+ const away = sign * scratch.nx * FACE[0] + sign * scratch.ny * FACE[1] + sign * scratch.nz * FACE[2];
126
+ /* One-sided: a normal pointing into the back of the triangle is a body already through it. */
127
+ if (away <= 0)
128
+ continue;
129
+ if (away < FACE_DOT)
130
+ filterInteriorEdge(mesh, triangle, meshPose, sign);
131
+ const target = out[count];
132
+ if (target === undefined)
133
+ break;
134
+ copyInto(scratch, target, triangle);
135
+ count++;
136
+ if (count === out.length)
137
+ break;
138
+ }
139
+ return count;
140
+ }
141
+ /** `[nx, ny, nz]` of the current triangle's world face normal. */
142
+ const FACE = new Float64Array(3);
143
+ /** The current triangle's plane in the mesh's own frame, from wherever it came. */
144
+ const PLANE = new Float32Array(4);
145
+ /** The current triangle's three corners, likewise. */
146
+ const CORNERS = new Float64Array(9);
147
+ /**
148
+ * The candidates a box could touch: a tree query for a level, an index range for a field.
149
+ *
150
+ * Both fill to `candidates.length` and stop, so the caller's grow-and-retry loop is the same one
151
+ * either way.
152
+ */
153
+ function candidatesFor(mesh, local) {
154
+ if (mesh.field !== undefined)
155
+ return fieldCandidates(mesh.field, local, candidates);
156
+ return mesh.tree?.query(local, candidates) ?? 0;
157
+ }
158
+ /** The current triangle's plane into `PLANE`: read for a level, computed for a field. */
159
+ function planeOf(mesh, triangle) {
160
+ if (mesh.field !== undefined) {
161
+ fieldPlane(mesh.field, triangle, PLANE_SCRATCH);
162
+ PLANE[0] = PLANE_SCRATCH[0] ?? 0;
163
+ PLANE[1] = PLANE_SCRATCH[1] ?? 0;
164
+ PLANE[2] = PLANE_SCRATCH[2] ?? 0;
165
+ PLANE[3] = PLANE_SCRATCH[3] ?? 0;
166
+ return;
167
+ }
168
+ PLANE[0] = mesh.planes[triangle * 4] ?? 0;
169
+ PLANE[1] = mesh.planes[triangle * 4 + 1] ?? 0;
170
+ PLANE[2] = mesh.planes[triangle * 4 + 2] ?? 0;
171
+ PLANE[3] = mesh.planes[triangle * 4 + 3] ?? 0;
172
+ }
173
+ const PLANE_SCRATCH = new Float64Array(4);
174
+ /** The current triangle's corners into `CORNERS`, in the mesh's own frame. */
175
+ function cornersOf(mesh, triangle) {
176
+ if (mesh.field !== undefined) {
177
+ fieldCorners(mesh.field, triangle, CORNERS);
178
+ return;
179
+ }
180
+ for (let k = 0; k < 3; k++) {
181
+ const index = mesh.indices[triangle * 3 + k] ?? 0;
182
+ CORNERS[k * 3] = mesh.positions[index * 3] ?? 0;
183
+ CORNERS[k * 3 + 1] = mesh.positions[index * 3 + 1] ?? 0;
184
+ CORNERS[k * 3 + 2] = mesh.positions[index * 3 + 2] ?? 0;
185
+ }
186
+ }
187
+ /** Which of a triangle's edges may produce a normal: stored for a level, computed for a field. */
188
+ function convexEdgesOf(mesh, triangle) {
189
+ if (mesh.field !== undefined)
190
+ return fieldConvexEdges(mesh.field, triangle);
191
+ return mesh.convexEdges[triangle] ?? 0;
192
+ }
193
+ /** One corner of a triangle, in world. */
194
+ function worldCorner(mesh, triangle, corner, pose, out, at) {
195
+ cornersOf(mesh, triangle);
196
+ poseRotation(pose);
197
+ const x = CORNERS[corner * 3] ?? 0;
198
+ const y = CORNERS[corner * 3 + 1] ?? 0;
199
+ const z = CORNERS[corner * 3 + 2] ?? 0;
200
+ out[at] = (rot[0] ?? 0) * x + (rot[1] ?? 0) * y + (rot[2] ?? 0) * z + pose.x;
201
+ out[at + 1] = (rot[3] ?? 0) * x + (rot[4] ?? 0) * y + (rot[5] ?? 0) * z + pose.y;
202
+ out[at + 2] = (rot[6] ?? 0) * x + (rot[7] ?? 0) * y + (rot[8] ?? 0) * z + pose.z;
203
+ }
204
+ /**
205
+ * Replace an edge normal with the face's, where the edge it came off is not a real one.
206
+ *
207
+ * The deepest contact point decides which edge, by distance to each of the three in the triangle's
208
+ * own plane — a vertex contact falls to whichever of its two edges is nearer, and both are treated
209
+ * the same way, which is what the filter wants.
210
+ *
211
+ * The separations are recomputed from the face plane rather than scaled, because the direction has
212
+ * changed and a scaled depth would hold a body off the floor by the cosine of an angle nobody
213
+ * chose.
214
+ */
215
+ function filterInteriorEdge(mesh, triangle, meshPose, sign) {
216
+ let deepest = 0;
217
+ for (let i = 1; i < scratch.count; i++) {
218
+ if ((scratch.separations[i] ?? 0) < (scratch.separations[deepest] ?? 0))
219
+ deepest = i;
220
+ }
221
+ const px = scratch.points[deepest * 3] ?? 0;
222
+ const py = scratch.points[deepest * 3 + 1] ?? 0;
223
+ const pz = scratch.points[deepest * 3 + 2] ?? 0;
224
+ let nearest = 0;
225
+ let best = Infinity;
226
+ for (let e = 0; e < 3; e++) {
227
+ worldCorner(mesh, triangle, e, meshPose, EDGE, 0);
228
+ worldCorner(mesh, triangle, (e + 1) % 3, meshPose, EDGE, 3);
229
+ const distance = pointSegmentSquared(px, py, pz);
230
+ if (distance < best) {
231
+ best = distance;
232
+ nearest = e;
233
+ }
234
+ }
235
+ /* A convex edge is a real one: the ridge is there and the body should feel it. */
236
+ if ((convexEdgesOf(mesh, triangle) >> nearest) & 1)
237
+ return;
238
+ scratch.nx = sign * FACE[0];
239
+ scratch.ny = sign * FACE[1];
240
+ scratch.nz = sign * FACE[2];
241
+ worldPlaneAnchor(mesh, triangle, meshPose, ANCHOR);
242
+ const plane = FACE[0] * ANCHOR[0] + FACE[1] * ANCHOR[1] + FACE[2] * ANCHOR[2];
243
+ for (let i = 0; i < scratch.count; i++) {
244
+ scratch.separations[i] =
245
+ (scratch.points[i * 3] ?? 0) * FACE[0] +
246
+ (scratch.points[i * 3 + 1] ?? 0) * FACE[1] +
247
+ (scratch.points[i * 3 + 2] ?? 0) * FACE[2] -
248
+ plane;
249
+ }
250
+ }
251
+ const EDGE = new Float64Array(6);
252
+ const ANCHOR = new Float64Array(3);
253
+ function pointSegmentSquared(px, py, pz) {
254
+ const ax = EDGE[0];
255
+ const ay = EDGE[1];
256
+ const az = EDGE[2];
257
+ const ex = EDGE[3] - ax;
258
+ const ey = EDGE[4] - ay;
259
+ const ez = EDGE[5] - az;
260
+ const ee = ex * ex + ey * ey + ez * ez;
261
+ let t = ee > 1e-12 ? ((px - ax) * ex + (py - ay) * ey + (pz - az) * ez) / ee : 0;
262
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
263
+ const dx = px - (ax + ex * t);
264
+ const dy = py - (ay + ey * t);
265
+ const dz = pz - (az + ez * t);
266
+ return dx * dx + dy * dy + dz * dz;
267
+ }
268
+ /** Write one triangle into the scratch shape. Returns false for a degenerate one. */
269
+ function writeTriangle(mesh, triangle) {
270
+ planeOf(mesh, triangle);
271
+ const nx = PLANE[0] ?? 0;
272
+ const ny = PLANE[1] ?? 0;
273
+ const nz = PLANE[2] ?? 0;
274
+ if (nx === 0 && ny === 0 && nz === 0)
275
+ return false;
276
+ cornersOf(mesh, triangle);
277
+ const v = TRIANGLE.vertices;
278
+ for (let k = 0; k < 9; k++)
279
+ v[k] = CORNERS[k] ?? 0;
280
+ TRIANGLE.faceNormals[0] = nx;
281
+ TRIANGLE.faceNormals[1] = ny;
282
+ TRIANGLE.faceNormals[2] = nz;
283
+ TRIANGLE.facePlanes[0] = nx;
284
+ TRIANGLE.facePlanes[1] = ny;
285
+ TRIANGLE.facePlanes[2] = nz;
286
+ TRIANGLE.facePlanes[3] = PLANE[3] ?? 0;
287
+ for (let e = 0; e < 3; e++) {
288
+ const j = (e + 1) % 3;
289
+ TRIANGLE.edgeDirs[e * 3] = (v[j * 3] ?? 0) - (v[e * 3] ?? 0);
290
+ TRIANGLE.edgeDirs[e * 3 + 1] = (v[j * 3 + 1] ?? 0) - (v[e * 3 + 1] ?? 0);
291
+ TRIANGLE.edgeDirs[e * 3 + 2] = (v[j * 3 + 2] ?? 0) - (v[e * 3 + 2] ?? 0);
292
+ }
293
+ return true;
294
+ }
295
+ /**
296
+ * Copy a manifold out of the scratch, folding the triangle into every feature id.
297
+ *
298
+ * **Warm starting is why the triangle has to be in the id.** Impulses persist across ticks by
299
+ * feature id, and two triangles under one body produce the same clipping features — so without
300
+ * this a box on a seam would hand triangle A's accumulated impulse to triangle B every tick, which
301
+ * reads as a stack that will not settle.
302
+ */
303
+ function copyInto(from, to, triangle) {
304
+ to.nx = from.nx;
305
+ to.ny = from.ny;
306
+ to.nz = from.nz;
307
+ to.count = from.count;
308
+ to.curvedA = from.curvedA;
309
+ to.curvedB = from.curvedB;
310
+ for (let i = 0; i < from.count; i++) {
311
+ to.points[i * 3] = from.points[i * 3] ?? 0;
312
+ to.points[i * 3 + 1] = from.points[i * 3 + 1] ?? 0;
313
+ to.points[i * 3 + 2] = from.points[i * 3 + 2] ?? 0;
314
+ to.separations[i] = from.separations[i] ?? 0;
315
+ to.featureIds[i] = (((triangle & 0xffff) << 12) ^ (from.featureIds[i] ?? 0)) | 0;
316
+ }
317
+ }
318
+ /** The triangle's first corner, in world, which is a point the face plane passes through. */
319
+ function worldPlaneAnchor(mesh, triangle, pose, out) {
320
+ worldCorner(mesh, triangle, 0, pose, out, 0);
321
+ }
322
+ /** Rotate a direction out of a packed array into `out`. */
323
+ function rotateInto(pose, source, at, out) {
324
+ poseRotation(pose);
325
+ const x = source[at] ?? 0;
326
+ const y = source[at + 1] ?? 0;
327
+ const z = source[at + 2] ?? 0;
328
+ out[0] = (rot[0] ?? 0) * x + (rot[1] ?? 0) * y + (rot[2] ?? 0) * z;
329
+ out[1] = (rot[3] ?? 0) * x + (rot[4] ?? 0) * y + (rot[5] ?? 0) * z;
330
+ out[2] = (rot[6] ?? 0) * x + (rot[7] ?? 0) * y + (rot[8] ?? 0) * z;
331
+ }
332
+ /**
333
+ * The moving shape's world bounds, expressed in the mesh's frame and grown by the margin.
334
+ *
335
+ * Eight corners through the inverse of the mesh's pose, re-bounded: a rotated box's axis-aligned
336
+ * bounds in another frame are larger than the shape, which costs a few candidates the narrow phase
337
+ * then rejects and is far cheaper than transforming the geometry.
338
+ */
339
+ function worldRegion(bounds, otherPose, meshPose, margin, out) {
340
+ out.minX = Infinity;
341
+ out.minY = Infinity;
342
+ out.minZ = Infinity;
343
+ out.maxX = -Infinity;
344
+ out.maxY = -Infinity;
345
+ out.maxZ = -Infinity;
346
+ for (let corner = 0; corner < 8; corner++) {
347
+ const x = corner & 1 ? bounds.maxX : bounds.minX;
348
+ const y = corner & 2 ? bounds.maxY : bounds.minY;
349
+ const z = corner & 4 ? bounds.maxZ : bounds.minZ;
350
+ /* Out of the shape's own frame into the world, then into the mesh's. */
351
+ poseRotation(otherPose);
352
+ const wx = (rot[0] ?? 0) * x + (rot[1] ?? 0) * y + (rot[2] ?? 0) * z + otherPose.x;
353
+ const wy = (rot[3] ?? 0) * x + (rot[4] ?? 0) * y + (rot[5] ?? 0) * z + otherPose.y;
354
+ const wz = (rot[6] ?? 0) * x + (rot[7] ?? 0) * y + (rot[8] ?? 0) * z + otherPose.z;
355
+ unrotate(meshPose, wx - meshPose.x, wy - meshPose.y, wz - meshPose.z, ANCHOR);
356
+ out.minX = Math.min(out.minX, ANCHOR[0]);
357
+ out.minY = Math.min(out.minY, ANCHOR[1]);
358
+ out.minZ = Math.min(out.minZ, ANCHOR[2]);
359
+ out.maxX = Math.max(out.maxX, ANCHOR[0]);
360
+ out.maxY = Math.max(out.maxY, ANCHOR[1]);
361
+ out.maxZ = Math.max(out.maxZ, ANCHOR[2]);
362
+ }
363
+ out.minX -= margin;
364
+ out.minY -= margin;
365
+ out.minZ -= margin;
366
+ out.maxX += margin;
367
+ out.maxY += margin;
368
+ out.maxZ += margin;
369
+ }
370
+ function poseRotation(p) {
371
+ const { qx: x, qy: y, qz: z, qw: w } = p;
372
+ rot[0] = 1 - 2 * (y * y + z * z);
373
+ rot[1] = 2 * (x * y - z * w);
374
+ rot[2] = 2 * (x * z + y * w);
375
+ rot[3] = 2 * (x * y + z * w);
376
+ rot[4] = 1 - 2 * (x * x + z * z);
377
+ rot[5] = 2 * (y * z - x * w);
378
+ rot[6] = 2 * (x * z - y * w);
379
+ rot[7] = 2 * (y * z + x * w);
380
+ rot[8] = 1 - 2 * (x * x + y * y);
381
+ }
382
+ function unrotate(p, x, y, z, out) {
383
+ const qx = -p.qx;
384
+ const qy = -p.qy;
385
+ const qz = -p.qz;
386
+ const qw = p.qw;
387
+ const tx = 2 * (qy * z - qz * y);
388
+ const ty = 2 * (qz * x - qx * z);
389
+ const tz = 2 * (qx * y - qy * x);
390
+ out[0] = x + qw * tx + (qy * tz - qz * ty);
391
+ out[1] = y + qw * ty + (qz * tx - qx * tz);
392
+ out[2] = z + qw * tz + (qx * ty - qy * tx);
393
+ }
@@ -0,0 +1,76 @@
1
+ import type { ConvexShape } from './shape.ts';
2
+ import type { Heightfield } from './heightfieldShape.ts';
3
+ import { DynamicTree } from './tree.ts';
4
+ /**
5
+ * A static triangle mesh as a collision shape: the triangles, their planes, and the tree over them.
6
+ *
7
+ * **Static only, and the limit is enforced rather than documented.** `PhysicsWorld.addBody` refuses
8
+ * one on anything but a static body and `colliderFromShape` refuses one outright. A moving concave
9
+ * mesh needs its tree rebuilt or refitted every tick and needs a mass tensor a triangle soup does
10
+ * not have — and the usual answer to a moving concave thing is convex decomposition, which is a row
11
+ * of its own. **Most consumers want exactly this**: the level is a mesh and everything in it is a
12
+ * hull, a box or a sphere.
13
+ *
14
+ * **The tree is `DynamicTree`, built once and never moved.** A second acceleration structure would
15
+ * be a second thing to get right, and the one this package already has is an AABB tree with a
16
+ * tested query — its incremental machinery simply goes unused. *What that costs* is the 10 cm fat
17
+ * margin every proxy carries, which for a small triangle means a query returns a few candidates it
18
+ * did not need; the narrow phase rejects them in one separating-axis test each. *What would make it
19
+ * wrong* is a mesh of millimetre triangles, where the margin dwarfs the geometry and every query
20
+ * returns most of the mesh.
21
+ *
22
+ * **The interior-edge data is the half that decides whether it feels right.** A box sliding across
23
+ * two triangles catches on their shared edge, because at the seam the narrow phase finds the edge
24
+ * rather than the face and pushes the box back along it — the classic symptom is a character
25
+ * stumbling on a perfectly flat floor every metre. Each edge is therefore classified once, here,
26
+ * as convex or not, and `meshContact.ts` uses that to replace an edge normal with the face's.
27
+ */
28
+ export interface TriangleMesh {
29
+ /** xyz per vertex, in the mesh's own space. */
30
+ readonly positions: Float32Array;
31
+ /** Three vertex indices per triangle. */
32
+ readonly indices: Uint32Array;
33
+ readonly triangleCount: number;
34
+ /** `nx, ny, nz, d` per triangle: the outward plane, with `d = n · v0`. */
35
+ readonly planes: Float32Array;
36
+ /**
37
+ * Three bits per triangle, one per edge: set where the fold at that edge turns **away** from the
38
+ * front face, or where the edge has no neighbour at all.
39
+ *
40
+ * Edge `i` runs from local vertex `i` to `(i + 1) % 3`. A boundary edge counts as convex because
41
+ * there is nothing on the other side to be caught between; a coplanar neighbour counts as *not*
42
+ * convex, because a flat seam is exactly the case a contact must never come off.
43
+ */
44
+ readonly convexEdges: Uint8Array;
45
+ /**
46
+ * Over triangle indices, built once — and **null for a heightfield**, which needs none.
47
+ *
48
+ * A field's candidates are an index range rather than a tree query: the cells a box covers are
49
+ * arithmetic on its own bounds. See `heightfieldShape.ts`, which is the other producer of this
50
+ * shape and the reason this is nullable.
51
+ */
52
+ readonly tree: DynamicTree | null;
53
+ /**
54
+ * Set where the triangles are generated from a heightfield rather than stored.
55
+ *
56
+ * The arrays above are then empty and nothing reads them: `meshContact.ts` and `query.ts` take
57
+ * their corners, planes and edge classifications from the field instead. Everything *else* about
58
+ * a mesh — the one-sided rule, the many manifolds, the interior-edge filter — is shared, which is
59
+ * the whole design.
60
+ */
61
+ readonly field?: Heightfield;
62
+ }
63
+ /**
64
+ * Build a static mesh shape.
65
+ *
66
+ * Returns a `ConvexShape` whose `triangles` names the mesh, whose `vertices` are the eight corners
67
+ * of its bounding box — so `shapeBounds`, `boundRadius` and the broad phase need no special case —
68
+ * and whose `faceNormals` and `edgeDirs` are **empty**. That last part is deliberate: a mesh has no
69
+ * separating axes worth enumerating, and handing the kinematic sweep the box's three would make it
70
+ * treat a hollow level as a solid brick.
71
+ *
72
+ * **`positions` and `indices` are kept, not copied**, the same bargain `packSplats` makes: a level
73
+ * mesh is megabytes and duplicating it serves nobody. A caller must not go on mutating them, which
74
+ * is stated here because nothing enforces it.
75
+ */
76
+ export declare function meshShape(positions: Float32Array, indices: Uint32Array): ConvexShape;
@@ -0,0 +1,175 @@
1
+ import { DynamicTree } from './tree.js';
2
+ /** How far a neighbour's opposite vertex must sit off a plane before the fold has a direction. */
3
+ const COPLANAR_M = 1e-5;
4
+ /**
5
+ * Build a static mesh shape.
6
+ *
7
+ * Returns a `ConvexShape` whose `triangles` names the mesh, whose `vertices` are the eight corners
8
+ * of its bounding box — so `shapeBounds`, `boundRadius` and the broad phase need no special case —
9
+ * and whose `faceNormals` and `edgeDirs` are **empty**. That last part is deliberate: a mesh has no
10
+ * separating axes worth enumerating, and handing the kinematic sweep the box's three would make it
11
+ * treat a hollow level as a solid brick.
12
+ *
13
+ * **`positions` and `indices` are kept, not copied**, the same bargain `packSplats` makes: a level
14
+ * mesh is megabytes and duplicating it serves nobody. A caller must not go on mutating them, which
15
+ * is stated here because nothing enforces it.
16
+ */
17
+ export function meshShape(positions, indices) {
18
+ if (indices.length === 0 || indices.length % 3 !== 0) {
19
+ throw new Error(`meshShape: ${indices.length} indices is not a whole number of triangles, or is none at all`);
20
+ }
21
+ const triangleCount = indices.length / 3;
22
+ const planes = new Float32Array(triangleCount * 4);
23
+ const convexEdges = new Uint8Array(triangleCount);
24
+ const tree = new DynamicTree(Math.max(64, triangleCount * 2));
25
+ let minX = Infinity;
26
+ let minY = Infinity;
27
+ let minZ = Infinity;
28
+ let maxX = -Infinity;
29
+ let maxY = -Infinity;
30
+ let maxZ = -Infinity;
31
+ const box = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
32
+ /*
33
+ * Every undirected edge to the one triangle that has claimed it, so the second triangle to meet
34
+ * it finds the first. A `Map` rather than a sorted array because this is build-time and the key
35
+ * is a pair of indices; the value packs the triangle and which of its three edges it was.
36
+ */
37
+ const edges = new Map();
38
+ const vertexCount = positions.length / 3;
39
+ const edgeKey = (a, b) => (a < b ? a : b) * vertexCount + (a < b ? b : a);
40
+ for (let t = 0; t < triangleCount; t++) {
41
+ const i0 = indices[t * 3] ?? 0;
42
+ const i1 = indices[t * 3 + 1] ?? 0;
43
+ const i2 = indices[t * 3 + 2] ?? 0;
44
+ const ax = positions[i0 * 3] ?? 0;
45
+ const ay = positions[i0 * 3 + 1] ?? 0;
46
+ const az = positions[i0 * 3 + 2] ?? 0;
47
+ const bx = positions[i1 * 3] ?? 0;
48
+ const by = positions[i1 * 3 + 1] ?? 0;
49
+ const bz = positions[i1 * 3 + 2] ?? 0;
50
+ const cx = positions[i2 * 3] ?? 0;
51
+ const cy = positions[i2 * 3 + 1] ?? 0;
52
+ const cz = positions[i2 * 3 + 2] ?? 0;
53
+ let nx = (by - ay) * (cz - az) - (bz - az) * (cy - ay);
54
+ let ny = (bz - az) * (cx - ax) - (bx - ax) * (cz - az);
55
+ let nz = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
56
+ const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
57
+ /*
58
+ * A degenerate triangle has no normal and is dropped rather than refused: a mesh exported from
59
+ * a modelling tool routinely carries a few, and rejecting the whole level for three slivers
60
+ * would be a refusal a consumer cannot act on. It keeps its slot so triangle indices still
61
+ * name the caller's own triangles, and its plane is a zero the contact code skips.
62
+ */
63
+ if (length > 1e-12) {
64
+ nx /= length;
65
+ ny /= length;
66
+ nz /= length;
67
+ }
68
+ else {
69
+ nx = 0;
70
+ ny = 0;
71
+ nz = 0;
72
+ }
73
+ planes[t * 4] = nx;
74
+ planes[t * 4 + 1] = ny;
75
+ planes[t * 4 + 2] = nz;
76
+ planes[t * 4 + 3] = nx * ax + ny * ay + nz * az;
77
+ box.minX = Math.min(ax, bx, cx);
78
+ box.minY = Math.min(ay, by, cy);
79
+ box.minZ = Math.min(az, bz, cz);
80
+ box.maxX = Math.max(ax, bx, cx);
81
+ box.maxY = Math.max(ay, by, cy);
82
+ box.maxZ = Math.max(az, bz, cz);
83
+ tree.insert(t, box);
84
+ minX = Math.min(minX, box.minX);
85
+ minY = Math.min(minY, box.minY);
86
+ minZ = Math.min(minZ, box.minZ);
87
+ maxX = Math.max(maxX, box.maxX);
88
+ maxY = Math.max(maxY, box.maxY);
89
+ maxZ = Math.max(maxZ, box.maxZ);
90
+ /* Every edge starts convex; meeting a neighbour is what can take that away. */
91
+ convexEdges[t] = 0b111;
92
+ for (let e = 0; e < 3; e++) {
93
+ const from = indices[t * 3 + e] ?? 0;
94
+ const to = indices[t * 3 + ((e + 1) % 3)] ?? 0;
95
+ const key = edgeKey(from, to);
96
+ const seen = edges.get(key);
97
+ if (seen === undefined) {
98
+ edges.set(key, t * 3 + e);
99
+ continue;
100
+ }
101
+ classify(positions, indices, planes, convexEdges, t, e, seen);
102
+ }
103
+ }
104
+ return {
105
+ /* The eight corners of the bounds, so everything that measures a shape measures the right box. */
106
+ vertices: new Float32Array([
107
+ minX,
108
+ minY,
109
+ minZ,
110
+ maxX,
111
+ minY,
112
+ minZ,
113
+ minX,
114
+ maxY,
115
+ minZ,
116
+ maxX,
117
+ maxY,
118
+ minZ,
119
+ minX,
120
+ minY,
121
+ maxZ,
122
+ maxX,
123
+ minY,
124
+ maxZ,
125
+ minX,
126
+ maxY,
127
+ maxZ,
128
+ maxX,
129
+ maxY,
130
+ maxZ,
131
+ ]),
132
+ faceNormals: new Float32Array(0),
133
+ edgeDirs: new Float32Array(0),
134
+ radius: 0,
135
+ sideRadius: 0,
136
+ boundRadius: Math.sqrt(Math.max(minX * minX, maxX * maxX) +
137
+ Math.max(minY * minY, maxY * maxY) +
138
+ Math.max(minZ * minZ, maxZ * maxZ)),
139
+ facePlanes: new Float32Array(0),
140
+ faceVertexStart: new Uint16Array(0),
141
+ faceVertexIndices: new Uint16Array(0),
142
+ triangles: { positions, indices, triangleCount, planes, convexEdges, tree },
143
+ };
144
+ }
145
+ /**
146
+ * Decide, for one shared edge, whether either side of it may produce a contact normal of its own.
147
+ *
148
+ * The test is where the *other* triangle's opposite vertex sits relative to this one's plane, and
149
+ * both sides are answered at once because the answer is symmetric in what it forbids: a fold that
150
+ * turns away from one face turns toward the other.
151
+ */
152
+ function classify(positions, indices, planes, convexEdges, triangle, edge, other) {
153
+ const otherTriangle = Math.floor(other / 3);
154
+ const otherEdge = other % 3;
155
+ /* The vertex neither triangle shares: the one opposite the edge. */
156
+ const opposite = indices[otherTriangle * 3 + ((otherEdge + 2) % 3)] ?? 0;
157
+ const px = positions[opposite * 3] ?? 0;
158
+ const py = positions[opposite * 3 + 1] ?? 0;
159
+ const pz = positions[opposite * 3 + 2] ?? 0;
160
+ const height = px * (planes[triangle * 4] ?? 0) +
161
+ py * (planes[triangle * 4 + 1] ?? 0) +
162
+ pz * (planes[triangle * 4 + 2] ?? 0) -
163
+ (planes[triangle * 4 + 3] ?? 0);
164
+ /*
165
+ * **Below the plane is a convex fold and everything else is not.** A neighbour that rises above
166
+ * this triangle's face is a concave corner, where a contact normal off the edge is a normal
167
+ * pointing into a wall that is really there; a neighbour lying in the plane is a flat seam, where
168
+ * an edge normal is the interior-edge artefact this exists to remove.
169
+ */
170
+ const convex = height < -COPLANAR_M;
171
+ if (!convex) {
172
+ convexEdges[triangle] = (convexEdges[triangle] ?? 0) & ~(1 << edge);
173
+ convexEdges[otherTriangle] = (convexEdges[otherTriangle] ?? 0) & ~(1 << otherEdge);
174
+ }
175
+ }