@adobe/data-gpu 0.9.87 → 0.9.89
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/dist/core/core-plugin.d.ts +1 -1
- package/dist/graphics/animation/animation-plugin.d.ts +2 -2
- package/dist/graphics/camera/camera-plugin.d.ts +1 -1
- package/dist/graphics/camera/orbit/orbit-data-plugin.d.ts +1 -1
- package/dist/graphics/camera/orbit/orbit-plugin.d.ts +1 -1
- package/dist/graphics/camera/orbit/orbit-system-plugin.d.ts +1 -1
- package/dist/graphics/graphics-plugin.d.ts +1 -1
- package/dist/graphics/picking/picking-plugin.d.ts +798 -861
- package/dist/graphics/rendering/bone-collider-plugin.d.ts +2647 -275
- package/dist/graphics/rendering/bone-collider-plugin.js +4 -4
- package/dist/graphics/rendering/bone-collider-plugin.js.map +1 -1
- package/dist/graphics/rendering/display-transform-plugin.d.ts +2 -2
- package/dist/graphics/rendering/display-transform-plugin.js +2 -2
- package/dist/graphics/rendering/display-transform-plugin.js.map +1 -1
- package/dist/graphics/rendering/ibl-render/ibl-render-plugin.d.ts +159 -177
- package/dist/graphics/rendering/interpolation-plugin.d.ts +2447 -56
- package/dist/graphics/rendering/jolt-ragdoll-plugin.d.ts +2634 -262
- package/dist/graphics/rendering/material-palette-gpu/material-palette-gpu-plugin.d.ts +8 -8
- package/dist/graphics/rendering/material-palette-gpu/material-palette-gpu-plugin.js +1 -1
- package/dist/graphics/rendering/material-palette-gpu/material-palette-gpu-plugin.js.map +1 -1
- package/dist/graphics/rendering/model-collider-plugin.d.ts +2567 -192
- package/dist/graphics/rendering/pbr-core-plugin.d.ts +5 -5
- package/dist/graphics/rendering/pbr-core-plugin.js +5 -5
- package/dist/graphics/rendering/pbr-core-plugin.js.map +1 -1
- package/dist/graphics/rendering/pbr-render/pbr-factor-render-plugin.d.ts +172 -190
- package/dist/graphics/rendering/pbr-render/physics-bridge-plugin.d.ts +2398 -5
- package/dist/graphics/rendering/skinning/skinning-plugin.d.ts +189 -210
- package/dist/graphics/rendering/skinning/skinning-plugin.js +1 -1
- package/dist/graphics/rendering/skinning/skinning-plugin.js.map +1 -1
- package/dist/graphics/scene/model/mesh-plugin.d.ts +20 -50
- package/dist/graphics/scene/model/model-loader-plugin.d.ts +156 -174
- package/dist/graphics/scene/model/shape/shape-geometry-plugin.d.ts +73 -76
- package/dist/graphics/scene/model/world-bounds-plugin.d.ts +156 -174
- package/dist/graphics/scene/node/node-data-plugin.d.ts +2 -5
- package/dist/graphics/scene/node/node-data-plugin.js +2 -2
- package/dist/graphics/scene/node/node-data-plugin.js.map +1 -1
- package/dist/graphics/scene/node/node-plugin.d.ts +2 -5
- package/dist/graphics/scene/node/transform-plugin.d.ts +2 -5
- package/dist/graphics/scene/scene-plugin.d.ts +16 -31
- package/dist/graphics/scene/scene-uniforms/scene-uniforms-plugin.d.ts +1 -1
- package/dist/physics/physics-clock-plugin.d.ts +1 -1
- package/dist/physics/physics-data-plugin.d.ts +2400 -4
- package/dist/physics/physics-data-plugin.js +28 -2
- package/dist/physics/physics-data-plugin.js.map +1 -1
- package/dist/physics/physics-hit.d.ts +18 -0
- package/dist/physics/physics-hit.js +3 -0
- package/dist/physics/physics-hit.js.map +1 -0
- package/dist/physics/physics-query.d.ts +43 -0
- package/dist/physics/physics-query.js +3 -0
- package/dist/physics/physics-query.js.map +1 -0
- package/dist/physics/public.d.ts +2 -0
- package/dist/physics/public.js +2 -0
- package/dist/physics/public.js.map +1 -1
- package/dist/physics/solvers/jolt-solver-plugin.d.ts +2399 -9
- package/dist/physics/solvers/jolt-solver-plugin.js +151 -2
- package/dist/physics/solvers/jolt-solver-plugin.js.map +1 -1
- package/dist/physics/solvers/mutable-hit.d.ts +14 -0
- package/dist/physics/solvers/mutable-hit.js +3 -0
- package/dist/physics/solvers/mutable-hit.js.map +1 -0
- package/dist/physics/solvers/orient-normal-against-ray.d.ts +11 -0
- package/dist/physics/solvers/orient-normal-against-ray.js +25 -0
- package/dist/physics/solvers/orient-normal-against-ray.js.map +1 -0
- package/dist/physics/solvers/orient-normal-against-ray.test.d.ts +1 -0
- package/dist/physics/solvers/orient-normal-against-ray.test.js +27 -0
- package/dist/physics/solvers/orient-normal-against-ray.test.js.map +1 -0
- package/dist/physics/solvers/pick-ray-conformance.test.d.ts +1 -0
- package/dist/physics/solvers/pick-ray-conformance.test.js +159 -0
- package/dist/physics/solvers/pick-ray-conformance.test.js.map +1 -0
- package/dist/physics/solvers/rapier-solver-plugin.d.ts +2399 -9
- package/dist/physics/solvers/rapier-solver-plugin.js +112 -3
- package/dist/physics/solvers/rapier-solver-plugin.js.map +1 -1
- package/package.json +2 -2
- package/src/graphics/rendering/bone-collider-plugin.ts +4 -4
- package/src/graphics/rendering/display-transform-plugin.ts +2 -2
- package/src/graphics/rendering/material-palette-gpu/material-palette-gpu-plugin.ts +1 -1
- package/src/graphics/rendering/pbr-core-plugin.ts +5 -5
- package/src/graphics/rendering/skinning/skinning-plugin.ts +1 -1
- package/src/graphics/scene/node/node-data-plugin.ts +2 -2
- package/src/physics/README.md +15 -2
- package/src/physics/physics-data-plugin.ts +31 -3
- package/src/physics/physics-hit.ts +21 -0
- package/src/physics/physics-query.ts +42 -0
- package/src/physics/public.ts +2 -0
- package/src/physics/solvers/jolt-solver-plugin.ts +121 -2
- package/src/physics/solvers/mutable-hit.ts +18 -0
- package/src/physics/solvers/orient-normal-against-ray.test.ts +31 -0
- package/src/physics/solvers/orient-normal-against-ray.ts +18 -0
- package/src/physics/solvers/pick-ray-conformance.test.ts +200 -0
- package/src/physics/solvers/rapier-solver-plugin.ts +94 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import initJolt from "jolt-physics";
|
|
4
4
|
import { Database, type Entity } from "@adobe/data/ecs";
|
|
5
|
+
import type { Line3, Vec3 } from "@adobe/data/math";
|
|
5
6
|
import { isVoxelShapePhysicsPending } from "../is-voxel-shape-physics-pending.js";
|
|
6
7
|
import { True } from "@adobe/data/schema";
|
|
7
8
|
import { physicsClock } from "../physics-clock-plugin.js";
|
|
@@ -11,6 +12,10 @@ import { BodyType } from "../body/body-type/body-type.js";
|
|
|
11
12
|
import { ColliderShape } from "../body/collider-shape/collider-shape.js";
|
|
12
13
|
import type { ColliderMesh } from "../body/collider-mesh.js";
|
|
13
14
|
import { massFromVolume } from "../body/collider-shape/mass-from-volume.js";
|
|
15
|
+
import type { PhysicsQuery } from "../physics-query.js";
|
|
16
|
+
import type { PhysicsHit } from "../physics-hit.js";
|
|
17
|
+
import { orientNormalAgainstRay } from "./orient-normal-against-ray.js";
|
|
18
|
+
import { newHit, type MutableHit } from "./mutable-hit.js";
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
21
|
* A third rigid-body solver behind the same `physicsData` seam — Jolt Physics
|
|
@@ -61,6 +66,8 @@ type JQuat = InstanceType<JoltModule["Quat"]>;
|
|
|
61
66
|
type JVec3 = InstanceType<JoltModule["Vec3"]>;
|
|
62
67
|
type JShape = InstanceType<JoltModule["Shape"]>;
|
|
63
68
|
type JPhysicsSystem = InstanceType<JoltModule["PhysicsSystem"]>;
|
|
69
|
+
type JRayCastResult = InstanceType<JoltModule["RayCastResult"]>;
|
|
70
|
+
type JShapeCastResult = InstanceType<JoltModule["ShapeCastResult"]>;
|
|
64
71
|
|
|
65
72
|
interface MatProps { density: number; restitution: number; friction: number }
|
|
66
73
|
|
|
@@ -78,8 +85,8 @@ export interface JoltContext {
|
|
|
78
85
|
export const joltSolver = Database.Plugin.create({
|
|
79
86
|
extends: Database.Plugin.combine(physicsData, jointData, physicsClock),
|
|
80
87
|
components: {
|
|
81
|
-
_joltBody: { ...True.schema, nonPersistent: true }, // tag: this body has been mirrored into the Jolt world
|
|
82
|
-
_joltJoint: { ...True.schema, nonPersistent: true }, // tag: this joint has been mirrored into the Jolt world
|
|
88
|
+
_joltBody: { ...True.schema, default: undefined, nonPersistent: true }, // tag: this body has been mirrored into the Jolt world
|
|
89
|
+
_joltJoint: { ...True.schema, default: undefined, nonPersistent: true }, // tag: this joint has been mirrored into the Jolt world
|
|
83
90
|
},
|
|
84
91
|
resources: {
|
|
85
92
|
_joltContext: { default: null as JoltContext | null, nonPersistent: true },
|
|
@@ -210,6 +217,7 @@ export const joltSolver = Database.Plugin.create({
|
|
|
210
217
|
}
|
|
211
218
|
}
|
|
212
219
|
const body = bi.CreateBody(settings);
|
|
220
|
+
body.SetUserData(id); // reverse lookup for ray-pick: hit body id → entity
|
|
213
221
|
bi.AddBody(body.GetID(), motion === "static" ? jolt.EActivation_DontActivate : jolt.EActivation_Activate);
|
|
214
222
|
if (motion === "dynamic" && (vx || vy || vz || wx || wy || wz)) {
|
|
215
223
|
const lv = new jolt.Vec3(vx, vy, vz), av = new jolt.Vec3(wx, wy, wz);
|
|
@@ -222,6 +230,116 @@ export const joltSolver = Database.Plugin.create({
|
|
|
222
230
|
if (half) jolt.destroy(half);
|
|
223
231
|
};
|
|
224
232
|
|
|
233
|
+
// Solver-agnostic ray-pick over the live Jolt world. As in the Rapier
|
|
234
|
+
// plugin, the segment vector (b − a) is the cast direction, so the hit
|
|
235
|
+
// fraction is the parametric α ∈ [0,1] along the segment directly
|
|
236
|
+
// (matching PhysicsHit). Accept-all filters + reusable settings are
|
|
237
|
+
// created once here; per-call WASM temporaries are freed after each pick.
|
|
238
|
+
const makeQuery = (jolt: JoltModule, ps: JPhysicsSystem, bi: JBodyInterface): PhysicsQuery => {
|
|
239
|
+
const narrow = ps.GetNarrowPhaseQuery();
|
|
240
|
+
const bpFilter = new jolt.BroadPhaseLayerFilter();
|
|
241
|
+
const objFilter = new jolt.ObjectLayerFilter();
|
|
242
|
+
const bodyFilter = new jolt.BodyFilter();
|
|
243
|
+
const shapeFilter = new jolt.ShapeFilter();
|
|
244
|
+
const raySettings = new jolt.RayCastSettings();
|
|
245
|
+
const shapeSettings = new jolt.ShapeCastSettings();
|
|
246
|
+
const baseOffset = new jolt.RVec3(0, 0, 0); // results in world space
|
|
247
|
+
const unitScale = new jolt.Vec3(1, 1, 1);
|
|
248
|
+
|
|
249
|
+
// Fill `out` from one ray result. Surface normal comes from the body
|
|
250
|
+
// we already hold (no body-lock round-trip). Reads every field into JS
|
|
251
|
+
// before any collector is destroyed. userData = entity id.
|
|
252
|
+
const fillRay = (out: MutableHit, hit: JRayCastResult, a: Vec3, dx: number, dy: number, dz: number): void => {
|
|
253
|
+
const alpha = hit.get_mFraction();
|
|
254
|
+
const entity = bi.GetUserData(hit.get_mBodyID()) as Entity;
|
|
255
|
+
out.entity = entity; out.distance = alpha;
|
|
256
|
+
out.point[0] = a[0] + dx * alpha; out.point[1] = a[1] + dy * alpha; out.point[2] = a[2] + dz * alpha;
|
|
257
|
+
out.normal[0] = 0; out.normal[1] = 0; out.normal[2] = 0;
|
|
258
|
+
const body = bodies.get(entity);
|
|
259
|
+
if (body) {
|
|
260
|
+
const p = new jolt.RVec3(out.point[0], out.point[1], out.point[2]);
|
|
261
|
+
const nv = body.GetWorldSpaceSurfaceNormal(hit.get_mSubShapeID2(), p);
|
|
262
|
+
out.normal[0] = nv.GetX(); out.normal[1] = nv.GetY(); out.normal[2] = nv.GetZ();
|
|
263
|
+
jolt.destroy(p);
|
|
264
|
+
}
|
|
265
|
+
orientNormalAgainstRay(out.normal, dx, dy, dz);
|
|
266
|
+
};
|
|
267
|
+
// Fill `out` from one swept-sphere result (contact point + penetration
|
|
268
|
+
// axis; both world-space with baseOffset = 0).
|
|
269
|
+
const fillShape = (out: MutableHit, hit: JShapeCastResult, dx: number, dy: number, dz: number): void => {
|
|
270
|
+
const cp = hit.get_mContactPointOn2();
|
|
271
|
+
out.entity = bi.GetUserData(hit.get_mBodyID2()) as Entity; out.distance = hit.get_mFraction();
|
|
272
|
+
out.point[0] = cp.GetX(); out.point[1] = cp.GetY(); out.point[2] = cp.GetZ();
|
|
273
|
+
const pax = hit.get_mPenetrationAxis();
|
|
274
|
+
out.normal[0] = pax.GetX(); out.normal[1] = pax.GetY(); out.normal[2] = pax.GetZ();
|
|
275
|
+
orientNormalAgainstRay(out.normal, dx, dy, dz);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const scratch = newHit(); // reused across castRayEach visits (zero-alloc)
|
|
279
|
+
return {
|
|
280
|
+
castRay(ray: Line3, options): PhysicsHit | null {
|
|
281
|
+
const a = ray.a, b = ray.b;
|
|
282
|
+
const dx = b[0] - a[0], dy = b[1] - a[1], dz = b[2] - a[2];
|
|
283
|
+
const out = newHit();
|
|
284
|
+
if ((options?.radius ?? 0) > 0) {
|
|
285
|
+
// sphere of `radius` swept along the segment
|
|
286
|
+
const shape = new jolt.SphereShape(options!.radius!);
|
|
287
|
+
const startPos = new jolt.RVec3(a[0], a[1], a[2]);
|
|
288
|
+
const comStart = jolt.RMat44.prototype.sTranslation(startPos);
|
|
289
|
+
const dir = new jolt.Vec3(dx, dy, dz);
|
|
290
|
+
const shapeCast = new jolt.RShapeCast(shape, unitScale, comStart, dir);
|
|
291
|
+
const collector = new jolt.CastShapeClosestHitCollisionCollector();
|
|
292
|
+
narrow.CastShape(shapeCast, shapeSettings, baseOffset, collector, bpFilter, objFilter, bodyFilter, shapeFilter);
|
|
293
|
+
const hadHit = collector.HadHit();
|
|
294
|
+
if (hadHit) fillShape(out, collector.mHit, dx, dy, dz);
|
|
295
|
+
jolt.destroy(shape); jolt.destroy(startPos); jolt.destroy(comStart); jolt.destroy(dir); jolt.destroy(shapeCast); jolt.destroy(collector);
|
|
296
|
+
return hadHit ? out : null;
|
|
297
|
+
}
|
|
298
|
+
const origin = new jolt.RVec3(a[0], a[1], a[2]);
|
|
299
|
+
const dir = new jolt.Vec3(dx, dy, dz);
|
|
300
|
+
const rc = new jolt.RRayCast(origin, dir);
|
|
301
|
+
const collector = new jolt.CastRayClosestHitCollisionCollector();
|
|
302
|
+
narrow.CastRay(rc, raySettings, collector, bpFilter, objFilter, bodyFilter, shapeFilter);
|
|
303
|
+
const hadHit = collector.HadHit();
|
|
304
|
+
if (hadHit) fillRay(out, collector.mHit, a, dx, dy, dz);
|
|
305
|
+
jolt.destroy(origin); jolt.destroy(dir); jolt.destroy(rc); jolt.destroy(collector);
|
|
306
|
+
return hadHit ? out : null;
|
|
307
|
+
},
|
|
308
|
+
castRayEach(ray: Line3, visit, options): void {
|
|
309
|
+
const a = ray.a, b = ray.b;
|
|
310
|
+
const dx = b[0] - a[0], dy = b[1] - a[1], dz = b[2] - a[2];
|
|
311
|
+
if ((options?.radius ?? 0) > 0) {
|
|
312
|
+
// Jolt has a native swept-sphere all-hits query.
|
|
313
|
+
const shape = new jolt.SphereShape(options!.radius!);
|
|
314
|
+
const startPos = new jolt.RVec3(a[0], a[1], a[2]);
|
|
315
|
+
const comStart = jolt.RMat44.prototype.sTranslation(startPos);
|
|
316
|
+
const dir = new jolt.Vec3(dx, dy, dz);
|
|
317
|
+
const shapeCast = new jolt.RShapeCast(shape, unitScale, comStart, dir);
|
|
318
|
+
const collector = new jolt.CastShapeAllHitCollisionCollector();
|
|
319
|
+
narrow.CastShape(shapeCast, shapeSettings, baseOffset, collector, bpFilter, objFilter, bodyFilter, shapeFilter);
|
|
320
|
+
if (collector.HadHit()) {
|
|
321
|
+
collector.Sort(); // near→far
|
|
322
|
+
const hits = collector.get_mHits();
|
|
323
|
+
for (let i = 0, len = hits.size(); i < len; i++) { fillShape(scratch, hits.at(i), dx, dy, dz); if (visit(scratch) === false) break; }
|
|
324
|
+
}
|
|
325
|
+
jolt.destroy(shape); jolt.destroy(startPos); jolt.destroy(comStart); jolt.destroy(dir); jolt.destroy(shapeCast); jolt.destroy(collector);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const origin = new jolt.RVec3(a[0], a[1], a[2]);
|
|
329
|
+
const dir = new jolt.Vec3(dx, dy, dz);
|
|
330
|
+
const rc = new jolt.RRayCast(origin, dir);
|
|
331
|
+
const collector = new jolt.CastRayAllHitCollisionCollector();
|
|
332
|
+
narrow.CastRay(rc, raySettings, collector, bpFilter, objFilter, bodyFilter, shapeFilter);
|
|
333
|
+
if (collector.HadHit()) {
|
|
334
|
+
collector.Sort(); // near→far
|
|
335
|
+
const hits = collector.get_mHits();
|
|
336
|
+
for (let i = 0, len = hits.size(); i < len; i++) { fillRay(scratch, hits.at(i), a, dx, dy, dz); if (visit(scratch) === false) break; }
|
|
337
|
+
}
|
|
338
|
+
jolt.destroy(origin); jolt.destroy(dir); jolt.destroy(rc); jolt.destroy(collector);
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
|
|
225
343
|
return () => {
|
|
226
344
|
if (!J || !bodyInterface || !joltInterface) {
|
|
227
345
|
if (!initStarted) {
|
|
@@ -229,6 +347,7 @@ export const joltSolver = Database.Plugin.create({
|
|
|
229
347
|
initJolt().then((j: JoltModule) => {
|
|
230
348
|
J = j; setup(j);
|
|
231
349
|
db.store.resources._joltContext = { jolt: j, physicsSystem: physicsSystem!, bodyInterface: bodyInterface!, ragdollLayer: L_RAGDOLL };
|
|
350
|
+
db.store.resources.physicsQuery = makeQuery(j, physicsSystem!, bodyInterface!);
|
|
232
351
|
});
|
|
233
352
|
}
|
|
234
353
|
return; // WASM not ready yet
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
|
|
3
|
+
import type { Entity } from "@adobe/data/ecs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal mutable form of `PhysicsHit` used by the solver query
|
|
7
|
+
* implementations. `castRay` fills a fresh one (retainable return value);
|
|
8
|
+
* `castRayEach` fills and re-visits a single reused instance (zero-alloc).
|
|
9
|
+
* Assignable to the public read-only `PhysicsHit` at the seam.
|
|
10
|
+
*/
|
|
11
|
+
export interface MutableHit {
|
|
12
|
+
entity: Entity;
|
|
13
|
+
distance: number;
|
|
14
|
+
point: [number, number, number];
|
|
15
|
+
normal: [number, number, number];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const newHit = (): MutableHit => ({ entity: 0 as Entity, distance: 0, point: [0, 0, 0], normal: [0, 0, 0] });
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
import { orientNormalAgainstRay } from "./orient-normal-against-ray.js";
|
|
5
|
+
|
|
6
|
+
describe("orientNormalAgainstRay", () => {
|
|
7
|
+
it("normalizes the normal to unit length", () => {
|
|
8
|
+
const n: [number, number, number] = [0, 3, 0];
|
|
9
|
+
orientNormalAgainstRay(n, 1, 0, 0); // ray perpendicular → no flip
|
|
10
|
+
expect(Math.hypot(...n)).toBeCloseTo(1, 6);
|
|
11
|
+
expect(n[1]).toBeCloseTo(1, 6);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("flips a normal that points along the ray so it opposes it", () => {
|
|
15
|
+
const n: [number, number, number] = [0, -1, 0]; // points down, same as a downward ray
|
|
16
|
+
orientNormalAgainstRay(n, 0, -20, 0); // downward ray
|
|
17
|
+
expect(n[1]).toBeGreaterThan(0); // now points up, opposing the ray
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("leaves a normal that already opposes the ray unchanged in direction", () => {
|
|
21
|
+
const n: [number, number, number] = [0, 1, 0];
|
|
22
|
+
orientNormalAgainstRay(n, 0, -20, 0);
|
|
23
|
+
expect(n[1]).toBeCloseTo(1, 6);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("leaves a zero-length normal as the zero vector (no divide-by-zero)", () => {
|
|
27
|
+
const n: [number, number, number] = [0, 0, 0];
|
|
28
|
+
orientNormalAgainstRay(n, 0, -1, 0);
|
|
29
|
+
expect(n).toEqual([0, 0, 0]);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize `n` in place and flip it so it opposes the ray direction
|
|
5
|
+
* `(dx,dy,dz)` — the standard picking convention (the returned normal points
|
|
6
|
+
* back toward the ray origin, i.e. outward from the hit surface).
|
|
7
|
+
*
|
|
8
|
+
* Unifies the normal convention across engines and across ray-cast vs.
|
|
9
|
+
* shape-cast, whose raw normals (surface normal, contact normal, penetration
|
|
10
|
+
* axis) don't otherwise agree on sign. A zero-length input is left as the zero
|
|
11
|
+
* vector (guarded so a degenerate hit doesn't divide by zero).
|
|
12
|
+
*/
|
|
13
|
+
export const orientNormalAgainstRay = (n: [number, number, number], dx: number, dy: number, dz: number): void => {
|
|
14
|
+
const len = Math.hypot(n[0], n[1], n[2]);
|
|
15
|
+
if (len === 0) return;
|
|
16
|
+
n[0] /= len; n[1] /= len; n[2] /= len;
|
|
17
|
+
if (n[0] * dx + n[1] * dy + n[2] * dz > 0) { n[0] = -n[0]; n[1] = -n[1]; n[2] = -n[2]; }
|
|
18
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
4
|
+
import { Database } from "@adobe/data/ecs";
|
|
5
|
+
import type { Line3 } from "@adobe/data/math";
|
|
6
|
+
import { rapierSolver } from "./rapier-solver-plugin.js";
|
|
7
|
+
import { joltSolver } from "./jolt-solver-plugin.js";
|
|
8
|
+
import type { PhysicsHit } from "../physics-hit.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Abstract picking conformance suite: the SAME expected outcomes are asserted
|
|
12
|
+
* against every solver behind the `physicsData` seam. Building a fixed static
|
|
13
|
+
* scene and running identical `pickRay` calls proves both engines fulfil the
|
|
14
|
+
* `PhysicsQuery` contract interchangeably (entity, parametric α, hit point,
|
|
15
|
+
* and a ray-opposing normal), including the with/without-radius variants.
|
|
16
|
+
*
|
|
17
|
+
* npx vitest --run src/physics/solvers/pick-ray-conformance.test.ts
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// The solver plugin extends `physicsData`, so the StaticCollider archetype, the
|
|
21
|
+
// `frameTime` resource, and the `pickRay` action all exist on the created db.
|
|
22
|
+
// The scene is driven dynamically, so the db is used through a loose shape
|
|
23
|
+
// (runtime invariant: these members exist on any physicsData solver).
|
|
24
|
+
interface LooseDb {
|
|
25
|
+
store: {
|
|
26
|
+
archetypes: Record<string, { insert(v: unknown): number }>;
|
|
27
|
+
resources: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
system: { functions: Record<string, () => unknown>; order?: string[][] };
|
|
30
|
+
actions: {
|
|
31
|
+
pickRay(args: { ray: Line3; radius?: number }): PhysicsHit | null;
|
|
32
|
+
pickRayEach(args: { ray: Line3; visit: (hit: PhysicsHit) => boolean | void; radius?: number }): void;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const yieldToEventLoop = () => new Promise<void>(r => setTimeout(r, 0));
|
|
37
|
+
|
|
38
|
+
interface Scene { db: LooseDb; floor: number; ball: number; box: number }
|
|
39
|
+
|
|
40
|
+
// Build a fixed scene of static colliders and drive the sim until the solver's
|
|
41
|
+
// WASM world is live, the bodies are mirrored, and the query pipeline is
|
|
42
|
+
// populated (both engines need at least one step for that). Picks are read-only,
|
|
43
|
+
// so one scene is shared across all cases for a solver.
|
|
44
|
+
async function buildScene(solver: Database.Plugin): Promise<Scene> {
|
|
45
|
+
const db = Database.create(solver) as unknown as LooseDb;
|
|
46
|
+
|
|
47
|
+
// Floor: wide thick box, top face at y = 0.
|
|
48
|
+
const floor = db.store.archetypes.StaticCollider.insert({
|
|
49
|
+
colliderShape: "box", halfExtents: [10, 1, 10], material: 0,
|
|
50
|
+
position: [0, -1, 0], rotation: [0, 0, 0, 1],
|
|
51
|
+
});
|
|
52
|
+
// Sphere radius 1 centred at (0, 3, 0) → top at y = 4.
|
|
53
|
+
const ball = db.store.archetypes.StaticCollider.insert({
|
|
54
|
+
colliderShape: "sphere", halfExtents: [1, 0, 0], material: 0,
|
|
55
|
+
position: [0, 3, 0], rotation: [0, 0, 0, 1],
|
|
56
|
+
});
|
|
57
|
+
// Unit box centred at (5, 3, 0) → top at y = 4, off to the side.
|
|
58
|
+
const box = db.store.archetypes.StaticCollider.insert({
|
|
59
|
+
colliderShape: "box", halfExtents: [1, 1, 1], material: 0,
|
|
60
|
+
position: [5, 3, 0], rotation: [0, 0, 0, 1],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const order = db.system.order;
|
|
64
|
+
const names = order ? order.flat() : Object.keys(db.system.functions);
|
|
65
|
+
const runNames = names.filter(n => n !== "_frameTime" && n !== "schedulerSystem");
|
|
66
|
+
const dt = 1 / 60;
|
|
67
|
+
const tick = (f: number): void => {
|
|
68
|
+
db.store.resources.frameTime = { now: f * dt * 1000, dt, elapsed: f * dt };
|
|
69
|
+
for (const n of runNames) { const fn = db.system.functions[n]; if (fn) fn(); }
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Drive until the solver publishes its query, then a few more steps so the
|
|
73
|
+
// bodies are mirrored and the broad/narrow phase is populated.
|
|
74
|
+
let f = 0;
|
|
75
|
+
for (; f < 300 && db.store.resources.physicsQuery == null; f++) { tick(f); await yieldToEventLoop(); }
|
|
76
|
+
for (let k = 0; k < 30; k++, f++) { tick(f); await yieldToEventLoop(); }
|
|
77
|
+
|
|
78
|
+
return { db, floor, ball, box };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function describePicking(name: string, solver: Database.Plugin): void {
|
|
82
|
+
describe(`pickRay conformance — ${name}`, () => {
|
|
83
|
+
let scene: Scene;
|
|
84
|
+
beforeAll(async () => { scene = await buildScene(solver); }, 60_000);
|
|
85
|
+
|
|
86
|
+
it("publishes a physicsQuery once the world is live", () => {
|
|
87
|
+
expect((scene.db.store.resources as { physicsQuery: unknown }).physicsQuery).not.toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("hits the sphere with a ray straight down through its centre", () => {
|
|
91
|
+
// segment y: 10 → −10 (span 20); sphere top at y = 4 ⇒ α = (10 − 4) / 20 = 0.3
|
|
92
|
+
const ray: Line3 = { a: [0, 10, 0], b: [0, -10, 0] };
|
|
93
|
+
const hit = scene.db.actions.pickRay({ ray });
|
|
94
|
+
expect(hit).not.toBeNull();
|
|
95
|
+
expect(hit!.entity).toBe(scene.ball);
|
|
96
|
+
expect(hit!.distance).toBeCloseTo(0.3, 1);
|
|
97
|
+
expect(hit!.point[1]).toBeCloseTo(4, 1);
|
|
98
|
+
// ray points down, so the surface normal points up (opposes the ray)
|
|
99
|
+
expect(hit!.normal[1]).toBeGreaterThan(0.5);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("hits the box off to the side", () => {
|
|
103
|
+
const ray: Line3 = { a: [5, 10, 0], b: [5, -10, 0] };
|
|
104
|
+
const hit = scene.db.actions.pickRay({ ray });
|
|
105
|
+
expect(hit).not.toBeNull();
|
|
106
|
+
expect(hit!.entity).toBe(scene.box);
|
|
107
|
+
expect(hit!.point[1]).toBeCloseTo(4, 1); // box top face
|
|
108
|
+
expect(hit!.normal[1]).toBeGreaterThan(0.5);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns the nearest collider when several lie along the ray", () => {
|
|
112
|
+
// straight down through the centre passes the sphere (top y = 4) before
|
|
113
|
+
// the floor (top y = 0) → the nearer sphere wins
|
|
114
|
+
const hit = scene.db.actions.pickRay({ ray: { a: [0, 10, 0], b: [0, -10, 0] } });
|
|
115
|
+
expect(hit!.entity).toBe(scene.ball);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("returns null when the ray misses every collider", () => {
|
|
119
|
+
const ray: Line3 = { a: [100, 10, 100], b: [100, -10, 100] };
|
|
120
|
+
expect(scene.db.actions.pickRay({ ray })).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("with/without radius: a grazing ray misses as a line but hits as a thick ray", () => {
|
|
124
|
+
// Short segment offset 1.4 in x from the sphere centre, kept above the
|
|
125
|
+
// floor (y: 5 → 2.5). As a thin line it clears the sphere (1.4 > radius 1)
|
|
126
|
+
// and never reaches the floor → miss. A 0.5-radius sweep has effective
|
|
127
|
+
// reach 1.5 > 1.4 → it grazes the sphere.
|
|
128
|
+
const ray: Line3 = { a: [1.4, 5, 0], b: [1.4, 2.5, 0] };
|
|
129
|
+
expect(scene.db.actions.pickRay({ ray })).toBeNull();
|
|
130
|
+
|
|
131
|
+
const hit = scene.db.actions.pickRay({ ray, radius: 0.5 });
|
|
132
|
+
expect(hit).not.toBeNull();
|
|
133
|
+
expect(hit!.entity).toBe(scene.ball);
|
|
134
|
+
expect(hit!.distance).toBeGreaterThanOrEqual(0);
|
|
135
|
+
expect(hit!.distance).toBeLessThanOrEqual(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("castRayEach visits every collider along the ray, nearest first", () => {
|
|
139
|
+
// straight down at x = 0 crosses the sphere (top y = 4, α = 0.3) then the
|
|
140
|
+
// floor (top y = 0, α = 0.5); the box at x = 5 is not on the ray
|
|
141
|
+
const seen: number[] = [];
|
|
142
|
+
const dists: number[] = [];
|
|
143
|
+
scene.db.actions.pickRayEach({
|
|
144
|
+
ray: { a: [0, 10, 0], b: [0, -10, 0] },
|
|
145
|
+
visit: (h) => { seen.push(h.entity); dists.push(h.distance); },
|
|
146
|
+
});
|
|
147
|
+
expect(seen).toEqual([scene.ball, scene.floor]);
|
|
148
|
+
expect(dists).toEqual([...dists].sort((x, y) => x - y)); // ascending
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("castRayEach stops early when visit returns false", () => {
|
|
152
|
+
const seen: number[] = [];
|
|
153
|
+
scene.db.actions.pickRayEach({
|
|
154
|
+
ray: { a: [0, 10, 0], b: [0, -10, 0] },
|
|
155
|
+
visit: (h) => { seen.push(h.entity); return false; }, // stop after the nearest
|
|
156
|
+
});
|
|
157
|
+
expect(seen).toEqual([scene.ball]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("castRayEach visits nothing when the ray misses everything", () => {
|
|
161
|
+
let count = 0;
|
|
162
|
+
scene.db.actions.pickRayEach({
|
|
163
|
+
ray: { a: [100, 10, 100], b: [100, -10, 100] },
|
|
164
|
+
visit: () => { count++; },
|
|
165
|
+
});
|
|
166
|
+
expect(count).toBe(0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("castRayEach with radius visits at least the nearest swept hit on every solver", () => {
|
|
170
|
+
// The Rapier compat binding can't sweep-all, so radius degrades to the
|
|
171
|
+
// nearest there; Jolt returns all. Both must at least visit the nearest.
|
|
172
|
+
const seen: number[] = [];
|
|
173
|
+
scene.db.actions.pickRayEach({
|
|
174
|
+
ray: { a: [1.4, 5, 0], b: [1.4, 2.5, 0] },
|
|
175
|
+
radius: 0.5,
|
|
176
|
+
visit: (h) => { seen.push(h.entity); },
|
|
177
|
+
});
|
|
178
|
+
expect(seen.length).toBeGreaterThanOrEqual(1);
|
|
179
|
+
expect(seen[0]).toBe(scene.ball);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("castRayEach reuses a single hit instance across visits (zero-alloc contract)", () => {
|
|
183
|
+
const refs: PhysicsHit[] = [];
|
|
184
|
+
scene.db.actions.pickRayEach({ ray: { a: [0, 10, 0], b: [0, -10, 0] }, visit: (h) => { refs.push(h); } });
|
|
185
|
+
expect(refs.length).toBe(2);
|
|
186
|
+
expect(refs[0]).toBe(refs[1]); // same object, refilled per visit — do not retain
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("castRay returns fresh, independent hits (safe to retain)", () => {
|
|
190
|
+
const h1 = scene.db.actions.pickRay({ ray: { a: [0, 10, 0], b: [0, -10, 0] } });
|
|
191
|
+
const h2 = scene.db.actions.pickRay({ ray: { a: [5, 10, 0], b: [5, -10, 0] } });
|
|
192
|
+
expect(h1).not.toBe(h2);
|
|
193
|
+
expect(h1!.entity).toBe(scene.ball); // unaffected by the second pick
|
|
194
|
+
expect(h2!.entity).toBe(scene.box);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
describePicking("rapierSolver", rapierSolver);
|
|
200
|
+
describePicking("joltSolver", joltSolver);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import RAPIER from "@dimforge/rapier3d-compat";
|
|
4
4
|
import { Database, type Entity } from "@adobe/data/ecs";
|
|
5
|
+
import type { Line3, Vec3 } from "@adobe/data/math";
|
|
5
6
|
import { isVoxelShapePhysicsPending } from "../is-voxel-shape-physics-pending.js";
|
|
6
7
|
import { True } from "@adobe/data/schema";
|
|
7
8
|
import { physicsClock } from "../physics-clock-plugin.js";
|
|
@@ -9,6 +10,10 @@ import { physicsData } from "../physics-data-plugin.js";
|
|
|
9
10
|
import { jointData } from "../joint/joint-plugin.js";
|
|
10
11
|
import { BodyType } from "../body/body-type/body-type.js";
|
|
11
12
|
import { ColliderShape } from "../body/collider-shape/collider-shape.js";
|
|
13
|
+
import type { PhysicsQuery } from "../physics-query.js";
|
|
14
|
+
import type { PhysicsHit } from "../physics-hit.js";
|
|
15
|
+
import { orientNormalAgainstRay } from "./orient-normal-against-ray.js";
|
|
16
|
+
import { newHit, type MutableHit } from "./mutable-hit.js";
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* A second rigid-body solver behind the same `physicsData` seam — the
|
|
@@ -48,8 +53,8 @@ interface MatProps { density: number; restitution: number; friction: number }
|
|
|
48
53
|
export const rapierSolver = Database.Plugin.create({
|
|
49
54
|
extends: Database.Plugin.combine(physicsData, jointData, physicsClock),
|
|
50
55
|
components: {
|
|
51
|
-
_rapierBody: { ...True.schema, nonPersistent: true }, // tag: this body has been mirrored into the Rapier world
|
|
52
|
-
_rapierJoint: { ...True.schema, nonPersistent: true }, // tag: this joint has been mirrored into the Rapier world
|
|
56
|
+
_rapierBody: { ...True.schema, default: undefined, nonPersistent: true }, // tag: this body has been mirrored into the Rapier world
|
|
57
|
+
_rapierJoint: { ...True.schema, default: undefined, nonPersistent: true }, // tag: this joint has been mirrored into the Rapier world
|
|
53
58
|
},
|
|
54
59
|
systems: {
|
|
55
60
|
rapierStep: {
|
|
@@ -84,6 +89,7 @@ export const rapierSolver = Database.Plugin.create({
|
|
|
84
89
|
desc.setTranslation(px, py, pz).setRotation({ x: q[0], y: q[1], z: q[2], w: q[3] });
|
|
85
90
|
if (motion === "dynamic") { desc.setLinvel(vx, vy, vz); desc.setAngvel({ x: wx, y: wy, z: wz }); }
|
|
86
91
|
const body = world.createRigidBody(desc);
|
|
92
|
+
body.userData = id; // reverse lookup for ray-pick: collider → parent body → entity
|
|
87
93
|
// capsule: Y-aligned, halfHeight = cylinder half (hy), radius = hx.
|
|
88
94
|
// hull: convex hull of the authored point cloud (read once here).
|
|
89
95
|
let col: RAPIER.ColliderDesc | null;
|
|
@@ -107,11 +113,96 @@ export const rapierSolver = Database.Plugin.create({
|
|
|
107
113
|
bodies.set(id, body);
|
|
108
114
|
};
|
|
109
115
|
|
|
116
|
+
// Solver-agnostic ray-pick over the live Rapier world. Passing the
|
|
117
|
+
// segment vector (b − a) as the ray/shape-cast direction and capping
|
|
118
|
+
// the time-of-impact at 1 makes the returned toi the parametric α ∈
|
|
119
|
+
// [0,1] along the segment directly (matching PhysicsHit's convention).
|
|
120
|
+
const makeQuery = (w: RAPIER.World): PhysicsQuery => {
|
|
121
|
+
// Fill `out` with a thin-ray hit. userData was set to the entity id
|
|
122
|
+
// when the body was mirrored. Normal is oriented to oppose the ray.
|
|
123
|
+
const fillRay = (out: MutableHit, entity: Entity, alpha: number, nx: number, ny: number, nz: number, a: Vec3, dx: number, dy: number, dz: number): void => {
|
|
124
|
+
out.entity = entity; out.distance = alpha;
|
|
125
|
+
out.point[0] = a[0] + dx * alpha; out.point[1] = a[1] + dy * alpha; out.point[2] = a[2] + dz * alpha;
|
|
126
|
+
out.normal[0] = nx; out.normal[1] = ny; out.normal[2] = nz;
|
|
127
|
+
orientNormalAgainstRay(out.normal, dx, dy, dz);
|
|
128
|
+
};
|
|
129
|
+
const nearest = (ray: Line3, options?: { radius?: number }): PhysicsHit | null => {
|
|
130
|
+
const a = ray.a, b = ray.b;
|
|
131
|
+
const dx = b[0] - a[0], dy = b[1] - a[1], dz = b[2] - a[2];
|
|
132
|
+
const radius = options?.radius ?? 0;
|
|
133
|
+
const out = newHit();
|
|
134
|
+
if (radius > 0) {
|
|
135
|
+
// sphere of `radius` swept along the segment
|
|
136
|
+
const hit = w.castShape(
|
|
137
|
+
{ x: a[0], y: a[1], z: a[2] }, { x: 0, y: 0, z: 0, w: 1 }, { x: dx, y: dy, z: dz },
|
|
138
|
+
new RAPIER.Ball(radius), 0, 1, true,
|
|
139
|
+
);
|
|
140
|
+
if (!hit) return null;
|
|
141
|
+
const bod = hit.collider.parent();
|
|
142
|
+
if (!bod) return null;
|
|
143
|
+
const alpha = hit.time_of_impact;
|
|
144
|
+
out.entity = bod.userData as Entity; out.distance = alpha;
|
|
145
|
+
out.normal[0] = hit.normal1.x; out.normal[1] = hit.normal1.y; out.normal[2] = hit.normal1.z;
|
|
146
|
+
orientNormalAgainstRay(out.normal, dx, dy, dz);
|
|
147
|
+
// pull the swept-sphere centre at impact back onto the collider surface
|
|
148
|
+
out.point[0] = a[0] + dx * alpha - out.normal[0] * radius;
|
|
149
|
+
out.point[1] = a[1] + dy * alpha - out.normal[1] * radius;
|
|
150
|
+
out.point[2] = a[2] + dz * alpha - out.normal[2] * radius;
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
const hit = w.castRayAndGetNormal(new RAPIER.Ray({ x: a[0], y: a[1], z: a[2] }, { x: dx, y: dy, z: dz }), 1, true);
|
|
154
|
+
if (!hit) return null;
|
|
155
|
+
const bod = hit.collider.parent();
|
|
156
|
+
if (!bod) return null;
|
|
157
|
+
fillRay(out, bod.userData as Entity, hit.timeOfImpact, hit.normal.x, hit.normal.y, hit.normal.z, a, dx, dy, dz);
|
|
158
|
+
return out;
|
|
159
|
+
};
|
|
160
|
+
// Zero-alloc scratch for castRayEach: one reused hit + a grow-only pool
|
|
161
|
+
// of records. intersectionsWithRay reports crossings in broadphase-
|
|
162
|
+
// arbitrary order, so records are collected then index-sorted near→far.
|
|
163
|
+
const scratch = newHit();
|
|
164
|
+
const pool: { entity: Entity; distance: number; nx: number; ny: number; nz: number }[] = [];
|
|
165
|
+
const order: number[] = [];
|
|
166
|
+
return {
|
|
167
|
+
castRay: nearest,
|
|
168
|
+
castRayEach(ray: Line3, visit, options): void {
|
|
169
|
+
// The compat binding has no swept-shape all-hits query, so a
|
|
170
|
+
// radius query degrades to the nearest hit (see PhysicsQuery).
|
|
171
|
+
if ((options?.radius ?? 0) > 0) { const h = nearest(ray, options); if (h) visit(h); return; }
|
|
172
|
+
const a = ray.a, b = ray.b;
|
|
173
|
+
const dx = b[0] - a[0], dy = b[1] - a[1], dz = b[2] - a[2];
|
|
174
|
+
let count = 0;
|
|
175
|
+
w.intersectionsWithRay(new RAPIER.Ray({ x: a[0], y: a[1], z: a[2] }, { x: dx, y: dy, z: dz }), 1, true, (isect: RAPIER.RayColliderIntersection) => {
|
|
176
|
+
const bod = isect.collider.parent();
|
|
177
|
+
if (bod) {
|
|
178
|
+
let rec = pool[count];
|
|
179
|
+
if (!rec) { rec = { entity: 0 as Entity, distance: 0, nx: 0, ny: 0, nz: 0 }; pool[count] = rec; }
|
|
180
|
+
rec.entity = bod.userData as Entity; rec.distance = isect.timeOfImpact;
|
|
181
|
+
rec.nx = isect.normal.x; rec.ny = isect.normal.y; rec.nz = isect.normal.z;
|
|
182
|
+
count++;
|
|
183
|
+
}
|
|
184
|
+
return true; // keep collecting
|
|
185
|
+
});
|
|
186
|
+
for (let i = 0; i < count; i++) order[i] = i;
|
|
187
|
+
order.length = count;
|
|
188
|
+
order.sort((i, j) => pool[i].distance - pool[j].distance);
|
|
189
|
+
for (let k = 0; k < count; k++) {
|
|
190
|
+
const rec = pool[order[k]];
|
|
191
|
+
fillRay(scratch, rec.entity, rec.distance, rec.nx, rec.ny, rec.nz, a, dx, dy, dz);
|
|
192
|
+
if (visit(scratch) === false) break;
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
|
|
110
198
|
return () => {
|
|
111
199
|
if (!world) {
|
|
112
200
|
if (!initStarted) {
|
|
113
201
|
initStarted = true;
|
|
114
|
-
RAPIER.init().then(() => {
|
|
202
|
+
RAPIER.init().then(() => {
|
|
203
|
+
world = new RAPIER.World({ x: 0, y: -GRAVITY, z: 0 });
|
|
204
|
+
db.store.resources.physicsQuery = makeQuery(world);
|
|
205
|
+
});
|
|
115
206
|
}
|
|
116
207
|
return; // WASM not ready yet
|
|
117
208
|
}
|