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