@realitycollective/threejs-interactions 0.1.0-preview.3 → 0.1.0-preview.4
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/CHANGELOG.md +7 -0
- package/README.md +25 -2
- package/dist/hit-tester.d.ts +18 -4
- package/dist/hit-tester.js +63 -30
- package/dist/hit-tester.js.map +1 -1
- package/package.json +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -33,11 +33,18 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
33
33
|
|
|
34
34
|
- `@realitycollective/iwsdk-interactions` targets **IWSDK 0.5.x**. Its peer range is now `>=0.5.0 <0.6.0` and it is developed and tested against `@iwsdk/core` 0.5.3. The adapter is source-compatible with 0.4.x and needed no code change, but 0.5 is the only line exercised, so it is the only line supported. Every symbol and runtime member the provider uses is present in both, and it already reads `world.input.xr.gamepads` rather than the accessor 0.5 deprecates.
|
|
35
35
|
- `demos/playground` no longer compiles UIKitML at build time. `@iwsdk/vite-plugin-uikitml` was discontinued at 0.4.2 and only ever did `JSON.stringify(parse(source))`, so the panel is served as `.uikitml` from `public/ui/` and parsed on load. The demo drops that plugin, and its station markup uses `rgba()` rather than `background-opacity`, which the 0.5 parser removed.
|
|
36
|
+
- `@realitycollective/iwsdk-interactions` - the approximate hit tester the bridge keeps for gaze and poke targeting resolves each registered entity's world position once per frame instead of on every query. The core asks it five questions a frame (a poke and a ray test per side, plus the gaze ray) and each one walked every entity's parent chain again through `getWorldPosition`; the bridge now calls `beginFrame()` before the runtime ticks and the five answers come from one resolution. Measured 3.6x to 4x faster on the tester in the gate-1 benchmarks, 32 us to 9 us a frame at 50 entities on desktop, with the same answers. Its ray test also no longer allocates a tuple per entity per query.
|
|
37
|
+
- `@realitycollective/threejs-interactions` - `ThreeHitTester` uses three-mesh-bvh when the app has installed its prototype hooks, which IWSDK does by default: geometries are given a bounds tree as they are registered and the raycaster asks for the first hit only, so a ray against a detailed mesh costs microseconds instead of the near-millisecond a plain triangle walk takes. `three-mesh-bvh` is declared as an optional peer; without it the plain raycast is used and the README explains collider proxies. The roots list handed to the raycaster and the intersections array are now reused between calls rather than built per call, and `hitProximity` reads each object's position and scale straight off its world matrix after one matrix update instead of two updates and a decompose, measured 4x faster at 50 registered objects.
|
|
38
|
+
- `@realitycollective/webxr-interactions` - `onSample`, `getSource` and the pointer bridge document the snapshot ownership rule `@realitycollective/webxr-input` 0.1.3 states: a snapshot handed over is the receiver's to keep and is never written to again. The velocity tracker and the pointer bridge already relied on it.
|
|
36
39
|
|
|
37
40
|
### Deprecated
|
|
38
41
|
|
|
39
42
|
- `@realitycollective/webxr-interactions` - `InteractionRuntime.onSourcesSampled` is deprecated in favour of `onSample`. They are the same stream, and since `InputSourceSnapshot` carries the velocity fields itself the two signatures are now identical, so only one name is needed. Nothing is removed: existing callers keep working and the method will go in a later major release.
|
|
40
43
|
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- `@realitycollective/threejs-interactions` - `ThreeHitTester.hitProximity` reported the object's world scale as the hit `point`. The bounding-radius lookup reused the scratch vector holding the world position, so a box at (2, 1, -3) came back with `point: [1, 1, 1]`. The core only reads the id, so targeting was unaffected; anything reading `point` from a proximity hit on three.js or XR Blocks got the wrong answer.
|
|
47
|
+
|
|
41
48
|
### Notes
|
|
42
49
|
|
|
43
50
|
- All five packages depend on `@realitycollective/webxr-input`, released independently from the [WebXR-Input](https://github.com/realitycollective/WebXR-Input) repository. That package must be published before this one.
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ It re-exports everything from [`@realitycollective/webxr-interactions`](https://
|
|
|
13
13
|
| Layer | Detail |
|
|
14
14
|
| --- | --- |
|
|
15
15
|
| **Input** | Raw WebXR, the browser's own XR API: controllers, hand joints, trigger and grip pressure, and haptic pulses |
|
|
16
|
-
| **Hit-testing** | three.js raycasting against your scene graph |
|
|
16
|
+
| **Hit-testing** | three.js raycasting against your scene graph, through three-mesh-bvh when your app installs it |
|
|
17
17
|
| **Movement** | Moves and rotates three.js objects for grab, hinge, dial and slide |
|
|
18
18
|
| **Desktop** | A mouse fallback, so the same scene is testable without a headset |
|
|
19
19
|
| **Presence** | Show and hide the user's own hand and controller models, once you have registered them |
|
|
@@ -53,10 +53,33 @@ renderer.setAnimationLoop(() => {
|
|
|
53
53
|
});
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
##
|
|
56
|
+
## Hit-testing detailed meshes
|
|
57
|
+
|
|
58
|
+
three.js tests every triangle of a mesh once a ray enters its bounding sphere, so a detailed model registered as an interactable costs the raycast close to a millisecond per ray on a desktop, and the runtime casts up to three rays a frame (one per controller, one for gaze). Two ways round it, and they combine.
|
|
59
|
+
|
|
60
|
+
**Install three-mesh-bvh.** The adapter declares it as an optional peer. Install it and add its prototype hooks once at startup; the hit tester then builds a bounds tree for every geometry it registers and asks the raycaster for the first hit only, so the same ray costs microseconds. IWSDK installs these hooks itself, so an IWSDK app has nothing to do.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { BufferGeometry, Mesh } from "three";
|
|
64
|
+
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from "three-mesh-bvh";
|
|
65
|
+
|
|
66
|
+
BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
|
|
67
|
+
BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
|
|
68
|
+
Mesh.prototype.raycast = acceleratedRaycast;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The tree lives on the geometry for as long as the geometry does. The adapter never removes it, so a geometry shared between interactables is built once, and a geometry you discard takes its tree with it.
|
|
72
|
+
|
|
73
|
+
**Register a collider proxy.** Register a low-poly stand-in (a box, a sphere, a simplified hull) as the interactable object, and parent the detailed model to it or move both from the same transform. The proxy is what the ray and the poke test see; the model is what the player sees.
|
|
74
|
+
|
|
75
|
+
Poke targeting (`hitProximity`) is a sphere test on each registered object's world position and bounding-sphere radius, so it costs the same whatever the mesh.
|
|
76
|
+
|
|
77
|
+
## Peer dependencies
|
|
57
78
|
|
|
58
79
|
`three >= 0.170.0`. The Reality Collective demos pin the `super-three@0.181` fork that Meta's IWSDK mandates; stock three.js works equally well for this adapter.
|
|
59
80
|
|
|
81
|
+
`three-mesh-bvh >= 0.9.14`, optional. See the section above.
|
|
82
|
+
|
|
60
83
|
## Live demo
|
|
61
84
|
|
|
62
85
|
The interaction playground - the full station set, mouse-capable on desktop, VR button for headsets: **[webxr-interactions.pages.dev](https://webxr-interactions.pages.dev)**
|
package/dist/hit-tester.d.ts
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
* three.js objects. Ray hits use a Raycaster over the registered roots
|
|
4
4
|
* (recursive); proximity uses world-position distance minus an
|
|
5
5
|
* approximate bounding radius, good enough for poke targets.
|
|
6
|
+
*
|
|
7
|
+
* Detailed meshes are the cost to watch. three.js tests every triangle of
|
|
8
|
+
* a mesh once a ray enters its bounding sphere, so a 20k-triangle
|
|
9
|
+
* interactable costs close to a millisecond per ray. When the app has
|
|
10
|
+
* installed three-mesh-bvh's prototype hooks -
|
|
11
|
+
* `BufferGeometry.prototype.computeBoundsTree` and
|
|
12
|
+
* `Mesh.prototype.raycast = acceleratedRaycast`, which IWSDK does for you -
|
|
13
|
+
* the tester builds a bounds tree for every geometry it registers and asks
|
|
14
|
+
* the raycaster for the first hit only, so the same ray costs microseconds.
|
|
15
|
+
* Without the hooks it falls back to the plain raycast, and a detailed
|
|
16
|
+
* interactable should be registered through a low-poly collider proxy
|
|
17
|
+
* instead (see the README).
|
|
6
18
|
*/
|
|
7
19
|
import { type Object3D } from "three";
|
|
8
20
|
import type { RayTuple, Vec3Tuple } from "@realitycollective/webxr-input";
|
|
@@ -10,15 +22,17 @@ import type { HitTester, InteractableHit } from "@realitycollective/webxr-intera
|
|
|
10
22
|
export declare class ThreeHitTester implements HitTester {
|
|
11
23
|
private readonly roots;
|
|
12
24
|
private readonly byObject;
|
|
25
|
+
/** `roots` as the array the raycaster takes, rebuilt only when the set changes. */
|
|
26
|
+
private rootList;
|
|
13
27
|
private readonly raycaster;
|
|
14
|
-
|
|
15
|
-
private readonly
|
|
16
|
-
|
|
28
|
+
/** Reused between calls; three.js appends into it and sorts it. */
|
|
29
|
+
private readonly intersections;
|
|
30
|
+
constructor();
|
|
17
31
|
register(id: string, object: Object3D): void;
|
|
18
32
|
unregister(id: string): void;
|
|
19
33
|
getObject(id: string): Object3D | undefined;
|
|
20
34
|
hitRay(ray: RayTuple): InteractableHit | null;
|
|
21
35
|
hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;
|
|
36
|
+
private objects;
|
|
22
37
|
private ownerOf;
|
|
23
|
-
private boundingRadius;
|
|
24
38
|
}
|
package/dist/hit-tester.js
CHANGED
|
@@ -3,24 +3,55 @@
|
|
|
3
3
|
* three.js objects. Ray hits use a Raycaster over the registered roots
|
|
4
4
|
* (recursive); proximity uses world-position distance minus an
|
|
5
5
|
* approximate bounding radius, good enough for poke targets.
|
|
6
|
+
*
|
|
7
|
+
* Detailed meshes are the cost to watch. three.js tests every triangle of
|
|
8
|
+
* a mesh once a ray enters its bounding sphere, so a 20k-triangle
|
|
9
|
+
* interactable costs close to a millisecond per ray. When the app has
|
|
10
|
+
* installed three-mesh-bvh's prototype hooks -
|
|
11
|
+
* `BufferGeometry.prototype.computeBoundsTree` and
|
|
12
|
+
* `Mesh.prototype.raycast = acceleratedRaycast`, which IWSDK does for you -
|
|
13
|
+
* the tester builds a bounds tree for every geometry it registers and asks
|
|
14
|
+
* the raycaster for the first hit only, so the same ray costs microseconds.
|
|
15
|
+
* Without the hooks it falls back to the plain raycast, and a detailed
|
|
16
|
+
* interactable should be registered through a low-poly collider proxy
|
|
17
|
+
* instead (see the README).
|
|
6
18
|
*/
|
|
7
|
-
import { Raycaster
|
|
19
|
+
import { Raycaster } from "three";
|
|
8
20
|
export class ThreeHitTester {
|
|
9
21
|
roots = new Map();
|
|
10
22
|
byObject = new WeakMap();
|
|
23
|
+
/** `roots` as the array the raycaster takes, rebuilt only when the set changes. */
|
|
24
|
+
rootList = null;
|
|
11
25
|
raycaster = new Raycaster();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
/** Reused between calls; three.js appends into it and sorts it. */
|
|
27
|
+
intersections = [];
|
|
28
|
+
constructor() {
|
|
29
|
+
// Read by three-mesh-bvh's accelerated raycast, ignored by the stock one.
|
|
30
|
+
this.raycaster.firstHitOnly = true;
|
|
31
|
+
}
|
|
15
32
|
register(id, object) {
|
|
33
|
+
this.unregister(id);
|
|
34
|
+
object.traverse((child) => {
|
|
35
|
+
const geometry = child.geometry;
|
|
36
|
+
// `disposeBoundsTree` leaves null behind, a never-built geometry has
|
|
37
|
+
// undefined: both mean there is no tree to use.
|
|
38
|
+
if (geometry &&
|
|
39
|
+
typeof geometry.computeBoundsTree === "function" &&
|
|
40
|
+
(geometry.boundsTree === undefined || geometry.boundsTree === null)) {
|
|
41
|
+
geometry.computeBoundsTree();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
16
44
|
this.roots.set(id, object);
|
|
17
45
|
this.byObject.set(object, id);
|
|
46
|
+
this.rootList = null;
|
|
18
47
|
}
|
|
19
48
|
unregister(id) {
|
|
20
49
|
const object = this.roots.get(id);
|
|
21
|
-
if (object)
|
|
22
|
-
|
|
50
|
+
if (!object)
|
|
51
|
+
return;
|
|
52
|
+
this.byObject.delete(object);
|
|
23
53
|
this.roots.delete(id);
|
|
54
|
+
this.rootList = null;
|
|
24
55
|
}
|
|
25
56
|
getObject(id) {
|
|
26
57
|
return this.roots.get(id);
|
|
@@ -28,10 +59,11 @@ export class ThreeHitTester {
|
|
|
28
59
|
hitRay(ray) {
|
|
29
60
|
if (this.roots.size === 0)
|
|
30
61
|
return null;
|
|
31
|
-
this.raycaster.ray.origin.set(
|
|
32
|
-
this.raycaster.ray.direction.set(
|
|
33
|
-
const
|
|
34
|
-
|
|
62
|
+
this.raycaster.ray.origin.set(ray.origin[0], ray.origin[1], ray.origin[2]);
|
|
63
|
+
this.raycaster.ray.direction.set(ray.direction[0], ray.direction[1], ray.direction[2]);
|
|
64
|
+
const intersections = this.intersections;
|
|
65
|
+
intersections.length = 0;
|
|
66
|
+
this.raycaster.intersectObjects(this.objects(), true, intersections);
|
|
35
67
|
for (const intersection of intersections) {
|
|
36
68
|
const id = this.ownerOf(intersection.object);
|
|
37
69
|
if (id !== null) {
|
|
@@ -45,21 +77,26 @@ export class ThreeHitTester {
|
|
|
45
77
|
return null;
|
|
46
78
|
}
|
|
47
79
|
hitProximity(point, radius) {
|
|
48
|
-
this.v.set(...point);
|
|
49
80
|
let best = null;
|
|
50
81
|
for (const [id, object] of this.roots) {
|
|
51
|
-
object
|
|
52
|
-
|
|
82
|
+
// One matrix update per object, then position and scale straight off
|
|
83
|
+
// the world matrix: the last column is the translation and the length
|
|
84
|
+
// of each of the first three columns is that axis's scale.
|
|
85
|
+
object.updateWorldMatrix(true, false);
|
|
86
|
+
const e = object.matrixWorld.elements;
|
|
87
|
+
const scale = Math.max(Math.hypot(e[0], e[1], e[2]), Math.hypot(e[4], e[5], e[6]), Math.hypot(e[8], e[9], e[10]));
|
|
88
|
+
const surfaceDistance = Math.max(0, Math.hypot(e[12] - point[0], e[13] - point[1], e[14] - point[2]) - radiusOf(object) * scale);
|
|
53
89
|
if (surfaceDistance <= radius && (best === null || surfaceDistance < best.distance)) {
|
|
54
|
-
best = {
|
|
55
|
-
interactableId: id,
|
|
56
|
-
distance: surfaceDistance,
|
|
57
|
-
point: [this.w.x, this.w.y, this.w.z],
|
|
58
|
-
};
|
|
90
|
+
best = { interactableId: id, distance: surfaceDistance, point: [e[12], e[13], e[14]] };
|
|
59
91
|
}
|
|
60
92
|
}
|
|
61
93
|
return best;
|
|
62
94
|
}
|
|
95
|
+
objects() {
|
|
96
|
+
if (this.rootList === null)
|
|
97
|
+
this.rootList = [...this.roots.values()];
|
|
98
|
+
return this.rootList;
|
|
99
|
+
}
|
|
63
100
|
ownerOf(object) {
|
|
64
101
|
let current = object;
|
|
65
102
|
while (current) {
|
|
@@ -70,18 +107,14 @@ export class ThreeHitTester {
|
|
|
70
107
|
}
|
|
71
108
|
return null;
|
|
72
109
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (geometry.boundingSphere) {
|
|
79
|
-
this.sphere.copy(geometry.boundingSphere);
|
|
80
|
-
const scale = object.getWorldScale(this.w);
|
|
81
|
-
return this.sphere.radius * Math.max(scale.x, scale.y, scale.z);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
110
|
+
}
|
|
111
|
+
/** Bounding-sphere radius of the object's own geometry in local units, or 0 for a plain group. */
|
|
112
|
+
function radiusOf(object) {
|
|
113
|
+
const geometry = object.geometry;
|
|
114
|
+
if (!geometry)
|
|
84
115
|
return 0;
|
|
85
|
-
|
|
116
|
+
if (!geometry.boundingSphere)
|
|
117
|
+
geometry.computeBoundingSphere();
|
|
118
|
+
return geometry.boundingSphere?.radius ?? 0;
|
|
86
119
|
}
|
|
87
120
|
//# sourceMappingURL=hit-tester.js.map
|
package/dist/hit-tester.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hit-tester.js","sourceRoot":"","sources":["../src/hit-tester.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ThreeHitTester - resolves core ray/proximity queries against registered\n * three.js objects. Ray hits use a Raycaster over the registered roots\n * (recursive); proximity uses world-position distance minus an\n * approximate bounding radius, good enough for poke targets.\n */\nimport { Raycaster, Sphere, Vector3, type Object3D, type Mesh } from \"three\";\nimport type { RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type { HitTester, InteractableHit } from \"@realitycollective/webxr-interactions\";\n\nexport class ThreeHitTester implements HitTester {\n private readonly roots = new Map<string, Object3D>();\n private readonly byObject = new WeakMap<Object3D, string>();\n private readonly raycaster = new Raycaster();\n private readonly v = new Vector3();\n private readonly w = new Vector3();\n private readonly sphere = new Sphere();\n\n register(id: string, object: Object3D): void {\n this.roots.set(id, object);\n this.byObject.set(object, id);\n }\n\n unregister(id: string): void {\n const object = this.roots.get(id);\n if (object) this.byObject.delete(object);\n this.roots.delete(id);\n }\n\n getObject(id: string): Object3D | undefined {\n return this.roots.get(id);\n }\n\n hitRay(ray: RayTuple): InteractableHit | null {\n if (this.roots.size === 0) return null;\n this.raycaster.ray.origin.set(...ray.origin);\n this.raycaster.ray.direction.set(...ray.direction);\n const objects = [...this.roots.values()];\n const intersections = this.raycaster.intersectObjects(objects, true);\n for (const intersection of intersections) {\n const id = this.ownerOf(intersection.object);\n if (id !== null) {\n return {\n interactableId: id,\n distance: intersection.distance,\n point: [intersection.point.x, intersection.point.y, intersection.point.z],\n };\n }\n }\n return null;\n }\n\n hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null {\n this.v.set(...point);\n let best: InteractableHit | null = null;\n for (const [id, object] of this.roots) {\n object.getWorldPosition(this.w);\n const surfaceDistance = Math.max(\n 0,\n this.w.distanceTo(this.v) - this.boundingRadius(object),\n );\n if (surfaceDistance <= radius && (best === null || surfaceDistance < best.distance)) {\n best = {\n interactableId: id,\n distance: surfaceDistance,\n point: [this.w.x, this.w.y, this.w.z],\n };\n }\n }\n return best;\n }\n\n private ownerOf(object: Object3D): string | null {\n let current: Object3D | null = object;\n while (current) {\n const id = this.byObject.get(current);\n if (id !== undefined) return id;\n current = current.parent;\n }\n return null;\n }\n\n private boundingRadius(object: Object3D): number {\n const geometry = (object as Mesh).geometry;\n if (geometry) {\n if (!geometry.boundingSphere) geometry.computeBoundingSphere();\n if (geometry.boundingSphere) {\n this.sphere.copy(geometry.boundingSphere);\n const scale = object.getWorldScale(this.w);\n return this.sphere.radius * Math.max(scale.x, scale.y, scale.z);\n }\n }\n return 0;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"hit-tester.js","sourceRoot":"","sources":["../src/hit-tester.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ThreeHitTester - resolves core ray/proximity queries against registered\n * three.js objects. Ray hits use a Raycaster over the registered roots\n * (recursive); proximity uses world-position distance minus an\n * approximate bounding radius, good enough for poke targets.\n *\n * Detailed meshes are the cost to watch. three.js tests every triangle of\n * a mesh once a ray enters its bounding sphere, so a 20k-triangle\n * interactable costs close to a millisecond per ray. When the app has\n * installed three-mesh-bvh's prototype hooks -\n * `BufferGeometry.prototype.computeBoundsTree` and\n * `Mesh.prototype.raycast = acceleratedRaycast`, which IWSDK does for you -\n * the tester builds a bounds tree for every geometry it registers and asks\n * the raycaster for the first hit only, so the same ray costs microseconds.\n * Without the hooks it falls back to the plain raycast, and a detailed\n * interactable should be registered through a low-poly collider proxy\n * instead (see the README).\n */\nimport { Raycaster, type BufferGeometry, type Intersection, type Mesh, type Object3D } from \"three\";\nimport type { RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type { HitTester, InteractableHit } from \"@realitycollective/webxr-interactions\";\n\n/** What three-mesh-bvh adds to a geometry once its prototype hooks are installed. */\ninterface BvhGeometryLike {\n boundsTree?: unknown;\n computeBoundsTree?: () => void;\n}\n\n/** A `Matrix4` always holds sixteen numbers; the type says so for indexed reads. */\ntype Matrix4Elements = [\n number, number, number, number,\n number, number, number, number,\n number, number, number, number,\n number, number, number, number,\n];\n\nexport class ThreeHitTester implements HitTester {\n private readonly roots = new Map<string, Object3D>();\n private readonly byObject = new WeakMap<Object3D, string>();\n /** `roots` as the array the raycaster takes, rebuilt only when the set changes. */\n private rootList: Object3D[] | null = null;\n private readonly raycaster = new Raycaster();\n /** Reused between calls; three.js appends into it and sorts it. */\n private readonly intersections: Intersection[] = [];\n\n constructor() {\n // Read by three-mesh-bvh's accelerated raycast, ignored by the stock one.\n (this.raycaster as { firstHitOnly?: boolean }).firstHitOnly = true;\n }\n\n register(id: string, object: Object3D): void {\n this.unregister(id);\n object.traverse((child) => {\n const geometry = (child as Mesh).geometry as (BufferGeometry & BvhGeometryLike) | undefined;\n // `disposeBoundsTree` leaves null behind, a never-built geometry has\n // undefined: both mean there is no tree to use.\n if (\n geometry &&\n typeof geometry.computeBoundsTree === \"function\" &&\n (geometry.boundsTree === undefined || geometry.boundsTree === null)\n ) {\n geometry.computeBoundsTree();\n }\n });\n this.roots.set(id, object);\n this.byObject.set(object, id);\n this.rootList = null;\n }\n\n unregister(id: string): void {\n const object = this.roots.get(id);\n if (!object) return;\n this.byObject.delete(object);\n this.roots.delete(id);\n this.rootList = null;\n }\n\n getObject(id: string): Object3D | undefined {\n return this.roots.get(id);\n }\n\n hitRay(ray: RayTuple): InteractableHit | null {\n if (this.roots.size === 0) return null;\n this.raycaster.ray.origin.set(ray.origin[0], ray.origin[1], ray.origin[2]);\n this.raycaster.ray.direction.set(ray.direction[0], ray.direction[1], ray.direction[2]);\n const intersections = this.intersections;\n intersections.length = 0;\n this.raycaster.intersectObjects(this.objects(), true, intersections);\n for (const intersection of intersections) {\n const id = this.ownerOf(intersection.object);\n if (id !== null) {\n return {\n interactableId: id,\n distance: intersection.distance,\n point: [intersection.point.x, intersection.point.y, intersection.point.z],\n };\n }\n }\n return null;\n }\n\n hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null {\n let best: InteractableHit | null = null;\n for (const [id, object] of this.roots) {\n // One matrix update per object, then position and scale straight off\n // the world matrix: the last column is the translation and the length\n // of each of the first three columns is that axis's scale.\n object.updateWorldMatrix(true, false);\n const e = object.matrixWorld.elements as Matrix4Elements;\n const scale = Math.max(\n Math.hypot(e[0], e[1], e[2]),\n Math.hypot(e[4], e[5], e[6]),\n Math.hypot(e[8], e[9], e[10]),\n );\n const surfaceDistance = Math.max(\n 0,\n Math.hypot(e[12] - point[0], e[13] - point[1], e[14] - point[2]) - radiusOf(object) * scale,\n );\n if (surfaceDistance <= radius && (best === null || surfaceDistance < best.distance)) {\n best = { interactableId: id, distance: surfaceDistance, point: [e[12], e[13], e[14]] };\n }\n }\n return best;\n }\n\n private objects(): Object3D[] {\n if (this.rootList === null) this.rootList = [...this.roots.values()];\n return this.rootList;\n }\n\n private ownerOf(object: Object3D): string | null {\n let current: Object3D | null = object;\n while (current) {\n const id = this.byObject.get(current);\n if (id !== undefined) return id;\n current = current.parent;\n }\n return null;\n }\n}\n\n/** Bounding-sphere radius of the object's own geometry in local units, or 0 for a plain group. */\nfunction radiusOf(object: Object3D): number {\n const geometry = (object as Mesh).geometry as BufferGeometry | undefined;\n if (!geometry) return 0;\n if (!geometry.boundingSphere) geometry.computeBoundingSphere();\n return geometry.boundingSphere?.radius ?? 0;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@realitycollective/threejs-interactions",
|
|
3
|
-
"version": "0.1.0-preview.
|
|
3
|
+
"version": "0.1.0-preview.4",
|
|
4
4
|
"description": "The default, standalone engine adapter for the Reality Collective Interaction Extensions: raw WebXR (the browser's OpenXR binding - XRSession, XRInputSource, select/squeeze, hand joints) plus three.js hit-testing and transform ports, with a desktop mouse fallback. No framework required. Re-exports the @realitycollective/webxr-interactions core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"realitycollective",
|
|
@@ -34,13 +34,20 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@realitycollective/webxr-input": "^0.1.1",
|
|
37
|
-
"@realitycollective/webxr-interactions": "^0.1.0-preview.
|
|
37
|
+
"@realitycollective/webxr-interactions": "^0.1.0-preview.4"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"three": ">=0.170.0"
|
|
40
|
+
"three": ">=0.170.0",
|
|
41
|
+
"three-mesh-bvh": ">=0.9.14"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"three-mesh-bvh": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
41
47
|
},
|
|
42
48
|
"devDependencies": {
|
|
43
49
|
"three": "npm:super-three@0.181.0",
|
|
50
|
+
"three-mesh-bvh": "^0.9.14",
|
|
44
51
|
"@types/three": "*"
|
|
45
52
|
},
|
|
46
53
|
"repository": {
|