@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/src/query.ts ADDED
@@ -0,0 +1,844 @@
1
+ import { BODY_STATIC } from './bodies.ts';
2
+ import type { BodySet } from './bodies.ts';
3
+ import { closestOnCylinder, isCylinder, readCylinder } from './cylinder.ts';
4
+ import { faceCount } from './faces.ts';
5
+ import type { Aabb } from './collide/index.ts';
6
+ import { collideShapes, createManifold } from './manifold.ts';
7
+ import { fieldCandidates, fieldCorners, fieldPlane } from './heightfieldShape.ts';
8
+ import { collideMesh, isMesh } from './meshContact.ts';
9
+ import type { ShapePose } from './manifold.ts';
10
+ import type { ConvexShape } from './shape.ts';
11
+ import { DynamicTree } from './tree.ts';
12
+
13
+ /**
14
+ * Asking the world questions: rays, shape sweeps and overlaps.
15
+ *
16
+ * **Nothing allocates.** Every result fills a caller-owned object, and a multi-hit query fills a
17
+ * caller-supplied array to its length and returns a count — so a query in a tick costs no garbage.
18
+ *
19
+ * **Ties break by body index**, which is the same rule the pair list follows and is here for the
20
+ * same reason: an answer decided by which proxy the tree reached first is an answer decided by
21
+ * insertion history. **No test pins it**, and that is worth saying rather than leaving somebody to
22
+ * look: two distinct bodies can only sit at the identical ray distance if they are coincident, and
23
+ * the tree has never been observed to enumerate coincident proxies out of index order — so a bare
24
+ * `<=` passes every variation tried. One comparison buys the guarantee by construction instead.
25
+ *
26
+ * A shapecast is conservative advancement over the same distance query the manifold code uses,
27
+ * capped at a fixed iteration count. **What that gives up** is exactness at a grazing angle, where
28
+ * advancement converges slowly and the cap stops it early — the hit is then reported slightly late.
29
+ * **What would make it wrong** is a consumer relying on a sweep for precise placement rather than
30
+ * for a first touch; the answer then is a root-find on the separation, which costs more per query.
31
+ */
32
+
33
+ /** What a ray or a sweep found. Caller-owned; a query fills it and never allocates one. */
34
+ export interface RayHit {
35
+ body: number;
36
+ /** Fraction along the ray or sweep, in [0, 1]. */
37
+ fraction: number;
38
+ x: number;
39
+ y: number;
40
+ z: number;
41
+ nx: number;
42
+ ny: number;
43
+ nz: number;
44
+ }
45
+
46
+ export function createRayHit(): RayHit {
47
+ return { body: -1, fraction: 1, x: 0, y: 0, z: 0, nx: 0, ny: 0, nz: 0 };
48
+ }
49
+
50
+ /** Which bodies a query is allowed to see. */
51
+ export interface QueryFilter {
52
+ /** The querier's own layer, tested against each body's mask. */
53
+ layer?: number;
54
+ /** Which layers the querier wants, tested against each body's layer. */
55
+ mask?: number;
56
+ /** A body to ignore, usually the one asking. */
57
+ ignore?: number;
58
+ }
59
+
60
+ const HITS_START = 256;
61
+ let hits = new Int32Array(HITS_START);
62
+ const box: Aabb = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
63
+ const poseA: ShapePose = { x: 0, y: 0, z: 0, qx: 0, qy: 0, qz: 0, qw: 1 };
64
+ const poseB: ShapePose = { x: 0, y: 0, z: 0, qx: 0, qy: 0, qz: 0, qw: 1 };
65
+ const manifold = createManifold();
66
+ /**
67
+ * One manifold for a mesh answer, because both callers here want one plane rather than the set.
68
+ *
69
+ * An overlap is a yes or a no, and a shapecast advances against the nearest surface it can see.
70
+ * The full set is `PhysicsWorld`'s business, where every plane becomes a constraint.
71
+ */
72
+ const MESH_ONE = [createManifold()] as const;
73
+
74
+ function copyManifold(from: typeof manifold, to: typeof manifold): void {
75
+ to.nx = from.nx;
76
+ to.ny = from.ny;
77
+ to.nz = from.nz;
78
+ to.count = from.count;
79
+ to.curvedA = from.curvedA;
80
+ to.curvedB = from.curvedB;
81
+ to.points.set(from.points);
82
+ to.separations.set(from.separations);
83
+ to.featureIds.set(from.featureIds);
84
+ }
85
+
86
+ function admits(bodies: BodySet, i: number, filter: QueryFilter | undefined): boolean {
87
+ if (!filter) return true;
88
+ if (filter.ignore === i) return false;
89
+ const layer = filter.layer ?? 0xffffffff;
90
+ const mask = filter.mask ?? 0xffffffff;
91
+ return ((bodies.layer[i] ?? 0) & mask) !== 0 && ((bodies.mask[i] ?? 0) & layer) !== 0;
92
+ }
93
+
94
+ /** Every body whose bounds the tree says could overlap `region`. Grows the scratch as needed. */
95
+ function candidates(tree: DynamicTree, region: Aabb): number {
96
+ let found = tree.query(region, hits);
97
+ while (found === hits.length) {
98
+ hits = new Int32Array(hits.length * 2);
99
+ found = tree.query(region, hits);
100
+ }
101
+ return found;
102
+ }
103
+
104
+ function readPose(bodies: BodySet, i: number, out: ShapePose): void {
105
+ out.x = bodies.posX[i] ?? 0;
106
+ out.y = bodies.posY[i] ?? 0;
107
+ out.z = bodies.posZ[i] ?? 0;
108
+ out.qx = bodies.rotX[i] ?? 0;
109
+ out.qy = bodies.rotY[i] ?? 0;
110
+ out.qz = bodies.rotZ[i] ?? 0;
111
+ out.qw = bodies.rotW[i] ?? 1;
112
+ }
113
+
114
+ /**
115
+ * The nearest body a ray meets, or `false` if it meets none.
116
+ *
117
+ * A ray starting *inside* a body reports that body at fraction zero, with the normal of the face it
118
+ * is nearest to leaving through. Reporting nothing instead is the tempting alternative and is worse:
119
+ * a line-of-sight test would then say a wall it started inside was not there.
120
+ */
121
+ export function raycastWorld(
122
+ bodies: BodySet,
123
+ tree: DynamicTree,
124
+ ox: number,
125
+ oy: number,
126
+ oz: number,
127
+ dx: number,
128
+ dy: number,
129
+ dz: number,
130
+ maxDistance: number,
131
+ out: RayHit,
132
+ filter?: QueryFilter,
133
+ ): boolean {
134
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
135
+ if (len === 0 || maxDistance <= 0) return false;
136
+ const ux = dx / len;
137
+ const uy = dy / len;
138
+ const uz = dz / len;
139
+ const ex = ox + ux * maxDistance;
140
+ const ey = oy + uy * maxDistance;
141
+ const ez = oz + uz * maxDistance;
142
+ box.minX = Math.min(ox, ex);
143
+ box.minY = Math.min(oy, ey);
144
+ box.minZ = Math.min(oz, ez);
145
+ box.maxX = Math.max(ox, ex);
146
+ box.maxY = Math.max(oy, ey);
147
+ box.maxZ = Math.max(oz, ez);
148
+
149
+ const n = candidates(tree, box);
150
+ out.body = -1;
151
+ out.fraction = 1;
152
+ let best = Infinity;
153
+ for (let k = 0; k < n; k++) {
154
+ const i = hits[k] ?? 0;
155
+ if (!admits(bodies, i, filter)) continue;
156
+ const shape = bodies.shape[i];
157
+ if (!shape) continue;
158
+ readPose(bodies, i, poseA);
159
+ if (!rayShape(shape, poseA, ox, oy, oz, ux, uy, uz, maxDistance)) continue;
160
+ if (RAY[0] < best || (RAY[0] === best && i < out.body)) {
161
+ best = RAY[0];
162
+ out.body = i;
163
+ out.fraction = RAY[0] / maxDistance;
164
+ out.x = ox + ux * RAY[0];
165
+ out.y = oy + uy * RAY[0];
166
+ out.z = oz + uz * RAY[0];
167
+ out.nx = RAY[1];
168
+ out.ny = RAY[2];
169
+ out.nz = RAY[3];
170
+ }
171
+ }
172
+ return out.body >= 0;
173
+ }
174
+
175
+ /** `[distance, nx, ny, nz]` of the last ray test. */
176
+ const RAY = new Float64Array(4);
177
+ const WORLD_PLANES = new Float32Array(64 * 4);
178
+
179
+ /**
180
+ * Slab test against a convex shape's world face planes, with the rounding radius folded in.
181
+ *
182
+ * A shape with no faces is a sphere or a capsule and takes the segment-to-segment path instead,
183
+ * which is what its radius makes it.
184
+ */
185
+ function rayShape(
186
+ shape: ConvexShape,
187
+ pose: ShapePose,
188
+ ox: number,
189
+ oy: number,
190
+ oz: number,
191
+ ux: number,
192
+ uy: number,
193
+ uz: number,
194
+ maxDistance: number,
195
+ ): boolean {
196
+ if (shape.triangles !== undefined) {
197
+ return rayMesh(shape, pose, ox, oy, oz, ux, uy, uz, maxDistance);
198
+ }
199
+ if (isCylinder(shape)) return rayCylinder(shape, pose, ox, oy, oz, ux, uy, uz, maxDistance);
200
+ const faces = faceCount(shape);
201
+ if (faces === 0) return rayRound(shape, pose, ox, oy, oz, ux, uy, uz, maxDistance);
202
+
203
+ worldPlanes(shape, pose, faces);
204
+ let enter = 0;
205
+ let exit = maxDistance;
206
+ let nx = 0;
207
+ let ny = 0;
208
+ let nz = 0;
209
+ for (let f = 0; f < faces; f++) {
210
+ const px = WORLD_PLANES[f * 4] ?? 0;
211
+ const py = WORLD_PLANES[f * 4 + 1] ?? 0;
212
+ const pz = WORLD_PLANES[f * 4 + 2] ?? 0;
213
+ const d = (WORLD_PLANES[f * 4 + 3] ?? 0) + shape.radius;
214
+ const denom = px * ux + py * uy + pz * uz;
215
+ const dist = px * ox + py * oy + pz * oz - d;
216
+ if (denom === 0) {
217
+ if (dist > 0) return false;
218
+ continue;
219
+ }
220
+ const t = -dist / denom;
221
+ if (denom < 0) {
222
+ if (t > enter) {
223
+ enter = t;
224
+ nx = px;
225
+ ny = py;
226
+ nz = pz;
227
+ }
228
+ } else if (t < exit) {
229
+ exit = t;
230
+ }
231
+ if (enter > exit) return false;
232
+ }
233
+ if (enter > maxDistance) return false;
234
+ RAY[0] = enter;
235
+ if (enter === 0) {
236
+ // Started inside: the nearest face's outward normal is the honest answer.
237
+ let nearest = 0;
238
+ let best = -Infinity;
239
+ for (let f = 0; f < faces; f++) {
240
+ const v =
241
+ (WORLD_PLANES[f * 4] ?? 0) * ox +
242
+ (WORLD_PLANES[f * 4 + 1] ?? 0) * oy +
243
+ (WORLD_PLANES[f * 4 + 2] ?? 0) * oz -
244
+ (WORLD_PLANES[f * 4 + 3] ?? 0);
245
+ if (v > best) {
246
+ best = v;
247
+ nearest = f;
248
+ }
249
+ }
250
+ nx = WORLD_PLANES[nearest * 4] ?? 0;
251
+ ny = WORLD_PLANES[nearest * 4 + 1] ?? 0;
252
+ nz = WORLD_PLANES[nearest * 4 + 2] ?? 0;
253
+ }
254
+ RAY[1] = nx;
255
+ RAY[2] = ny;
256
+ RAY[3] = nz;
257
+ return true;
258
+ }
259
+
260
+ /**
261
+ * A ray against a cylinder: a quadratic against the infinite side, and two discs for the caps.
262
+ *
263
+ * **Closed form rather than the march `rayRound` uses**, and the reason is that a cylinder has one.
264
+ * A capsule's distance field has no root a quadratic can find, so that function halves its way in
265
+ * at a fixed iteration count; a cylinder's side is a quadratic in the ray parameter and its caps
266
+ * are two plane intersections with one radial test each. Exact, and cheaper.
267
+ *
268
+ * A ray starting inside reports zero distance and the nearest surface's outward normal, which is
269
+ * the same answer `rayShape` gives for a polytope and is what a caller asking "what am I in"
270
+ * needs.
271
+ */
272
+ function rayCylinder(
273
+ shape: ConvexShape,
274
+ pose: ShapePose,
275
+ ox: number,
276
+ oy: number,
277
+ oz: number,
278
+ ux: number,
279
+ uy: number,
280
+ uz: number,
281
+ maxDistance: number,
282
+ ): boolean {
283
+ readCylinder(shape, pose, CYL);
284
+ const cx = CYL[0] ?? 0;
285
+ const cy = CYL[1] ?? 0;
286
+ const cz = CYL[2] ?? 0;
287
+ const ax = CYL[3] ?? 0;
288
+ const ay = CYL[4] ?? 0;
289
+ const az = CYL[5] ?? 0;
290
+ const h = CYL[6] ?? 0;
291
+ const r = (CYL[7] ?? 0) + shape.radius;
292
+
293
+ closestOnCylinder(CYL, ox, oy, oz, INSIDE);
294
+ if (INSIDE[6] <= shape.radius) {
295
+ RAY[0] = 0;
296
+ RAY[1] = INSIDE[3];
297
+ RAY[2] = INSIDE[4];
298
+ RAY[3] = INSIDE[5];
299
+ return true;
300
+ }
301
+
302
+ const dx = ox - cx;
303
+ const dy = oy - cy;
304
+ const dz = oz - cz;
305
+ const dAlong = dx * ax + dy * ay + dz * az;
306
+ const uAlong = ux * ax + uy * ay + uz * az;
307
+ const dpx = dx - ax * dAlong;
308
+ const dpy = dy - ay * dAlong;
309
+ const dpz = dz - az * dAlong;
310
+ const upx = ux - ax * uAlong;
311
+ const upy = uy - ay * uAlong;
312
+ const upz = uz - az * uAlong;
313
+
314
+ let best = Infinity;
315
+ let nx = 0;
316
+ let ny = 1;
317
+ let nz = 0;
318
+
319
+ const qa = upx * upx + upy * upy + upz * upz;
320
+ if (qa > 1e-12) {
321
+ const qb = 2 * (dpx * upx + dpy * upy + dpz * upz);
322
+ const qc = dpx * dpx + dpy * dpy + dpz * dpz - r * r;
323
+ const disc = qb * qb - 4 * qa * qc;
324
+ if (disc >= 0) {
325
+ const root = Math.sqrt(disc);
326
+ for (let i = 0; i < 2; i++) {
327
+ const t = (i === 0 ? -qb - root : -qb + root) / (2 * qa);
328
+ if (t < 0 || t >= best) continue;
329
+ const along = dAlong + uAlong * t;
330
+ if (Math.abs(along) > h) continue;
331
+ const px = dpx + upx * t;
332
+ const py = dpy + upy * t;
333
+ const pz = dpz + upz * t;
334
+ const len = Math.sqrt(px * px + py * py + pz * pz) || 1;
335
+ best = t;
336
+ nx = px / len;
337
+ ny = py / len;
338
+ nz = pz / len;
339
+ }
340
+ }
341
+ }
342
+
343
+ // The two caps: a plane each, kept only where the crossing is inside the disc.
344
+ if (Math.abs(uAlong) > 1e-12) {
345
+ for (let i = 0; i < 2; i++) {
346
+ const side = i === 0 ? 1 : -1;
347
+ const t = (side * h - dAlong) / uAlong;
348
+ if (t < 0 || t >= best) continue;
349
+ const px = dpx + upx * t;
350
+ const py = dpy + upy * t;
351
+ const pz = dpz + upz * t;
352
+ if (px * px + py * py + pz * pz > r * r) continue;
353
+ best = t;
354
+ nx = ax * side;
355
+ ny = ay * side;
356
+ nz = az * side;
357
+ }
358
+ }
359
+
360
+ if (best > maxDistance) return false;
361
+ RAY[0] = best;
362
+ RAY[1] = nx;
363
+ RAY[2] = ny;
364
+ RAY[3] = nz;
365
+ return true;
366
+ }
367
+
368
+ const CYL = new Float64Array(11);
369
+ const INSIDE = new Float64Array(7);
370
+
371
+ /**
372
+ * A ray against a static triangle mesh: the segment's own box against the tree, then every
373
+ * candidate.
374
+ *
375
+ * **The tree is queried with the whole segment's bounds rather than walked**, and that is a cost
376
+ * stated rather than hidden: a long ray across a large mesh returns candidates along a box that is
377
+ * mostly empty, where a descent testing the ray against each node's box would return a handful.
378
+ * *What it buys* is that nothing new had to be built — `DynamicTree.query` is the tested thing this
379
+ * package already has. *What would make it wrong* is a consumer casting long rays through a dense
380
+ * level every frame, at which point the tree grows a ray descent and this calls it instead.
381
+ *
382
+ * Möller-Trumbore per triangle, which needs only the arithmetic the determinism gate permits: four
383
+ * cross products, three dots and one divide, with no transcendental anywhere. **Two-sided here**,
384
+ * unlike the contact path: a ray is a question about geometry rather than a body being pushed, and
385
+ * a line-of-sight test that saw through a wall from behind would be a worse answer than one that
386
+ * did not.
387
+ */
388
+ function rayMesh(
389
+ shape: ConvexShape,
390
+ pose: ShapePose,
391
+ ox: number,
392
+ oy: number,
393
+ oz: number,
394
+ ux: number,
395
+ uy: number,
396
+ uz: number,
397
+ maxDistance: number,
398
+ ): boolean {
399
+ const mesh = shape.triangles;
400
+ if (mesh === undefined) return false;
401
+ /* The ray in the mesh's own frame, because the tree is in that frame. */
402
+ unrotatePose(pose, ox - pose.x, oy - pose.y, oz - pose.z, RAY_LOCAL, 0);
403
+ unrotatePose(pose, ux, uy, uz, RAY_LOCAL, 3);
404
+ const lx = RAY_LOCAL[0];
405
+ const ly = RAY_LOCAL[1];
406
+ const lz = RAY_LOCAL[2];
407
+ const dx = RAY_LOCAL[3];
408
+ const dy = RAY_LOCAL[4];
409
+ const dz = RAY_LOCAL[5];
410
+ const ex = lx + dx * maxDistance;
411
+ const ey = ly + dy * maxDistance;
412
+ const ez = lz + dz * maxDistance;
413
+ box.minX = Math.min(lx, ex);
414
+ box.minY = Math.min(ly, ey);
415
+ box.minZ = Math.min(lz, ez);
416
+ box.maxX = Math.max(lx, ex);
417
+ box.maxY = Math.max(ly, ey);
418
+ box.maxZ = Math.max(lz, ez);
419
+ /* A tree for a level, an index range for a heightfield. Both fill to the buffer's length and
420
+ stop, so the grow-and-retry is the same either way. See `heightfieldShape.ts`. */
421
+ const ask = (): number =>
422
+ mesh.field !== undefined
423
+ ? fieldCandidates(mesh.field, box, hits)
424
+ : (mesh.tree?.query(box, hits) ?? 0);
425
+ let found = ask();
426
+ while (found === hits.length) {
427
+ hits = new Int32Array(hits.length * 2);
428
+ found = ask();
429
+ }
430
+
431
+ let best = maxDistance;
432
+ let triangle = -1;
433
+ for (let i = 0; i < found; i++) {
434
+ const t = hits[i] ?? 0;
435
+ const distance = rayTriangle(mesh, t, lx, ly, lz, dx, dy, dz);
436
+ if (distance >= 0 && distance < best) {
437
+ best = distance;
438
+ triangle = t;
439
+ }
440
+ }
441
+ if (triangle < 0) return false;
442
+
443
+ RAY[0] = best;
444
+ /* The face normal, turned to face the ray: a hit from behind reports the side it was hit on. */
445
+ trianglePlane(mesh, triangle, RAY_PLANE);
446
+ rotatePose(pose, RAY_PLANE[0] ?? 0, RAY_PLANE[1] ?? 0, RAY_PLANE[2] ?? 0, RAY_LOCAL, 6);
447
+ const facing = RAY_LOCAL[6] * ux + RAY_LOCAL[7] * uy + RAY_LOCAL[8] * uz;
448
+ const sign = facing > 0 ? -1 : 1;
449
+ RAY[1] = RAY_LOCAL[6] * sign;
450
+ RAY[2] = RAY_LOCAL[7] * sign;
451
+ RAY[3] = RAY_LOCAL[8] * sign;
452
+ return true;
453
+ }
454
+
455
+ const RAY_LOCAL = new Float64Array(9);
456
+ /** The current triangle's plane and corners, from whichever source the shape has. */
457
+ const RAY_PLANE = new Float64Array(4);
458
+ const RAY_CORNERS = new Float64Array(9);
459
+
460
+ /** A triangle's plane: read for a level, computed for a heightfield. */
461
+ function trianglePlane(
462
+ mesh: NonNullable<ConvexShape['triangles']>,
463
+ triangle: number,
464
+ out: Float64Array,
465
+ ): void {
466
+ if (mesh.field !== undefined) {
467
+ fieldPlane(mesh.field, triangle, out);
468
+ return;
469
+ }
470
+ out[0] = mesh.planes[triangle * 4] ?? 0;
471
+ out[1] = mesh.planes[triangle * 4 + 1] ?? 0;
472
+ out[2] = mesh.planes[triangle * 4 + 2] ?? 0;
473
+ out[3] = mesh.planes[triangle * 4 + 3] ?? 0;
474
+ }
475
+
476
+ /** A triangle's three corners, likewise. */
477
+ function triangleCorners(
478
+ mesh: NonNullable<ConvexShape['triangles']>,
479
+ triangle: number,
480
+ out: Float64Array,
481
+ ): void {
482
+ if (mesh.field !== undefined) {
483
+ fieldCorners(mesh.field, triangle, out);
484
+ return;
485
+ }
486
+ for (let k = 0; k < 3; k++) {
487
+ const index = mesh.indices[triangle * 3 + k] ?? 0;
488
+ out[k * 3] = mesh.positions[index * 3] ?? 0;
489
+ out[k * 3 + 1] = mesh.positions[index * 3 + 1] ?? 0;
490
+ out[k * 3 + 2] = mesh.positions[index * 3 + 2] ?? 0;
491
+ }
492
+ }
493
+
494
+ /** Möller-Trumbore. Returns the distance along the ray, or −1 for a miss. */
495
+ function rayTriangle(
496
+ mesh: NonNullable<ConvexShape['triangles']>,
497
+ triangle: number,
498
+ ox: number,
499
+ oy: number,
500
+ oz: number,
501
+ dx: number,
502
+ dy: number,
503
+ dz: number,
504
+ ): number {
505
+ triangleCorners(mesh, triangle, RAY_CORNERS);
506
+ const ax = RAY_CORNERS[0] ?? 0;
507
+ const ay = RAY_CORNERS[1] ?? 0;
508
+ const az = RAY_CORNERS[2] ?? 0;
509
+ const e1x = (RAY_CORNERS[3] ?? 0) - ax;
510
+ const e1y = (RAY_CORNERS[4] ?? 0) - ay;
511
+ const e1z = (RAY_CORNERS[5] ?? 0) - az;
512
+ const e2x = (RAY_CORNERS[6] ?? 0) - ax;
513
+ const e2y = (RAY_CORNERS[7] ?? 0) - ay;
514
+ const e2z = (RAY_CORNERS[8] ?? 0) - az;
515
+
516
+ const px = dy * e2z - dz * e2y;
517
+ const py = dz * e2x - dx * e2z;
518
+ const pz = dx * e2y - dy * e2x;
519
+ const determinant = e1x * px + e1y * py + e1z * pz;
520
+ /* Parallel to the plane, from either side: no crossing to report. */
521
+ if (determinant > -1e-12 && determinant < 1e-12) return -1;
522
+ const inverse = 1 / determinant;
523
+
524
+ const tx = ox - ax;
525
+ const ty = oy - ay;
526
+ const tz = oz - az;
527
+ const u = (tx * px + ty * py + tz * pz) * inverse;
528
+ if (u < 0 || u > 1) return -1;
529
+
530
+ const qx = ty * e1z - tz * e1y;
531
+ const qy = tz * e1x - tx * e1z;
532
+ const qz = tx * e1y - ty * e1x;
533
+ const v = (dx * qx + dy * qy + dz * qz) * inverse;
534
+ if (v < 0 || u + v > 1) return -1;
535
+
536
+ const distance = (e2x * qx + e2y * qy + e2z * qz) * inverse;
537
+ return distance >= 0 ? distance : -1;
538
+ }
539
+
540
+ function rotatePose(
541
+ p: ShapePose,
542
+ x: number,
543
+ y: number,
544
+ z: number,
545
+ out: Float64Array,
546
+ at: number,
547
+ ): void {
548
+ const tx = 2 * (p.qy * z - p.qz * y);
549
+ const ty = 2 * (p.qz * x - p.qx * z);
550
+ const tz = 2 * (p.qx * y - p.qy * x);
551
+ out[at] = x + p.qw * tx + (p.qy * tz - p.qz * ty);
552
+ out[at + 1] = y + p.qw * ty + (p.qz * tx - p.qx * tz);
553
+ out[at + 2] = z + p.qw * tz + (p.qx * ty - p.qy * tx);
554
+ }
555
+
556
+ function unrotatePose(
557
+ p: ShapePose,
558
+ x: number,
559
+ y: number,
560
+ z: number,
561
+ out: Float64Array,
562
+ at: number,
563
+ ): void {
564
+ const qx = -p.qx;
565
+ const qy = -p.qy;
566
+ const qz = -p.qz;
567
+ const qw = p.qw;
568
+ const tx = 2 * (qy * z - qz * y);
569
+ const ty = 2 * (qz * x - qx * z);
570
+ const tz = 2 * (qx * y - qy * x);
571
+ out[at] = x + qw * tx + (qy * tz - qz * ty);
572
+ out[at + 1] = y + qw * ty + (qz * tx - qx * tz);
573
+ out[at + 2] = z + qw * tz + (qx * ty - qy * tx);
574
+ }
575
+
576
+ /** A ray against a sphere or a capsule: the closest approach to its segment against the radius. */
577
+ function rayRound(
578
+ shape: ConvexShape,
579
+ pose: ShapePose,
580
+ ox: number,
581
+ oy: number,
582
+ oz: number,
583
+ ux: number,
584
+ uy: number,
585
+ uz: number,
586
+ maxDistance: number,
587
+ ): boolean {
588
+ const count = shape.vertices.length / 3;
589
+ rotatePoint(shape, pose, 0, SEG, 0);
590
+ if (count > 1) rotatePoint(shape, pose, 1, SEG, 3);
591
+ else {
592
+ SEG[3] = SEG[0];
593
+ SEG[4] = SEG[1];
594
+ SEG[5] = SEG[2];
595
+ }
596
+ // March the ray, halving the step whenever it would pass through. Fixed count, no tolerance loop.
597
+ let t = 0;
598
+ for (let step = 0; step < 32; step++) {
599
+ const px = ox + ux * t;
600
+ const py = oy + uy * t;
601
+ const pz = oz + uz * t;
602
+ const d = pointSegmentDistance(px, py, pz) - shape.radius;
603
+ if (d < 1e-4) {
604
+ RAY[0] = t;
605
+ const cx = px - CLOSE[0];
606
+ const cy = py - CLOSE[1];
607
+ const cz = pz - CLOSE[2];
608
+ const cl = Math.sqrt(cx * cx + cy * cy + cz * cz) || 1;
609
+ RAY[1] = cx / cl;
610
+ RAY[2] = cy / cl;
611
+ RAY[3] = cz / cl;
612
+ return true;
613
+ }
614
+ t += d;
615
+ if (t > maxDistance) return false;
616
+ }
617
+ return false;
618
+ }
619
+
620
+ const SEG = new Float64Array(6);
621
+ const CLOSE = new Float64Array(3);
622
+
623
+ function pointSegmentDistance(px: number, py: number, pz: number): number {
624
+ const ax = SEG[0];
625
+ const ay = SEG[1];
626
+ const az = SEG[2];
627
+ const bx = SEG[3] - ax;
628
+ const by = SEG[4] - ay;
629
+ const bz = SEG[5] - az;
630
+ const bb = bx * bx + by * by + bz * bz;
631
+ let s = 0;
632
+ if (bb > 1e-12) {
633
+ s = ((px - ax) * bx + (py - ay) * by + (pz - az) * bz) / bb;
634
+ s = s < 0 ? 0 : s > 1 ? 1 : s;
635
+ }
636
+ CLOSE[0] = ax + bx * s;
637
+ CLOSE[1] = ay + by * s;
638
+ CLOSE[2] = az + bz * s;
639
+ const dx = px - CLOSE[0];
640
+ const dy = py - CLOSE[1];
641
+ const dz = pz - CLOSE[2];
642
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
643
+ }
644
+
645
+ function rotatePoint(
646
+ shape: ConvexShape,
647
+ pose: ShapePose,
648
+ index: number,
649
+ out: Float64Array,
650
+ at: number,
651
+ ): void {
652
+ const x = shape.vertices[index * 3] ?? 0;
653
+ const y = shape.vertices[index * 3 + 1] ?? 0;
654
+ const z = shape.vertices[index * 3 + 2] ?? 0;
655
+ const tx = 2 * (pose.qy * z - pose.qz * y);
656
+ const ty = 2 * (pose.qz * x - pose.qx * z);
657
+ const tz = 2 * (pose.qx * y - pose.qy * x);
658
+ out[at] = x + pose.qw * tx + (pose.qy * tz - pose.qz * ty) + pose.x;
659
+ out[at + 1] = y + pose.qw * ty + (pose.qz * tx - pose.qx * tz) + pose.y;
660
+ out[at + 2] = z + pose.qw * tz + (pose.qx * ty - pose.qy * tx) + pose.z;
661
+ }
662
+
663
+ function worldPlanes(shape: ConvexShape, pose: ShapePose, faces: number): void {
664
+ const { qx, qy, qz, qw } = pose;
665
+ const r0 = 1 - 2 * (qy * qy + qz * qz);
666
+ const r1 = 2 * (qx * qy - qz * qw);
667
+ const r2 = 2 * (qx * qz + qy * qw);
668
+ const r3 = 2 * (qx * qy + qz * qw);
669
+ const r4 = 1 - 2 * (qx * qx + qz * qz);
670
+ const r5 = 2 * (qy * qz - qx * qw);
671
+ const r6 = 2 * (qx * qz - qy * qw);
672
+ const r7 = 2 * (qy * qz + qx * qw);
673
+ const r8 = 1 - 2 * (qx * qx + qy * qy);
674
+ for (let f = 0; f < faces; f++) {
675
+ const x = shape.facePlanes[f * 4] ?? 0;
676
+ const y = shape.facePlanes[f * 4 + 1] ?? 0;
677
+ const z = shape.facePlanes[f * 4 + 2] ?? 0;
678
+ const d = shape.facePlanes[f * 4 + 3] ?? 0;
679
+ const nx = r0 * x + r1 * y + r2 * z;
680
+ const ny = r3 * x + r4 * y + r5 * z;
681
+ const nz = r6 * x + r7 * y + r8 * z;
682
+ WORLD_PLANES[f * 4] = nx;
683
+ WORLD_PLANES[f * 4 + 1] = ny;
684
+ WORLD_PLANES[f * 4 + 2] = nz;
685
+ // A plane through a rotated point at the same offset, translated onto the pose.
686
+ WORLD_PLANES[f * 4 + 3] = d + nx * pose.x + ny * pose.y + nz * pose.z;
687
+ }
688
+ }
689
+
690
+ /**
691
+ * Every body overlapping a shape at a pose. Fills `out` to its length and returns how many.
692
+ *
693
+ * A return equal to `out.length` means the buffer filled, and the caller has been told about that
694
+ * many rather than about all of them. Returning the true total instead would invite a loop over
695
+ * entries that were never written.
696
+ */
697
+ export function overlapWorld(
698
+ bodies: BodySet,
699
+ tree: DynamicTree,
700
+ shape: ConvexShape,
701
+ pose: ShapePose,
702
+ out: Int32Array,
703
+ filter?: QueryFilter,
704
+ ): number {
705
+ const r = shape.boundRadius;
706
+ box.minX = pose.x - r;
707
+ box.minY = pose.y - r;
708
+ box.minZ = pose.z - r;
709
+ box.maxX = pose.x + r;
710
+ box.maxY = pose.y + r;
711
+ box.maxZ = pose.z + r;
712
+ const n = candidates(tree, box);
713
+ let found = 0;
714
+ // Ascending body index, because the tree's order is insertion history.
715
+ for (let i = 0; i < bodies.count && found < out.length; i++) {
716
+ let seen = false;
717
+ for (let k = 0; k < n; k++) {
718
+ if (hits[k] === i) {
719
+ seen = true;
720
+ break;
721
+ }
722
+ }
723
+ if (!seen || !admits(bodies, i, filter)) continue;
724
+ const other = bodies.shape[i];
725
+ if (!other) continue;
726
+ readPose(bodies, i, poseB);
727
+ /* A mesh answers through its own path, which fills a set; overlap only needs "any". */
728
+ const touching =
729
+ isMesh(shape) || isMesh(other)
730
+ ? collideMesh(shape, pose, other, poseB, 0, MESH_ONE) > 0
731
+ : collideShapes(shape, pose, other, poseB, 0, manifold);
732
+ if (touching) out[found++] = i;
733
+ }
734
+ return found;
735
+ }
736
+
737
+ /**
738
+ * Sweep a shape from one point to another and report the first body it touches.
739
+ *
740
+ * Conservative advancement: step by the current separation, which cannot overshoot a contact, and
741
+ * stop after a fixed number of steps. A fixed count rather than a tolerance, because §3a forbids
742
+ * iterating to one.
743
+ */
744
+ export function shapecastWorld(
745
+ bodies: BodySet,
746
+ tree: DynamicTree,
747
+ shape: ConvexShape,
748
+ pose: ShapePose,
749
+ dx: number,
750
+ dy: number,
751
+ dz: number,
752
+ out: RayHit,
753
+ filter?: QueryFilter,
754
+ ): boolean {
755
+ const travel = Math.sqrt(dx * dx + dy * dy + dz * dz);
756
+ if (travel === 0) return false;
757
+ const r = shape.boundRadius;
758
+ box.minX = Math.min(pose.x, pose.x + dx) - r;
759
+ box.minY = Math.min(pose.y, pose.y + dy) - r;
760
+ box.minZ = Math.min(pose.z, pose.z + dz) - r;
761
+ box.maxX = Math.max(pose.x, pose.x + dx) + r;
762
+ box.maxY = Math.max(pose.y, pose.y + dy) + r;
763
+ box.maxZ = Math.max(pose.z, pose.z + dz) + r;
764
+ const n = candidates(tree, box);
765
+
766
+ out.body = -1;
767
+ out.fraction = 1;
768
+ let best = Infinity;
769
+ const startX = pose.x;
770
+ const startY = pose.y;
771
+ const startZ = pose.z;
772
+ for (let k = 0; k < n; k++) {
773
+ const i = hits[k] ?? 0;
774
+ if (!admits(bodies, i, filter)) continue;
775
+ const other = bodies.shape[i];
776
+ if (!other) continue;
777
+ readPose(bodies, i, poseB);
778
+ let t = 0;
779
+ let touched = false;
780
+ let brushing = false;
781
+ for (let step = 0; step < 24; step++) {
782
+ poseA.x = startX + dx * t;
783
+ poseA.y = startY + dy * t;
784
+ poseA.z = startZ + dz * t;
785
+ poseA.qx = pose.qx;
786
+ poseA.qy = pose.qy;
787
+ poseA.qz = pose.qz;
788
+ poseA.qw = pose.qw;
789
+ /*
790
+ * A mesh fills a set and the advance wants the *nearest* plane, which is the deepest
791
+ * separation of the first entry the tree returned — good enough for a conservative step,
792
+ * since a step short of the true one converges rather than missing.
793
+ */
794
+ if (isMesh(shape) || isMesh(other)) {
795
+ if (collideMesh(shape, poseA, other, poseB, 1e9, MESH_ONE) === 0) break;
796
+ const first = MESH_ONE[0];
797
+ if (first === undefined) break;
798
+ copyManifold(first, manifold);
799
+ } else if (!collideShapes(shape, poseA, other, poseB, 1e9, manifold)) break;
800
+ const gap = manifold.separations[0] ?? 0;
801
+ if (gap < 1e-4) {
802
+ /*
803
+ * A shape already touching a surface it is travelling *along* has not run into it.
804
+ *
805
+ * Without this, a character resting on the floor is reported as blocked at fraction zero by
806
+ * every horizontal sweep, and never moves. `manifold.n` points from the swept shape into the
807
+ * body, so travel into it is a positive dot; zero or negative is brushing past.
808
+ */
809
+ if (step === 0 && dx * manifold.nx + dy * manifold.ny + dz * manifold.nz <= 1e-9) {
810
+ brushing = true;
811
+ break;
812
+ }
813
+ touched = true;
814
+ break;
815
+ }
816
+ t += gap / travel;
817
+ if (t > 1) break;
818
+ }
819
+ if (brushing || !touched || t > 1) continue;
820
+ if (t < best || (t === best && i < out.body)) {
821
+ best = t;
822
+ out.body = i;
823
+ out.fraction = t;
824
+ out.x = startX + dx * t;
825
+ out.y = startY + dy * t;
826
+ out.z = startZ + dz * t;
827
+ /*
828
+ * The **surface** normal, pointing out of the body toward the sweeper, which is the same
829
+ * convention `raycastWorld` answers in. `manifold.n` points the other way — from the swept
830
+ * shape into the body — and returning that made every caller that used both queries read a
831
+ * floor as a wall.
832
+ */
833
+ out.nx = -manifold.nx;
834
+ out.ny = -manifold.ny;
835
+ out.nz = -manifold.nz;
836
+ }
837
+ }
838
+ return out.body >= 0;
839
+ }
840
+
841
+ /** Whether a body is one a query should treat as scenery. */
842
+ export function isStaticBody(bodies: BodySet, i: number): boolean {
843
+ return bodies.type[i] === BODY_STATIC;
844
+ }