@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.
Files changed (50) hide show
  1. package/dist/{System-ln4Q7Eot.d.cts → System-V_mv07le.d.cts} +2 -2
  2. package/dist/{System-ln4Q7Eot.d.ts → System-V_mv07le.d.ts} +2 -2
  3. package/dist/{XRHandPose-CxPv-LBo.d.cts → XRHandPose-CQTLGxFC.d.cts} +218 -2
  4. package/dist/{XRHandPose-BImIa4ZS.d.ts → XRHandPose-CU3vNFuS.d.ts} +218 -2
  5. package/dist/{chunk-4AK4I3QF.js → chunk-BUEKOSR5.js} +102 -3
  6. package/dist/chunk-BUEKOSR5.js.map +1 -0
  7. package/dist/{chunk-SGADE55G.js → chunk-RXBLOO5B.js} +249 -90
  8. package/dist/chunk-RXBLOO5B.js.map +1 -0
  9. package/dist/{chunk-4DWZK4X4.js → chunk-WJD2VKEH.js} +3 -3
  10. package/dist/chunk-WJD2VKEH.js.map +1 -0
  11. package/dist/{chunk-OY2OONRR.js → chunk-Z6ZMR3CS.js} +1374 -30
  12. package/dist/chunk-Z6ZMR3CS.js.map +1 -0
  13. package/dist/components/index.cjs +99 -0
  14. package/dist/components/index.cjs.map +1 -1
  15. package/dist/components/index.d.cts +3 -3
  16. package/dist/components/index.d.ts +3 -3
  17. package/dist/components/index.js +1 -1
  18. package/dist/docs/guide/mixed-reality.md +137 -3
  19. package/dist/docs/guide/spatial-ui.md +165 -0
  20. package/dist/docs/guide/vr-and-hand-poses.md +29 -0
  21. package/dist/docs/guide/world-api.md +1 -1
  22. package/dist/factories/index.cjs +372 -114
  23. package/dist/factories/index.cjs.map +1 -1
  24. package/dist/factories/index.d.cts +13 -3
  25. package/dist/factories/index.d.ts +13 -3
  26. package/dist/factories/index.js +3 -3
  27. package/dist/{index-CuGqxuk-.d.ts → index-C3ik3sA1.d.ts} +16 -3
  28. package/dist/{index-B2oHfqkj.d.ts → index-CppKDxbu.d.ts} +187 -3
  29. package/dist/{index-CQsHUez3.d.cts → index-Dlr-7-4m.d.cts} +16 -3
  30. package/dist/{index-7YCk4cmQ.d.cts → index-I2IsocYW.d.cts} +187 -3
  31. package/dist/index.cjs +1977 -366
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +42 -9
  34. package/dist/index.d.ts +42 -9
  35. package/dist/index.js +10 -8
  36. package/dist/index.js.map +1 -1
  37. package/dist/systems/index.cjs +1465 -25
  38. package/dist/systems/index.cjs.map +1 -1
  39. package/dist/systems/index.d.cts +2 -2
  40. package/dist/systems/index.d.ts +2 -2
  41. package/dist/systems/index.js +3 -3
  42. package/dist/types.d.cts +1 -1
  43. package/dist/types.d.ts +1 -1
  44. package/dist/worldSnapshotUtils.d.cts +1 -1
  45. package/dist/worldSnapshotUtils.d.ts +1 -1
  46. package/package.json +1 -1
  47. package/dist/chunk-4AK4I3QF.js.map +0 -1
  48. package/dist/chunk-4DWZK4X4.js.map +0 -1
  49. package/dist/chunk-OY2OONRR.js.map +0 -1
  50. 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 VRTeleportAreaComponent, a6 as VRTeleportAreaOptions, a7 as VideoComponent, a8 as VrTeleportShape, a9 as XRAnchorComponent, aa as XRAnchorOptions, ab as XRHandPoseComponent, ac as XRHandPoseOptions, ad as XRInteractableComponent, ae as XRInteractableOptions, af as XRInteractorComponent, ag as XRInteractorOptions, ah as XRInteractorSideComponent, ai as XRRigComponent, aj as XRRigOptions, ak as XrAttachPoint, al as XrDistanceGrabBehavior, am as XrEnvironmentPhysics, an as XrGrabGesture, ao as XrGrabHands, ap as XrGrabReach, aq as XrHandPoseEntry, ar as XrHandPreset, as as XrInteractorSide, at as XrInteractorType, au as XrLocomotionMode, av as XrTrackingOrigin, aw as XrTwoHandRotation, ax as createAttachPoint, ay as createHandPoseEntry, az as defaultMaterialValue, aA as handPoseDefinitionFor, aB as normalizePhysicsState, aC as setPhysicsLiveRef } from '../XRHandPose-CxPv-LBo.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-ln4Q7Eot.cjs';
3
- export { P as PrefabInstance, a as PrefabNode, X as XREnvironmentComponent, b as XrEnvironmentKind, c as XrEnvironmentOrientation } from '../index-CQsHUez3.cjs';
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 VRTeleportAreaComponent, a6 as VRTeleportAreaOptions, a7 as VideoComponent, a8 as VrTeleportShape, a9 as XRAnchorComponent, aa as XRAnchorOptions, ab as XRHandPoseComponent, ac as XRHandPoseOptions, ad as XRInteractableComponent, ae as XRInteractableOptions, af as XRInteractorComponent, ag as XRInteractorOptions, ah as XRInteractorSideComponent, ai as XRRigComponent, aj as XRRigOptions, ak as XrAttachPoint, al as XrDistanceGrabBehavior, am as XrEnvironmentPhysics, an as XrGrabGesture, ao as XrGrabHands, ap as XrGrabReach, aq as XrHandPoseEntry, ar as XrHandPreset, as as XrInteractorSide, at as XrInteractorType, au as XrLocomotionMode, av as XrTrackingOrigin, aw as XrTwoHandRotation, ax as createAttachPoint, ay as createHandPoseEntry, az as defaultMaterialValue, aA as handPoseDefinitionFor, aB as normalizePhysicsState, aC as setPhysicsLiveRef } from '../XRHandPose-BImIa4ZS.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-ln4Q7Eot.js';
3
- export { P as PrefabInstance, a as PrefabNode, X as XREnvironmentComponent, b as XrEnvironmentKind, c as XrEnvironmentOrientation } from '../index-CuGqxuk-.js';
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';
@@ -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-4AK4I3QF.js';
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. Safari on visionOS grants
11
- `immersive-ar` to no web page at all, so a passthrough project falls back to an
12
- ordinary VR session there rather than failing to start.
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(SystemClass)` | Resolve a registered system instance. |
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. |