@relu-interactives/spatial-ecs 0.4.4 → 0.5.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/dist/{System-ln4Q7Eot.d.cts → System-V_mv07le.d.cts} +2 -2
- package/dist/{System-ln4Q7Eot.d.ts → System-V_mv07le.d.ts} +2 -2
- package/dist/{XRHandPose-CxPv-LBo.d.cts → XRHandPose-CQTLGxFC.d.cts} +218 -2
- package/dist/{XRHandPose-BImIa4ZS.d.ts → XRHandPose-CU3vNFuS.d.ts} +218 -2
- package/dist/{chunk-4AK4I3QF.js → chunk-BUEKOSR5.js} +102 -3
- package/dist/chunk-BUEKOSR5.js.map +1 -0
- package/dist/{chunk-SGADE55G.js → chunk-RXBLOO5B.js} +249 -90
- package/dist/chunk-RXBLOO5B.js.map +1 -0
- package/dist/{chunk-4DWZK4X4.js → chunk-WJD2VKEH.js} +3 -3
- package/dist/chunk-WJD2VKEH.js.map +1 -0
- package/dist/{chunk-OY2OONRR.js → chunk-Z6ZMR3CS.js} +1374 -30
- package/dist/chunk-Z6ZMR3CS.js.map +1 -0
- package/dist/components/index.cjs +99 -0
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.cts +3 -3
- package/dist/components/index.d.ts +3 -3
- package/dist/components/index.js +1 -1
- package/dist/docs/guide/mixed-reality.md +137 -3
- package/dist/docs/guide/spatial-ui.md +165 -0
- package/dist/docs/guide/vr-and-hand-poses.md +29 -0
- package/dist/docs/guide/world-api.md +1 -1
- package/dist/factories/index.cjs +372 -114
- package/dist/factories/index.cjs.map +1 -1
- package/dist/factories/index.d.cts +13 -3
- package/dist/factories/index.d.ts +13 -3
- package/dist/factories/index.js +3 -3
- package/dist/{index-CuGqxuk-.d.ts → index-C3ik3sA1.d.ts} +16 -3
- package/dist/{index-B2oHfqkj.d.ts → index-CppKDxbu.d.ts} +187 -3
- package/dist/{index-CQsHUez3.d.cts → index-Dlr-7-4m.d.cts} +16 -3
- package/dist/{index-7YCk4cmQ.d.cts → index-I2IsocYW.d.cts} +187 -3
- package/dist/index.cjs +1977 -366
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -9
- package/dist/index.d.ts +42 -9
- package/dist/index.js +10 -8
- package/dist/index.js.map +1 -1
- package/dist/systems/index.cjs +1465 -25
- package/dist/systems/index.cjs.map +1 -1
- package/dist/systems/index.d.cts +2 -2
- package/dist/systems/index.d.ts +2 -2
- package/dist/systems/index.js +3 -3
- package/dist/types.d.cts +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/worldSnapshotUtils.d.cts +1 -1
- package/dist/worldSnapshotUtils.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-4AK4I3QF.js.map +0 -1
- package/dist/chunk-4DWZK4X4.js.map +0 -1
- package/dist/chunk-OY2OONRR.js.map +0 -1
- package/dist/chunk-SGADE55G.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { A as AnimationComponent, a as AnimationComponentOptions, b as AudioComponent, C as CameraComponent, c as CameraComponentOptions, d as CameraProjection, e as CameraRuntimeType, f as ColliderShape, g as ColliderState, h as CubeMeshParameters, D as DEFAULT_COLLIDER_STATE, i as DEFAULT_INSTANCED_MESH_PARAMETERS, j as DEFAULT_INSTANCE_TRANSFORM, k as DEFAULT_MESH_GEOMETRY_PARAMETERS, l as DEFAULT_PHYSICS_STATE, m as DEFAULT_POSE_CURLS, n as DEFAULT_POSE_DIRECTIONS, o as DEFAULT_RIGIDBODY_STATE, E as EditableMeshObjectKind, p as EntityType, I as ImageComponent, q as ImageTarget, r as ImageTargetAnchor, s as ImageTargetAnchorOptions, t as ImageTargetReference, u as InstanceSlotComponent, v as InstanceTransform, w as InstancedMeshComponent, x as InstancedMeshKind, L as LightComponent, y as LightValueState, M as MaterialComponent, z as MaterialTypeValue, B as MaterialValueState, F as MeshComponent, G as MeshGeometryComponent, H as MeshGeometryParameters, J as MeshGeometryParametersByKind, K as ModelComponent, N as Name, O as Object3DRef, P as PhysicsComponent, Q as PhysicsState, R as PlaneMeshParameters, S as RadiusHeightMeshParameters, T as RadiusMeshParameters, U as RigidbodyState, V as RigidbodyType, W as ScriptComponent, X as ScriptEntry, Y as ScriptObjectComponent, Z as ScriptObjectComponentInput, _ as ScriptObjectRuntimeState, $ as Selectable, a0 as SelectionTransformState, a1 as SpriteComponent, a2 as TextureFilteringValue, a3 as TransformModeValue, a4 as TransformSpaceValue, a5 as
|
|
2
|
-
export { D as DEFAULT_ENVIRONMENT_STATE, a as DEFAULT_POSTPROCESSING_STATE, E as Environment, b as EnvironmentFogState, c as EnvironmentFogType, d as EnvironmentGradientEffect, e as EnvironmentSkyboxState, f as EnvironmentSkyboxType, g as EnvironmentState, P as ParentId, h as Postprocessing, i as PostprocessingState, T as TransformComponent, j as TransformData, V as Vec3 } from '../System-
|
|
3
|
-
export { P as PrefabInstance, a as PrefabNode, X as XREnvironmentComponent, b as XrEnvironmentKind, c as XrEnvironmentOrientation } from '../index-
|
|
1
|
+
export { A as AnimationComponent, a as AnimationComponentOptions, b as AudioComponent, C as CameraComponent, c as CameraComponentOptions, d as CameraProjection, e as CameraRuntimeType, f as ColliderShape, g as ColliderState, h as CubeMeshParameters, D as DEFAULT_COLLIDER_STATE, i as DEFAULT_INSTANCED_MESH_PARAMETERS, j as DEFAULT_INSTANCE_TRANSFORM, k as DEFAULT_MESH_GEOMETRY_PARAMETERS, l as DEFAULT_PHYSICS_STATE, m as DEFAULT_POSE_CURLS, n as DEFAULT_POSE_DIRECTIONS, o as DEFAULT_RIGIDBODY_STATE, E as EditableMeshObjectKind, p as EntityType, I as ImageComponent, q as ImageTarget, r as ImageTargetAnchor, s as ImageTargetAnchorOptions, t as ImageTargetReference, u as InstanceSlotComponent, v as InstanceTransform, w as InstancedMeshComponent, x as InstancedMeshKind, L as LightComponent, y as LightValueState, M as MaterialComponent, z as MaterialTypeValue, B as MaterialValueState, F as MeshComponent, G as MeshGeometryComponent, H as MeshGeometryParameters, J as MeshGeometryParametersByKind, K as ModelComponent, N as Name, O as Object3DRef, P as PhysicsComponent, Q as PhysicsState, R as PlaneMeshParameters, S as RadiusHeightMeshParameters, T as RadiusMeshParameters, U as RigidbodyState, V as RigidbodyType, W as ScriptComponent, X as ScriptEntry, Y as ScriptObjectComponent, Z as ScriptObjectComponentInput, _ as ScriptObjectRuntimeState, $ as Selectable, a0 as SelectionTransformState, a1 as SpriteComponent, a2 as TextureFilteringValue, a3 as TransformModeValue, a4 as TransformSpaceValue, a5 as UIButtonComponent, a6 as UIButtonOptions, a7 as UIPanelComponent, a8 as UIPanelOptions, a9 as UISliderComponent, aa as UISliderOptions, ab as UITextComponent, ac as UITextOptions, ad as UIToggleComponent, ae as UIToggleOptions, af as UiFollowMode, ag as UiTextAlign, ah as UiTextVAlign, ai as VRTeleportAreaComponent, aj as VRTeleportAreaOptions, ak as VideoComponent, al as VrTeleportShape, am as XRAnchorComponent, an as XRAnchorOptions, ao as XRHandPoseComponent, ap as XRHandPoseOptions, aq as XRInteractableComponent, ar as XRInteractableOptions, as as XRInteractorComponent, at as XRInteractorOptions, au as XRInteractorSideComponent, av as XRRigComponent, aw as XRRigOptions, ax as XrAttachPoint, ay as XrDistanceGrabBehavior, az as XrEnvironmentPhysics, aA as XrGrabGesture, aB as XrGrabHands, aC as XrGrabReach, aD as XrHandPoseEntry, aE as XrHandPreset, aF as XrInteractorSide, aG as XrInteractorType, aH as XrLocomotionMode, aI as XrTrackingOrigin, aJ as XrTwoHandRotation, aK as createAttachPoint, aL as createHandPoseEntry, aM as defaultMaterialValue, aN as handPoseDefinitionFor, aO as normalizePhysicsState, aP as setPhysicsLiveRef } from '../XRHandPose-CQTLGxFC.cjs';
|
|
2
|
+
export { D as DEFAULT_ENVIRONMENT_STATE, a as DEFAULT_POSTPROCESSING_STATE, E as Environment, b as EnvironmentFogState, c as EnvironmentFogType, d as EnvironmentGradientEffect, e as EnvironmentSkyboxState, f as EnvironmentSkyboxType, g as EnvironmentState, P as ParentId, h as Postprocessing, i as PostprocessingState, T as TransformComponent, j as TransformData, V as Vec3 } from '../System-V_mv07le.cjs';
|
|
3
|
+
export { P as PrefabInstance, a as PrefabNode, X as XREnvironmentComponent, b as XrEnvironmentKind, c as XrEnvironmentOrientation } from '../index-Dlr-7-4m.cjs';
|
|
4
4
|
import 'three';
|
|
5
5
|
import '@dimforge/rapier3d-compat';
|
|
6
6
|
import '../handPose-DlcTtqgx.cjs';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { A as AnimationComponent, a as AnimationComponentOptions, b as AudioComponent, C as CameraComponent, c as CameraComponentOptions, d as CameraProjection, e as CameraRuntimeType, f as ColliderShape, g as ColliderState, h as CubeMeshParameters, D as DEFAULT_COLLIDER_STATE, i as DEFAULT_INSTANCED_MESH_PARAMETERS, j as DEFAULT_INSTANCE_TRANSFORM, k as DEFAULT_MESH_GEOMETRY_PARAMETERS, l as DEFAULT_PHYSICS_STATE, m as DEFAULT_POSE_CURLS, n as DEFAULT_POSE_DIRECTIONS, o as DEFAULT_RIGIDBODY_STATE, E as EditableMeshObjectKind, p as EntityType, I as ImageComponent, q as ImageTarget, r as ImageTargetAnchor, s as ImageTargetAnchorOptions, t as ImageTargetReference, u as InstanceSlotComponent, v as InstanceTransform, w as InstancedMeshComponent, x as InstancedMeshKind, L as LightComponent, y as LightValueState, M as MaterialComponent, z as MaterialTypeValue, B as MaterialValueState, F as MeshComponent, G as MeshGeometryComponent, H as MeshGeometryParameters, J as MeshGeometryParametersByKind, K as ModelComponent, N as Name, O as Object3DRef, P as PhysicsComponent, Q as PhysicsState, R as PlaneMeshParameters, S as RadiusHeightMeshParameters, T as RadiusMeshParameters, U as RigidbodyState, V as RigidbodyType, W as ScriptComponent, X as ScriptEntry, Y as ScriptObjectComponent, Z as ScriptObjectComponentInput, _ as ScriptObjectRuntimeState, $ as Selectable, a0 as SelectionTransformState, a1 as SpriteComponent, a2 as TextureFilteringValue, a3 as TransformModeValue, a4 as TransformSpaceValue, a5 as
|
|
2
|
-
export { D as DEFAULT_ENVIRONMENT_STATE, a as DEFAULT_POSTPROCESSING_STATE, E as Environment, b as EnvironmentFogState, c as EnvironmentFogType, d as EnvironmentGradientEffect, e as EnvironmentSkyboxState, f as EnvironmentSkyboxType, g as EnvironmentState, P as ParentId, h as Postprocessing, i as PostprocessingState, T as TransformComponent, j as TransformData, V as Vec3 } from '../System-
|
|
3
|
-
export { P as PrefabInstance, a as PrefabNode, X as XREnvironmentComponent, b as XrEnvironmentKind, c as XrEnvironmentOrientation } from '../index-
|
|
1
|
+
export { A as AnimationComponent, a as AnimationComponentOptions, b as AudioComponent, C as CameraComponent, c as CameraComponentOptions, d as CameraProjection, e as CameraRuntimeType, f as ColliderShape, g as ColliderState, h as CubeMeshParameters, D as DEFAULT_COLLIDER_STATE, i as DEFAULT_INSTANCED_MESH_PARAMETERS, j as DEFAULT_INSTANCE_TRANSFORM, k as DEFAULT_MESH_GEOMETRY_PARAMETERS, l as DEFAULT_PHYSICS_STATE, m as DEFAULT_POSE_CURLS, n as DEFAULT_POSE_DIRECTIONS, o as DEFAULT_RIGIDBODY_STATE, E as EditableMeshObjectKind, p as EntityType, I as ImageComponent, q as ImageTarget, r as ImageTargetAnchor, s as ImageTargetAnchorOptions, t as ImageTargetReference, u as InstanceSlotComponent, v as InstanceTransform, w as InstancedMeshComponent, x as InstancedMeshKind, L as LightComponent, y as LightValueState, M as MaterialComponent, z as MaterialTypeValue, B as MaterialValueState, F as MeshComponent, G as MeshGeometryComponent, H as MeshGeometryParameters, J as MeshGeometryParametersByKind, K as ModelComponent, N as Name, O as Object3DRef, P as PhysicsComponent, Q as PhysicsState, R as PlaneMeshParameters, S as RadiusHeightMeshParameters, T as RadiusMeshParameters, U as RigidbodyState, V as RigidbodyType, W as ScriptComponent, X as ScriptEntry, Y as ScriptObjectComponent, Z as ScriptObjectComponentInput, _ as ScriptObjectRuntimeState, $ as Selectable, a0 as SelectionTransformState, a1 as SpriteComponent, a2 as TextureFilteringValue, a3 as TransformModeValue, a4 as TransformSpaceValue, a5 as UIButtonComponent, a6 as UIButtonOptions, a7 as UIPanelComponent, a8 as UIPanelOptions, a9 as UISliderComponent, aa as UISliderOptions, ab as UITextComponent, ac as UITextOptions, ad as UIToggleComponent, ae as UIToggleOptions, af as UiFollowMode, ag as UiTextAlign, ah as UiTextVAlign, ai as VRTeleportAreaComponent, aj as VRTeleportAreaOptions, ak as VideoComponent, al as VrTeleportShape, am as XRAnchorComponent, an as XRAnchorOptions, ao as XRHandPoseComponent, ap as XRHandPoseOptions, aq as XRInteractableComponent, ar as XRInteractableOptions, as as XRInteractorComponent, at as XRInteractorOptions, au as XRInteractorSideComponent, av as XRRigComponent, aw as XRRigOptions, ax as XrAttachPoint, ay as XrDistanceGrabBehavior, az as XrEnvironmentPhysics, aA as XrGrabGesture, aB as XrGrabHands, aC as XrGrabReach, aD as XrHandPoseEntry, aE as XrHandPreset, aF as XrInteractorSide, aG as XrInteractorType, aH as XrLocomotionMode, aI as XrTrackingOrigin, aJ as XrTwoHandRotation, aK as createAttachPoint, aL as createHandPoseEntry, aM as defaultMaterialValue, aN as handPoseDefinitionFor, aO as normalizePhysicsState, aP as setPhysicsLiveRef } from '../XRHandPose-CU3vNFuS.js';
|
|
2
|
+
export { D as DEFAULT_ENVIRONMENT_STATE, a as DEFAULT_POSTPROCESSING_STATE, E as Environment, b as EnvironmentFogState, c as EnvironmentFogType, d as EnvironmentGradientEffect, e as EnvironmentSkyboxState, f as EnvironmentSkyboxType, g as EnvironmentState, P as ParentId, h as Postprocessing, i as PostprocessingState, T as TransformComponent, j as TransformData, V as Vec3 } from '../System-V_mv07le.js';
|
|
3
|
+
export { P as PrefabInstance, a as PrefabNode, X as XREnvironmentComponent, b as XrEnvironmentKind, c as XrEnvironmentOrientation } from '../index-C3ik3sA1.js';
|
|
4
4
|
import 'three';
|
|
5
5
|
import '@dimforge/rapier3d-compat';
|
|
6
6
|
import '../handPose-DlcTtqgx.js';
|
package/dist/components/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { AnimationComponent, AudioComponent, CameraComponent, DEFAULT_COLLIDER_STATE, DEFAULT_ENVIRONMENT_STATE, DEFAULT_INSTANCED_MESH_PARAMETERS, DEFAULT_INSTANCE_TRANSFORM, DEFAULT_MESH_GEOMETRY_PARAMETERS, DEFAULT_PHYSICS_STATE, DEFAULT_POSE_CURLS, DEFAULT_POSE_DIRECTIONS, DEFAULT_POSTPROCESSING_STATE, DEFAULT_RIGIDBODY_STATE, EntityType, Environment, ImageComponent, ImageTarget, ImageTargetAnchor, InstanceSlotComponent, InstancedMeshComponent, LightComponent, MaterialComponent, MeshComponent, MeshGeometryComponent, ModelComponent, Name, Object3DRef, ParentId, PhysicsComponent, Postprocessing, PrefabInstance, PrefabNode, ScriptComponent, ScriptObjectComponent, Selectable, SelectionTransformState, SpriteComponent, TransformComponent, VRTeleportAreaComponent, VideoComponent, XRAnchorComponent, XREnvironmentComponent, XRHandPoseComponent, XRInteractableComponent, XRInteractorComponent, XRInteractorSideComponent, XRRigComponent, createAttachPoint, createHandPoseEntry, defaultMaterialValue, handPoseDefinitionFor, normalizePhysicsState, setPhysicsLiveRef } from '../chunk-
|
|
1
|
+
export { AnimationComponent, AudioComponent, CameraComponent, DEFAULT_COLLIDER_STATE, DEFAULT_ENVIRONMENT_STATE, DEFAULT_INSTANCED_MESH_PARAMETERS, DEFAULT_INSTANCE_TRANSFORM, DEFAULT_MESH_GEOMETRY_PARAMETERS, DEFAULT_PHYSICS_STATE, DEFAULT_POSE_CURLS, DEFAULT_POSE_DIRECTIONS, DEFAULT_POSTPROCESSING_STATE, DEFAULT_RIGIDBODY_STATE, EntityType, Environment, ImageComponent, ImageTarget, ImageTargetAnchor, InstanceSlotComponent, InstancedMeshComponent, LightComponent, MaterialComponent, MeshComponent, MeshGeometryComponent, ModelComponent, Name, Object3DRef, ParentId, PhysicsComponent, Postprocessing, PrefabInstance, PrefabNode, ScriptComponent, ScriptObjectComponent, Selectable, SelectionTransformState, SpriteComponent, TransformComponent, UIButtonComponent, UIPanelComponent, UISliderComponent, UITextComponent, UIToggleComponent, VRTeleportAreaComponent, VideoComponent, XRAnchorComponent, XREnvironmentComponent, XRHandPoseComponent, XRInteractableComponent, XRInteractorComponent, XRInteractorSideComponent, XRRigComponent, createAttachPoint, createHandPoseEntry, defaultMaterialValue, handPoseDefinitionFor, normalizePhysicsState, setPhysicsLiveRef } from '../chunk-BUEKOSR5.js';
|
|
2
2
|
import '../chunk-OY7ZODZP.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -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
|
|
|
@@ -64,6 +89,64 @@ If a user reports "it never asked to scan my room" and the geometry does not
|
|
|
64
89
|
match, they are almost certainly running against a stale scan from another
|
|
65
90
|
space; re-running Space Setup for the current room fixes it.
|
|
66
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
|
+
|
|
67
150
|
## Finding the real room from a script
|
|
68
151
|
|
|
69
152
|
Detected surfaces carry `XREnvironmentComponent`, so they are queryable like any
|
|
@@ -116,6 +199,36 @@ export default class RoomAware extends Behaviour {
|
|
|
116
199
|
}
|
|
117
200
|
```
|
|
118
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
|
+
|
|
119
232
|
## Anchors
|
|
120
233
|
|
|
121
234
|
A headset keeps revising where it thinks the room is. Content at fixed scene
|
|
@@ -129,6 +242,27 @@ on, and it stays put relative to the room.
|
|
|
129
242
|
Because the anchor owns the transform, moving the entity by script or physics has
|
|
130
243
|
no lasting effect — disable the component first, move it, then re-enable.
|
|
131
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
|
+
|
|
132
266
|
Anchors last for the session only. Persisting one across launches needs
|
|
133
267
|
`requestPersistentHandle`, which is a draft extension and not supported here.
|
|
134
268
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Spatial UI
|
|
2
|
+
|
|
3
|
+
Menus, HUDs, labels and settings panels that live **in the world**. UI is built
|
|
4
|
+
from five widget entities — **panel**, **text**, **button**, **toggle**,
|
|
5
|
+
**slider** — each a real entity with its own transform: move it with the gizmo,
|
|
6
|
+
parent it like anything else, save it with the scene.
|
|
7
|
+
|
|
8
|
+
Widgets render on both XR headsets (point with the ray or gaze, click with
|
|
9
|
+
trigger or pinch) and flat 3D experiences (hover and click with the mouse).
|
|
10
|
+
Hover and press feedback is built in — you never script your own highlight.
|
|
11
|
+
|
|
12
|
+
## The one sizing rule
|
|
13
|
+
|
|
14
|
+
**Widget size is authored in metres on the component — never with transform
|
|
15
|
+
scale.** The widget's face is a texture rasterised for the authored size;
|
|
16
|
+
scaling the entity stretches those texels into blur. To make a panel bigger,
|
|
17
|
+
raise its `width`/`height` fields and it re-renders crisply.
|
|
18
|
+
|
|
19
|
+
## Building a menu
|
|
20
|
+
|
|
21
|
+
1. Create a **UI Panel** — the backdrop. A comfortable read is 1–2 m from the
|
|
22
|
+
player, centred around eye height (y ≈ 1.2–1.6).
|
|
23
|
+
2. Create **Text**, **Button**, **Toggle**, **Slider** entities and parent
|
|
24
|
+
them under the panel. Place them with local transforms, a hair in front of
|
|
25
|
+
the backdrop (local z ≈ 0.01) so they draw over it.
|
|
26
|
+
3. Give each interactive widget an `actionId` (empty means "use the entity's
|
|
27
|
+
name").
|
|
28
|
+
4. Attach **one Behaviour to the panel**. UI events fire on the widget's own
|
|
29
|
+
entity first, then **bubble up the parent chain** — so the panel script
|
|
30
|
+
hears every child widget and can switch on `actionId`:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { Behaviour, type UiInteractionEvent } from "@relu-interactives/spatial-ecs";
|
|
34
|
+
|
|
35
|
+
export class MenuController extends Behaviour {
|
|
36
|
+
onUiPress(event: UiInteractionEvent) {
|
|
37
|
+
switch (event.actionId) {
|
|
38
|
+
case "start-game":
|
|
39
|
+
// A button under this panel was clicked.
|
|
40
|
+
break;
|
|
41
|
+
case "quit":
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onUiToggle(event: UiInteractionEvent) {
|
|
47
|
+
if (event.actionId === "music") {
|
|
48
|
+
const on = event.value === true;
|
|
49
|
+
console.log("music enabled:", on);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onUiChange(event: UiInteractionEvent) {
|
|
54
|
+
if (event.actionId === "volume" && typeof event.value === "number") {
|
|
55
|
+
console.log("volume:", event.value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Bubbling — not broadcast — is what keeps two copies of the same menu prefab
|
|
62
|
+
independent: each panel script hears only its own children.
|
|
63
|
+
|
|
64
|
+
## The widgets
|
|
65
|
+
|
|
66
|
+
| Widget | Component | What it is |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| Panel | `UIPanelComponent` | The flat backdrop. Rounded corners, border, translucency, follow mode. |
|
|
69
|
+
| Text | `UITextComponent` | Wrapped, aligned text. `fontSize` is metres of glyph height. |
|
|
70
|
+
| Button | `UIButtonComponent` | Pressable. Fires `onUiPress` on release. |
|
|
71
|
+
| Toggle | `UIToggleComponent` | On/off switch. Fires `onUiToggle`; `value` persists with the scene. |
|
|
72
|
+
| Slider | `UISliderComponent` | Draggable value in `[min, max]`. Fires `onUiChange`. |
|
|
73
|
+
|
|
74
|
+
Images need no widget: parent an ordinary `image` entity under the panel.
|
|
75
|
+
|
|
76
|
+
### Press semantics
|
|
77
|
+
|
|
78
|
+
A click is **press, then release while still pointing at the same widget** —
|
|
79
|
+
sliding off before releasing cancels, exactly like every platform's buttons.
|
|
80
|
+
While a press or slider drag is held, that hand cannot accidentally teleport
|
|
81
|
+
(suppressed the same way grabbing suppresses it).
|
|
82
|
+
|
|
83
|
+
`onUiChange` streams while a slider is dragged (only on real value changes)
|
|
84
|
+
and always fires once more with the final value on release.
|
|
85
|
+
|
|
86
|
+
### Text sizing
|
|
87
|
+
|
|
88
|
+
`fontSize` is **how tall the glyphs stand in the world**, in metres —
|
|
89
|
+
`0.035` reads like comfortable body text at arm's length, independent of the
|
|
90
|
+
panel's texture density. Lines wrap automatically to the component's `width`;
|
|
91
|
+
overflow past `height` is clamped with an ellipsis.
|
|
92
|
+
|
|
93
|
+
## Follow modes
|
|
94
|
+
|
|
95
|
+
- `follow: "world"` (default) — the panel stays where you put it.
|
|
96
|
+
- `follow: "billboard"` — the panel yaw-rotates to face the player every frame
|
|
97
|
+
at runtime (in the editor it holds still so you can work on it).
|
|
98
|
+
|
|
99
|
+
For a **wrist menu**, don't billboard — parent the panel under an XR
|
|
100
|
+
Interactor's **Left** or **Right** slot entity (the same slots used for custom
|
|
101
|
+
hand meshes). It then rides the hand like a watch.
|
|
102
|
+
|
|
103
|
+
## Reading and driving widgets from scripts
|
|
104
|
+
|
|
105
|
+
Widget state is ordinary component data:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { Behaviour } from "@relu-interactives/spatial-ecs";
|
|
109
|
+
import {
|
|
110
|
+
UISliderComponent,
|
|
111
|
+
UITextComponent,
|
|
112
|
+
} from "@relu-interactives/spatial-ecs/components";
|
|
113
|
+
|
|
114
|
+
export class ScoreHud extends Behaviour {
|
|
115
|
+
private score = 0;
|
|
116
|
+
|
|
117
|
+
addPoint() {
|
|
118
|
+
this.score += 1;
|
|
119
|
+
// This entity is the HUD text; editing the component re-renders it.
|
|
120
|
+
const text = this.world.getComponent(this.entityId, UITextComponent);
|
|
121
|
+
if (text) text.text = `Score: ${this.score}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
readVolume(sliderEntityId: number): number {
|
|
125
|
+
const slider = this.world.getComponent(sliderEntityId, UISliderComponent);
|
|
126
|
+
return slider ? slider.value : 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Style/text edits re-render the widget's texture on the next frame. Toggle and
|
|
132
|
+
slider `value` are authored state — what you set in the editor is the state
|
|
133
|
+
the experience starts with.
|
|
134
|
+
|
|
135
|
+
## Event payload
|
|
136
|
+
|
|
137
|
+
Every UI hook receives the same shape:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { Behaviour, type UiInteractionEvent } from "@relu-interactives/spatial-ecs";
|
|
141
|
+
|
|
142
|
+
export class Inspect extends Behaviour {
|
|
143
|
+
onUiPress(event: UiInteractionEvent) {
|
|
144
|
+
event.targetEntityId; // the widget that was activated
|
|
145
|
+
event.actionId; // its actionId, or its entity name
|
|
146
|
+
event.input; // the XR input source, or null for mouse clicks
|
|
147
|
+
event.value; // boolean (toggle) / number (slider); absent for presses
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
onUiHoverStart(event: UiInteractionEvent) {
|
|
151
|
+
// The pointer moved onto the widget (also fires for mouse hover).
|
|
152
|
+
console.log("hovering", event.targetEntityId);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Performance notes
|
|
158
|
+
|
|
159
|
+
- A widget's face redraws only when its **style or content** changes — an
|
|
160
|
+
idle menu costs nothing per frame.
|
|
161
|
+
- Slider drags and toggle flips move **geometry** (the knob and fill quads),
|
|
162
|
+
not pixels, so dragging never re-renders the texture.
|
|
163
|
+
- `pixelDensity` (panel) trades texture memory for sharpness: the default
|
|
164
|
+
1024 px/m is crisp at arm's length; raise it only for panels the player
|
|
165
|
+
inspects up close.
|
|
@@ -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. |
|