@relu-interactives/spatial-ecs 0.4.3 → 0.4.5
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/{chunk-6UZC6M7V.js → chunk-QIIWFP3V.js} +244 -31
- package/dist/chunk-QIIWFP3V.js.map +1 -0
- package/dist/docs/guide/mixed-reality.md +160 -3
- package/dist/docs/guide/vr-and-hand-poses.md +29 -0
- package/dist/docs/guide/world-api.md +1 -1
- package/dist/{index-n7SeH119.d.ts → index-BphKHaV-.d.ts} +65 -1
- package/dist/{index-CBdnIyI8.d.cts → index-CH7qMsQT.d.cts} +65 -1
- package/dist/index.cjs +242 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/systems/index.cjs +242 -29
- package/dist/systems/index.cjs.map +1 -1
- package/dist/systems/index.d.cts +1 -1
- package/dist/systems/index.d.ts +1 -1
- package/dist/systems/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-6UZC6M7V.js.map +0 -1
|
@@ -7,9 +7,34 @@ With passthrough on, the experience composites over the user's real room instead
|
|
|
7
7
|
of a virtual background. Turn it on in the inspector (XR Rig → Mixed Reality), or
|
|
8
8
|
from a script by authoring the rig.
|
|
9
9
|
|
|
10
|
-
Mixed reality is a **Quest and Android XR** capability.
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
Mixed reality passthrough is a **Quest and Android XR** capability.
|
|
11
|
+
|
|
12
|
+
## Apple Vision Pro
|
|
13
|
+
|
|
14
|
+
Safari on visionOS — checked through visionOS 27 — supports WebXR
|
|
15
|
+
`immersive-vr` only. It grants `immersive-ar` to no web page, and exposes no
|
|
16
|
+
plane detection, mesh detection or depth sensing. This is a platform policy,
|
|
17
|
+
not a missing feature on our side; every web XR tool works within it (the
|
|
18
|
+
industry-standard answer is a VR fallback mode).
|
|
19
|
+
|
|
20
|
+
A mixed-reality project still works there, three ways:
|
|
21
|
+
|
|
22
|
+
1. **Automatic VR fallback.** The session probe falls back to `immersive-vr`,
|
|
23
|
+
the authored background applies, and hands/pinch interaction works. The
|
|
24
|
+
entry page says so honestly: "Passthrough isn't available in this browser —
|
|
25
|
+
this experience will run in VR."
|
|
26
|
+
2. **A synthetic floor, no scan needed.** With `environmentPhysics` on and no
|
|
27
|
+
scene understanding granted at all, the engine synthesizes one floor
|
|
28
|
+
collider whose top face sits at y = 0 — which `local-floor` makes the
|
|
29
|
+
user's REAL floor. Dropped objects land at floor height instead of falling
|
|
30
|
+
forever. (This only happens when the detection features are entirely
|
|
31
|
+
absent; a Quest that merely has not scanned yet keeps its room-capture
|
|
32
|
+
prompt instead.)
|
|
33
|
+
3. **"View in your room" via AR Quick Look.** On Apple devices the entry page
|
|
34
|
+
offers a native placement preview: the scene's content is exported to
|
|
35
|
+
`.usdz` on the fly and handed to AR Quick Look, which places it in the real
|
|
36
|
+
room with passthrough — Apple's sanctioned web-AR path (it also works on
|
|
37
|
+
iPhone and iPad). This is a static snapshot: no scripts, no physics.
|
|
13
38
|
|
|
14
39
|
## What the rig controls
|
|
15
40
|
|
|
@@ -41,6 +66,87 @@ Each detected surface becomes an ordinary entity, so it goes through the same
|
|
|
41
66
|
physics as everything else: a dynamic rigidbody dropped in a mixed-reality scene
|
|
42
67
|
lands on the real floor with no extra code.
|
|
43
68
|
|
|
69
|
+
### The room has to have been scanned first
|
|
70
|
+
|
|
71
|
+
This is the single most common reason a mixed-reality scene comes up empty, and
|
|
72
|
+
it is not a bug in the experience.
|
|
73
|
+
|
|
74
|
+
**Plane and mesh detection do not scan anything.** They replay a scan the user
|
|
75
|
+
captured earlier through the headset's own room setup — on Quest, *Settings >
|
|
76
|
+
Guardian > Mixed Reality* (Space Setup). If the user has never run it,
|
|
77
|
+
`detectedPlanes` and `detectedMeshes` simply stay empty, with no prompt and no
|
|
78
|
+
error. If they ran it in a **different room**, you get that room's furniture
|
|
79
|
+
overlaid on this one, which looks like the tracking is broken.
|
|
80
|
+
|
|
81
|
+
`XREnvironmentSystem` handles this: when `environmentPhysics` is on and nothing
|
|
82
|
+
at all has been detected three seconds into the session, it calls
|
|
83
|
+
`XRSession.initiateRoomCapture()`, which launches room setup from inside the
|
|
84
|
+
session. It is a Meta extension, so it is feature-detected and silently skipped
|
|
85
|
+
elsewhere, and — per Meta's contract — it is only ever called **once per
|
|
86
|
+
session**.
|
|
87
|
+
|
|
88
|
+
If a user reports "it never asked to scan my room" and the geometry does not
|
|
89
|
+
match, they are almost certainly running against a stale scan from another
|
|
90
|
+
space; re-running Space Setup for the current room fixes it.
|
|
91
|
+
|
|
92
|
+
### Reading the `environmentDebug` overlay
|
|
93
|
+
|
|
94
|
+
The overlay colours surfaces by what the runtime says they are, so a glance
|
|
95
|
+
(or a screenshot) tells you which surface is wrong rather than just "wireframe
|
|
96
|
+
somewhere":
|
|
97
|
+
|
|
98
|
+
- **green** — the floor plane
|
|
99
|
+
- **blue** — walls
|
|
100
|
+
- **purple** — the ceiling
|
|
101
|
+
- **orange** — labelled things: tables, couches, shelves, doors, windows
|
|
102
|
+
- **faint gray** — the raw room scan (the "global mesh")
|
|
103
|
+
|
|
104
|
+
Two things about the global mesh are easy to misread. It is deliberately
|
|
105
|
+
COARSE — a couch becomes a melted lump, a wall gets bumps — because it is a
|
|
106
|
+
collision mesh, not a picture; those lumps are not phantom objects. And under
|
|
107
|
+
`environmentPhysics: "auto"` it is what the physics uses, with the flat planes
|
|
108
|
+
skipped entirely, so orange furniture boxes and gray scan lumps can legitimately
|
|
109
|
+
overlap the same real object.
|
|
110
|
+
|
|
111
|
+
The overlay is depth-tested like ordinary geometry — the near wall hides the
|
|
112
|
+
far one — but it never *writes* depth, so it cannot occlude actual content. (It
|
|
113
|
+
was briefly rendered X-ray; on device, the dense global scan at low opacity
|
|
114
|
+
fuses into translucent sheets, and without a depth test the walls' sheets drew
|
|
115
|
+
through everything and stacked in front of the user's face.)
|
|
116
|
+
|
|
117
|
+
For identifying a mystery shape, `XREnvironmentSystem.getDebugSnapshot(world)`
|
|
118
|
+
returns a census of every spawned surface — kind, semantic label, position and
|
|
119
|
+
approximate radius.
|
|
120
|
+
|
|
121
|
+
One artifact is handled for you: three.js's depth-occlusion pass draws a
|
|
122
|
+
head-locked mesh that writes only depth, but leaves its colour output
|
|
123
|
+
undefined — on Quest hardware that painted the depth camera's view over the
|
|
124
|
+
screen as faint contour bands that follow the head. `XRSessionSystem` forces
|
|
125
|
+
`colorWrite` off on that mesh every frame, so enabling `depthOcclusion` costs
|
|
126
|
+
no visual artifacts.
|
|
127
|
+
|
|
128
|
+
### Lost tracking freezes a surface — it never follows the head
|
|
129
|
+
|
|
130
|
+
When the headset loses tracking of a surface, WebXR does not stop reporting a
|
|
131
|
+
pose — it reports an *emulated* one: the last known pose held relative to the
|
|
132
|
+
viewer, i.e. glued to the head. Applying that to room geometry would make the
|
|
133
|
+
surface — and its collider — follow the user around, shoving physics objects
|
|
134
|
+
out of the way. The engine instead freezes the surface at its last tracked
|
|
135
|
+
pose, resumes when tracking returns, and never creates a surface from an
|
|
136
|
+
emulated pose in the first place. The diagnostics census marks such surfaces
|
|
137
|
+
with `emulated: true`.
|
|
138
|
+
|
|
139
|
+
### No artificial locomotion in passthrough
|
|
140
|
+
|
|
141
|
+
While a session composites over the real world, `XRLocomotionSystem` disables
|
|
142
|
+
ALL artificial locomotion — snap turn, teleport and smooth movement — whatever
|
|
143
|
+
the rig says. Virtual content in mixed reality is registered to the real room;
|
|
144
|
+
rotating or translating the rig swings every virtual object out of alignment
|
|
145
|
+
while the passthrough room stays put, which feels broken rather than like
|
|
146
|
+
movement. The user moves by walking. Note that `snapTurn` is a separate field
|
|
147
|
+
from `locomotion`, so `locomotion: "none"` alone would NOT have stopped the
|
|
148
|
+
stick from turning the rig — the gate is on the live blend mode.
|
|
149
|
+
|
|
44
150
|
## Finding the real room from a script
|
|
45
151
|
|
|
46
152
|
Detected surfaces carry `XREnvironmentComponent`, so they are queryable like any
|
|
@@ -93,6 +199,36 @@ export default class RoomAware extends Behaviour {
|
|
|
93
199
|
}
|
|
94
200
|
```
|
|
95
201
|
|
|
202
|
+
## Hit-testing the real room from a script
|
|
203
|
+
|
|
204
|
+
With `hitTest` on the rig, `XRHitTestSystem` keeps a ray from the viewer (and
|
|
205
|
+
one per hand) cast against the real room. Systems are resolved by INSTANCE, so
|
|
206
|
+
pass a throwaway one to `getSystem`:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { Behaviour, XRHitTestSystem } from "@relu-interactives/spatial-ecs";
|
|
210
|
+
import { TransformComponent } from "@relu-interactives/spatial-ecs/components";
|
|
211
|
+
|
|
212
|
+
export default class PlaceOnRealSurface extends Behaviour {
|
|
213
|
+
onSelectStart() {
|
|
214
|
+
const hitTest = this.world.getSystem(new XRHitTestSystem());
|
|
215
|
+
// "viewer" = where the user is looking; "left"/"right" = hand rays.
|
|
216
|
+
const hit = hitTest.getHitResult("viewer");
|
|
217
|
+
if (!hit) return; // nothing granted, or no surface under the ray yet
|
|
218
|
+
|
|
219
|
+
const transform = this.world.getComponent(this.entityId, TransformComponent);
|
|
220
|
+
if (transform) {
|
|
221
|
+
transform.position.x = hit.position.x;
|
|
222
|
+
transform.position.y = hit.position.y;
|
|
223
|
+
transform.position.z = hit.position.z;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`getHitResult` returns null until the runtime grants hit-testing and the ray
|
|
230
|
+
actually meets a surface — always handle that case.
|
|
231
|
+
|
|
96
232
|
## Anchors
|
|
97
233
|
|
|
98
234
|
A headset keeps revising where it thinks the room is. Content at fixed scene
|
|
@@ -106,6 +242,27 @@ on, and it stays put relative to the room.
|
|
|
106
242
|
Because the anchor owns the transform, moving the entity by script or physics has
|
|
107
243
|
no lasting effect — disable the component first, move it, then re-enable.
|
|
108
244
|
|
|
245
|
+
From a script the toggle is the whole API:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { Behaviour } from "@relu-interactives/spatial-ecs";
|
|
249
|
+
import { XRAnchorComponent } from "@relu-interactives/spatial-ecs/components";
|
|
250
|
+
|
|
251
|
+
export default class ReleaseOnGrab extends Behaviour {
|
|
252
|
+
onGrab() {
|
|
253
|
+
// Let the hand move it: release the weld for the duration of the grab.
|
|
254
|
+
const anchor = this.world.getComponent(this.entityId, XRAnchorComponent);
|
|
255
|
+
if (anchor) anchor.enabled = false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
onRelease() {
|
|
259
|
+
// Re-anchor wherever the user left it.
|
|
260
|
+
const anchor = this.world.getComponent(this.entityId, XRAnchorComponent);
|
|
261
|
+
if (anchor) anchor.enabled = true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
109
266
|
Anchors last for the session only. Persisting one across launches needs
|
|
110
267
|
`requestPersistentHandle`, which is a draft extension and not supported here.
|
|
111
268
|
|
|
@@ -185,6 +185,35 @@ onXRSessionStart(capabilities: XrCapabilities) {
|
|
|
185
185
|
|
|
186
186
|
Two rules worth internalising: **move the rig, never the camera** (the headset owns the camera while presenting and overwrites it every frame), and give any destructive gesture a longer **Hold Time** so it cannot fire in passing.
|
|
187
187
|
|
|
188
|
+
### Reading grab state from a script
|
|
189
|
+
|
|
190
|
+
`XRInteractionSystem.getGrabInfo(entityId)` reports who is holding an entity —
|
|
191
|
+
hand count, primary/secondary handedness, live hand separation (two-hand grabs)
|
|
192
|
+
and the attach-point ids in use. Systems resolve by instance:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import {
|
|
196
|
+
Behaviour,
|
|
197
|
+
XRInputSystem,
|
|
198
|
+
XRInteractionSystem,
|
|
199
|
+
} from "@relu-interactives/spatial-ecs";
|
|
200
|
+
|
|
201
|
+
export default class TwoHandChargeUp extends Behaviour {
|
|
202
|
+
onUpdate() {
|
|
203
|
+
const interaction = this.world.getSystem(
|
|
204
|
+
new XRInteractionSystem(new XRInputSystem()),
|
|
205
|
+
);
|
|
206
|
+
const grab = interaction.getGrabInfo(this.entityId);
|
|
207
|
+
if (!grab || grab.hands < 2 || grab.handSeparation === null) return;
|
|
208
|
+
|
|
209
|
+
// Example: pull both hands apart to "charge" — separation is metres.
|
|
210
|
+
console.log("charge level", Math.min(1, grab.handSeparation / 0.8));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Returns null while nothing holds the entity, so gate on it every frame.
|
|
216
|
+
|
|
188
217
|
## Hand poses
|
|
189
218
|
|
|
190
219
|
Add an **XR Hand Pose** component and list the poses to watch for. Each is either a built-in preset or a custom set of finger curls.
|
|
@@ -102,7 +102,7 @@ Both methods accept `Vec3` (`THREE.Vector3` or `{ x, y, z }`) and return `Inters
|
|
|
102
102
|
|
|
103
103
|
| Method | Purpose |
|
|
104
104
|
| --- | --- |
|
|
105
|
-
| `getSystem(
|
|
105
|
+
| `getSystem(instance)` | Resolve the REGISTERED system matching the argument's class — pass a throwaway instance (`world.getSystem(new XRHitTestSystem())`), never the class itself. |
|
|
106
106
|
| `hasSystem(SystemClass)` | `true` if the system is registered. |
|
|
107
107
|
| `getObjectManagementSystem()` | Shorthand for the built-in `ObjectManagementSystem`. Use this to spawn or destroy entities at runtime (`requestCreate`, `createNow`, `deleteEntity`). |
|
|
108
108
|
| `getEnvironmentComponent()` | Returns the singleton [`Environment`](./scene/environment) component, or `undefined` if not yet initialized. Mutate `values` to change skybox, fog, or gravity at runtime. |
|
|
@@ -647,8 +647,29 @@ declare class XRSessionSystem implements EcsSystem {
|
|
|
647
647
|
private readonly inputSystem;
|
|
648
648
|
/** Y compensation applied when a floor-relative space was requested but not granted. */
|
|
649
649
|
private appliedOriginOffset;
|
|
650
|
+
/** The live session, so an async floor probe can tell if it has been replaced. */
|
|
651
|
+
private session;
|
|
650
652
|
constructor(inputSystem?: XRInputSystem);
|
|
651
653
|
update(world: World): void;
|
|
654
|
+
/**
|
|
655
|
+
* Stop three's depth-occlusion pass from writing COLOR.
|
|
656
|
+
*
|
|
657
|
+
* With depth sensing granted, three draws a full-screen, head-locked mesh
|
|
658
|
+
* every frame whose fragment shader assigns ONLY `gl_FragDepth` — that is
|
|
659
|
+
* the whole occlusion mechanism — but its material never sets
|
|
660
|
+
* `colorWrite: false`, so the colour output is UNDEFINED per GLSL. Desktop
|
|
661
|
+
* drivers happen to zero it, which is why this ships unnoticed; Quest's
|
|
662
|
+
* mobile GPU frequently leaves the last-computed temporary in the register —
|
|
663
|
+
* the sampled DEPTH value — so the pass paints the depth camera's view over
|
|
664
|
+
* the screen as faint head-locked contour bands. On device that reads as "a
|
|
665
|
+
* large wireframe dome that follows my head".
|
|
666
|
+
*
|
|
667
|
+
* The mesh is created lazily inside three's render, so this is re-checked
|
|
668
|
+
* every frame rather than hooked once; the write is idempotent and the
|
|
669
|
+
* check is two property reads. Depth-only is the mesh's entire purpose —
|
|
670
|
+
* occlusion is unaffected.
|
|
671
|
+
*/
|
|
672
|
+
private muteDepthOcclusionColor;
|
|
652
673
|
private handleSessionStart;
|
|
653
674
|
/**
|
|
654
675
|
* Apply the rig's authored tracking origin, and compensate when the device
|
|
@@ -690,12 +711,55 @@ declare class XREnvironmentSystem implements EcsSystem {
|
|
|
690
711
|
private readonly meshes;
|
|
691
712
|
private presenting;
|
|
692
713
|
private debugVisible;
|
|
693
|
-
|
|
714
|
+
/** Seconds presenting, used to decide the room has genuinely nothing in it. */
|
|
715
|
+
private elapsed;
|
|
716
|
+
/** `initiateRoomCapture` may be called only ONCE per session. */
|
|
717
|
+
private roomCaptureRequested;
|
|
718
|
+
update(world: World, deltaTime?: number): void;
|
|
719
|
+
/**
|
|
720
|
+
* Ask the runtime to scan the room when it turns out we have no room.
|
|
721
|
+
*
|
|
722
|
+
* Plane and mesh detection do NOT scan anything — they replay a scan the user
|
|
723
|
+
* captured earlier through the system's own room setup. Most people have
|
|
724
|
+
* never run it, and those who have may have run it in a DIFFERENT room, so a
|
|
725
|
+
* mixed-reality experience opens against either nothing at all or someone
|
|
726
|
+
* else's furniture, with no prompt and no error.
|
|
727
|
+
*
|
|
728
|
+
* `XRSession.initiateRoomCapture()` (Meta's extension) launches that setup
|
|
729
|
+
* from inside the session, and exists precisely for this case. Contract, from
|
|
730
|
+
* Meta's docs: callable ONCE per session, and only worth calling once we are
|
|
731
|
+
* confident the room really is empty — the arrays populate a beat after the
|
|
732
|
+
* session starts, so a check on frame one would fire for everyone.
|
|
733
|
+
*/
|
|
734
|
+
private maybeCaptureRoom;
|
|
694
735
|
/** How many pieces of real geometry are currently colliding. Script-facing. */
|
|
695
736
|
getDetectedCount(): {
|
|
696
737
|
planes: number;
|
|
697
738
|
meshes: number;
|
|
698
739
|
};
|
|
740
|
+
/**
|
|
741
|
+
* A census of every surface currently spawned: what it is, where it sits and
|
|
742
|
+
* roughly how big it is. Built for the template's post-session readout, so a
|
|
743
|
+
* mystery shape on the headset can be looked up by its label instead of
|
|
744
|
+
* guessed at — a small-radius entry at head height IS the mystery shape, and
|
|
745
|
+
* a clean list means whatever was seen did not come from this system.
|
|
746
|
+
*/
|
|
747
|
+
getDebugSnapshot(world: World): {
|
|
748
|
+
kind: "plane" | "mesh";
|
|
749
|
+
label: string | null;
|
|
750
|
+
position: {
|
|
751
|
+
x: number;
|
|
752
|
+
y: number;
|
|
753
|
+
z: number;
|
|
754
|
+
};
|
|
755
|
+
radius: number;
|
|
756
|
+
/** Tracking currently lost — pose frozen at the last known place. */
|
|
757
|
+
emulated: boolean;
|
|
758
|
+
}[];
|
|
759
|
+
/** Create the no-scan floor once; it lives in the ordinary plane map. */
|
|
760
|
+
private ensureSyntheticFloor;
|
|
761
|
+
/** Real scene data exists (or arrived) — the stand-in must go. */
|
|
762
|
+
private removeSyntheticFloor;
|
|
699
763
|
private findRig;
|
|
700
764
|
private syncPlanes;
|
|
701
765
|
private syncMeshes;
|
|
@@ -647,8 +647,29 @@ declare class XRSessionSystem implements EcsSystem {
|
|
|
647
647
|
private readonly inputSystem;
|
|
648
648
|
/** Y compensation applied when a floor-relative space was requested but not granted. */
|
|
649
649
|
private appliedOriginOffset;
|
|
650
|
+
/** The live session, so an async floor probe can tell if it has been replaced. */
|
|
651
|
+
private session;
|
|
650
652
|
constructor(inputSystem?: XRInputSystem);
|
|
651
653
|
update(world: World): void;
|
|
654
|
+
/**
|
|
655
|
+
* Stop three's depth-occlusion pass from writing COLOR.
|
|
656
|
+
*
|
|
657
|
+
* With depth sensing granted, three draws a full-screen, head-locked mesh
|
|
658
|
+
* every frame whose fragment shader assigns ONLY `gl_FragDepth` — that is
|
|
659
|
+
* the whole occlusion mechanism — but its material never sets
|
|
660
|
+
* `colorWrite: false`, so the colour output is UNDEFINED per GLSL. Desktop
|
|
661
|
+
* drivers happen to zero it, which is why this ships unnoticed; Quest's
|
|
662
|
+
* mobile GPU frequently leaves the last-computed temporary in the register —
|
|
663
|
+
* the sampled DEPTH value — so the pass paints the depth camera's view over
|
|
664
|
+
* the screen as faint head-locked contour bands. On device that reads as "a
|
|
665
|
+
* large wireframe dome that follows my head".
|
|
666
|
+
*
|
|
667
|
+
* The mesh is created lazily inside three's render, so this is re-checked
|
|
668
|
+
* every frame rather than hooked once; the write is idempotent and the
|
|
669
|
+
* check is two property reads. Depth-only is the mesh's entire purpose —
|
|
670
|
+
* occlusion is unaffected.
|
|
671
|
+
*/
|
|
672
|
+
private muteDepthOcclusionColor;
|
|
652
673
|
private handleSessionStart;
|
|
653
674
|
/**
|
|
654
675
|
* Apply the rig's authored tracking origin, and compensate when the device
|
|
@@ -690,12 +711,55 @@ declare class XREnvironmentSystem implements EcsSystem {
|
|
|
690
711
|
private readonly meshes;
|
|
691
712
|
private presenting;
|
|
692
713
|
private debugVisible;
|
|
693
|
-
|
|
714
|
+
/** Seconds presenting, used to decide the room has genuinely nothing in it. */
|
|
715
|
+
private elapsed;
|
|
716
|
+
/** `initiateRoomCapture` may be called only ONCE per session. */
|
|
717
|
+
private roomCaptureRequested;
|
|
718
|
+
update(world: World, deltaTime?: number): void;
|
|
719
|
+
/**
|
|
720
|
+
* Ask the runtime to scan the room when it turns out we have no room.
|
|
721
|
+
*
|
|
722
|
+
* Plane and mesh detection do NOT scan anything — they replay a scan the user
|
|
723
|
+
* captured earlier through the system's own room setup. Most people have
|
|
724
|
+
* never run it, and those who have may have run it in a DIFFERENT room, so a
|
|
725
|
+
* mixed-reality experience opens against either nothing at all or someone
|
|
726
|
+
* else's furniture, with no prompt and no error.
|
|
727
|
+
*
|
|
728
|
+
* `XRSession.initiateRoomCapture()` (Meta's extension) launches that setup
|
|
729
|
+
* from inside the session, and exists precisely for this case. Contract, from
|
|
730
|
+
* Meta's docs: callable ONCE per session, and only worth calling once we are
|
|
731
|
+
* confident the room really is empty — the arrays populate a beat after the
|
|
732
|
+
* session starts, so a check on frame one would fire for everyone.
|
|
733
|
+
*/
|
|
734
|
+
private maybeCaptureRoom;
|
|
694
735
|
/** How many pieces of real geometry are currently colliding. Script-facing. */
|
|
695
736
|
getDetectedCount(): {
|
|
696
737
|
planes: number;
|
|
697
738
|
meshes: number;
|
|
698
739
|
};
|
|
740
|
+
/**
|
|
741
|
+
* A census of every surface currently spawned: what it is, where it sits and
|
|
742
|
+
* roughly how big it is. Built for the template's post-session readout, so a
|
|
743
|
+
* mystery shape on the headset can be looked up by its label instead of
|
|
744
|
+
* guessed at — a small-radius entry at head height IS the mystery shape, and
|
|
745
|
+
* a clean list means whatever was seen did not come from this system.
|
|
746
|
+
*/
|
|
747
|
+
getDebugSnapshot(world: World): {
|
|
748
|
+
kind: "plane" | "mesh";
|
|
749
|
+
label: string | null;
|
|
750
|
+
position: {
|
|
751
|
+
x: number;
|
|
752
|
+
y: number;
|
|
753
|
+
z: number;
|
|
754
|
+
};
|
|
755
|
+
radius: number;
|
|
756
|
+
/** Tracking currently lost — pose frozen at the last known place. */
|
|
757
|
+
emulated: boolean;
|
|
758
|
+
}[];
|
|
759
|
+
/** Create the no-scan floor once; it lives in the ordinary plane map. */
|
|
760
|
+
private ensureSyntheticFloor;
|
|
761
|
+
/** Real scene data exists (or arrived) — the stand-in must go. */
|
|
762
|
+
private removeSyntheticFloor;
|
|
699
763
|
private findRig;
|
|
700
764
|
private syncPlanes;
|
|
701
765
|
private syncMeshes;
|