@relu-interactives/spatial-ecs 0.3.0 → 0.3.1

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 (45) hide show
  1. package/dist/{System-CYWZ_Zx5.d.cts → System-vrU46Vuw.d.cts} +16 -2
  2. package/dist/{System-CYWZ_Zx5.d.ts → System-vrU46Vuw.d.ts} +16 -2
  3. package/dist/{Video-AIQYTAAr.d.cts → XRRig-CAmf04NX.d.ts} +131 -4
  4. package/dist/{Video-DlZ4G9ig.d.ts → XRRig-Cu-EyliG.d.cts} +131 -4
  5. package/dist/{chunk-PZ4CAALA.js → chunk-67WGASUJ.js} +3 -3
  6. package/dist/{chunk-PZ4CAALA.js.map → chunk-67WGASUJ.js.map} +1 -1
  7. package/dist/{chunk-AT5SPSPM.js → chunk-PCOZLCXS.js} +160 -83
  8. package/dist/chunk-PCOZLCXS.js.map +1 -0
  9. package/dist/{chunk-7BPHZWQ6.js → chunk-U3MDYKPV.js} +47 -3
  10. package/dist/chunk-U3MDYKPV.js.map +1 -0
  11. package/dist/{chunk-7JFSEV7A.js → chunk-VXUVNC7S.js} +633 -6
  12. package/dist/chunk-VXUVNC7S.js.map +1 -0
  13. package/dist/components/index.cjs +44 -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/factories/index.cjs +228 -107
  19. package/dist/factories/index.cjs.map +1 -1
  20. package/dist/factories/index.d.cts +9 -3
  21. package/dist/factories/index.d.ts +9 -3
  22. package/dist/factories/index.js +3 -3
  23. package/dist/{index-Gr8aCPRM.d.cts → index-CUFd71ri.d.cts} +12 -3
  24. package/dist/index-D84o_UA6.d.ts +488 -0
  25. package/dist/index-YYypJaVw.d.cts +488 -0
  26. package/dist/{index-BwiRJNIT.d.ts → index-w1oqWl_M.d.ts} +12 -3
  27. package/dist/index.cjs +971 -198
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +42 -8
  30. package/dist/index.d.ts +42 -8
  31. package/dist/index.js +24 -8
  32. package/dist/index.js.map +1 -1
  33. package/dist/systems/index.cjs +673 -1
  34. package/dist/systems/index.cjs.map +1 -1
  35. package/dist/systems/index.d.cts +3 -278
  36. package/dist/systems/index.d.ts +3 -278
  37. package/dist/systems/index.js +3 -3
  38. package/dist/types.d.cts +1 -1
  39. package/dist/types.d.ts +1 -1
  40. package/dist/worldSnapshotUtils.d.cts +1 -1
  41. package/dist/worldSnapshotUtils.d.ts +1 -1
  42. package/package.json +1 -1
  43. package/dist/chunk-7BPHZWQ6.js.map +0 -1
  44. package/dist/chunk-7JFSEV7A.js.map +0 -1
  45. package/dist/chunk-AT5SPSPM.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { resolveDracoDecoderPath, applyTransform, SHARED_MODEL_RESOURCE_FLAG } from './chunk-PZ4CAALA.js';
2
- import { AnimationComponent, ModelComponent, CameraComponent, Object3DRef, Environment, Postprocessing, TransformComponent, PhysicsComponent, setPhysicsLiveRef, LightComponent, MaterialComponent, DEFAULT_MESH_GEOMETRY_PARAMETERS, MeshGeometryComponent, InstancedMeshComponent, ParentId, ScriptComponent, ScriptObjectComponent, InstanceSlotComponent, Name, ImageComponent, SpriteComponent, VideoComponent, AudioComponent, ImageTarget, Selectable, PrefabNode, PrefabInstance } from './chunk-7BPHZWQ6.js';
1
+ import { resolveDracoDecoderPath, applyTransform, SHARED_MODEL_RESOURCE_FLAG } from './chunk-67WGASUJ.js';
2
+ import { AnimationComponent, ModelComponent, CameraComponent, Object3DRef, Environment, Postprocessing, TransformComponent, PhysicsComponent, setPhysicsLiveRef, LightComponent, MaterialComponent, DEFAULT_MESH_GEOMETRY_PARAMETERS, MeshGeometryComponent, InstancedMeshComponent, ParentId, ScriptComponent, ScriptObjectComponent, InstanceSlotComponent, Name, ImageComponent, SpriteComponent, VideoComponent, AudioComponent, ImageTarget, Selectable, PrefabNode, PrefabInstance, XRInteractableComponent, XRRigComponent, VRTeleportAreaComponent } from './chunk-U3MDYKPV.js';
3
3
  import { __name } from './chunk-OY7ZODZP.js';
4
4
  import * as THREE7 from 'three';
5
5
  import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
@@ -501,6 +501,7 @@ var _CameraSyncSystem = class _CameraSyncSystem {
501
501
  update(world) {
502
502
  const entityIds = world.query(CameraComponent, Object3DRef);
503
503
  const windowAspect = readWindowAspect();
504
+ const xrCamera = world.isXrPresenting() ? world.getCamera() : null;
504
505
  const alive = new Set(entityIds);
505
506
  for (const trackedId of this.signatures.keys()) {
506
507
  if (!alive.has(trackedId)) {
@@ -517,6 +518,9 @@ var _CameraSyncSystem = class _CameraSyncSystem {
517
518
  if (!(camera instanceof THREE7.Camera)) {
518
519
  continue;
519
520
  }
521
+ if (xrCamera !== null && camera === xrCamera) {
522
+ continue;
523
+ }
520
524
  const nextSignature = buildSignature(component, camera, windowAspect);
521
525
  if (this.signatures.get(entityId) === nextSignature) {
522
526
  continue;
@@ -3488,7 +3492,9 @@ var HYDRATABLE_ENTITY_KINDS = /* @__PURE__ */ new Set([
3488
3492
  "areaLight",
3489
3493
  "instancedMesh",
3490
3494
  "instanceSlot",
3491
- "scriptObject"
3495
+ "scriptObject",
3496
+ "xrRig",
3497
+ "teleportArea"
3492
3498
  ]);
3493
3499
 
3494
3500
  // core/hydration/hydrateWorld.ts
@@ -4735,6 +4741,49 @@ var _Behaviour = class _Behaviour {
4735
4741
  */
4736
4742
  onTargetLost(target) {
4737
4743
  }
4744
+ // ─── WebXR (VR / MR) hooks ────────────────────────────────────────────────
4745
+ // Dispatched by XRSessionSystem / XRInteractionSystem, following the same
4746
+ // pattern as the AR hooks above: public no-op methods that subclasses opt
4747
+ // into by overriding. They never fire outside an immersive session.
4748
+ /**
4749
+ * Called once when an immersive session starts. Optional XR hook.
4750
+ *
4751
+ * Use it to set up XR-only state. Inspect `capabilities` rather than guessing
4752
+ * by device: feature support differs sharply per headset (for example Vision
4753
+ * Pro grants no passthrough on the web today).
4754
+ *
4755
+ * @param capabilities - What the session actually granted, read from
4756
+ * `XRSession.enabledFeatures`.
4757
+ */
4758
+ onXRSessionStart(capabilities) {
4759
+ }
4760
+ /** Called once when the immersive session ends. Optional XR hook. */
4761
+ onXRSessionEnd() {
4762
+ }
4763
+ /**
4764
+ * Called when a primary action begins — controller trigger, hand pinch, or
4765
+ * Vision Pro gaze-pinch. Optional XR hook.
4766
+ *
4767
+ * @param input - Normalized state for the source that acted.
4768
+ */
4769
+ onSelectStart(input) {
4770
+ }
4771
+ /** Called when the primary action ends. Optional XR hook. */
4772
+ onSelectEnd(input) {
4773
+ }
4774
+ /**
4775
+ * Called when this entity is grabbed. Optional XR hook.
4776
+ *
4777
+ * Only fires for entities carrying an enabled, grabbable
4778
+ * `XRInteractableComponent`.
4779
+ *
4780
+ * @param input - The source doing the grabbing.
4781
+ */
4782
+ onGrab(input) {
4783
+ }
4784
+ /** Called when this entity is released. Optional XR hook. */
4785
+ onRelease(input) {
4786
+ }
4738
4787
  };
4739
4788
  __name(_Behaviour, "Behaviour");
4740
4789
  /**
@@ -5582,12 +5631,16 @@ var ScriptObjectSystem = _ScriptObjectSystem;
5582
5631
  var _TransformSyncSystem = class _TransformSyncSystem {
5583
5632
  update(world) {
5584
5633
  const entities = world.query(TransformComponent, Object3DRef);
5634
+ const xrCamera = world.isXrPresenting() ? world.getCamera() : null;
5585
5635
  for (const entityId of entities) {
5586
5636
  const transform = world.getComponent(entityId, TransformComponent);
5587
5637
  const object = world.getComponent(entityId, Object3DRef)?.object;
5588
5638
  if (!transform || !object) {
5589
5639
  continue;
5590
5640
  }
5641
+ if (xrCamera !== null && object === xrCamera) {
5642
+ continue;
5643
+ }
5591
5644
  applyTransform(object, transform);
5592
5645
  object.updateMatrixWorld(true);
5593
5646
  }
@@ -5838,7 +5891,581 @@ var _VideoSystem = class _VideoSystem {
5838
5891
  };
5839
5892
  __name(_VideoSystem, "VideoSystem");
5840
5893
  var VideoSystem = _VideoSystem;
5894
+ function handednessOf(source) {
5895
+ const value = source.handedness;
5896
+ return value === "left" || value === "right" ? value : "none";
5897
+ }
5898
+ __name(handednessOf, "handednessOf");
5899
+ function kindOf(source) {
5900
+ const mode = source.targetRayMode;
5901
+ if (mode === "transient-pointer") return "transient-pointer";
5902
+ if (mode === "gaze") return "gaze";
5903
+ if (source.hand) return "hand";
5904
+ return "controller";
5905
+ }
5906
+ __name(kindOf, "kindOf");
5907
+ function blankState(source) {
5908
+ return {
5909
+ handedness: handednessOf(source),
5910
+ kind: kindOf(source),
5911
+ connected: true,
5912
+ rayOrigin: new THREE7.Vector3(),
5913
+ rayDirection: new THREE7.Vector3(0, 0, -1),
5914
+ gripPosition: null,
5915
+ gripQuaternion: null,
5916
+ select: false,
5917
+ selectStart: false,
5918
+ selectEnd: false,
5919
+ squeeze: false,
5920
+ squeezeStart: false,
5921
+ squeezeEnd: false,
5922
+ axes: [],
5923
+ hasAxes: false,
5924
+ buttons: []
5925
+ };
5926
+ }
5927
+ __name(blankState, "blankState");
5928
+ var _XRInputSystem = class _XRInputSystem {
5929
+ constructor() {
5930
+ this.records = /* @__PURE__ */ new Map();
5931
+ this.session = null;
5932
+ this.onSelectStart = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSelectStart"), "onSelectStart");
5933
+ this.onSelectEnd = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSelectEnd"), "onSelectEnd");
5934
+ this.onSqueezeStart = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSqueezeStart"), "onSqueezeStart");
5935
+ this.onSqueezeEnd = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSqueezeEnd"), "onSqueezeEnd");
5936
+ }
5937
+ mark(event, flag) {
5938
+ const record = this.ensureRecord(event.inputSource);
5939
+ record[flag] = true;
5940
+ }
5941
+ ensureRecord(source) {
5942
+ let record = this.records.get(source);
5943
+ if (!record) {
5944
+ record = {
5945
+ source,
5946
+ state: blankState(source),
5947
+ pendingSelectStart: false,
5948
+ pendingSelectEnd: false,
5949
+ pendingSqueezeStart: false,
5950
+ pendingSqueezeEnd: false,
5951
+ removed: false
5952
+ };
5953
+ this.records.set(source, record);
5954
+ }
5955
+ return record;
5956
+ }
5957
+ /** Bind to a session. Called by {@link XRSessionSystem} on session start. */
5958
+ attachSession(session) {
5959
+ this.detachSession();
5960
+ this.session = session;
5961
+ session.addEventListener("selectstart", this.onSelectStart);
5962
+ session.addEventListener("selectend", this.onSelectEnd);
5963
+ session.addEventListener(
5964
+ "squeezestart",
5965
+ this.onSqueezeStart
5966
+ );
5967
+ session.addEventListener("squeezeend", this.onSqueezeEnd);
5968
+ }
5969
+ /** Unbind and drop all per-source state. */
5970
+ detachSession() {
5971
+ const session = this.session;
5972
+ if (session) {
5973
+ session.removeEventListener(
5974
+ "selectstart",
5975
+ this.onSelectStart
5976
+ );
5977
+ session.removeEventListener(
5978
+ "selectend",
5979
+ this.onSelectEnd
5980
+ );
5981
+ session.removeEventListener(
5982
+ "squeezestart",
5983
+ this.onSqueezeStart
5984
+ );
5985
+ session.removeEventListener(
5986
+ "squeezeend",
5987
+ this.onSqueezeEnd
5988
+ );
5989
+ }
5990
+ this.session = null;
5991
+ this.records.clear();
5992
+ }
5993
+ update(world) {
5994
+ for (const [source, record] of this.records) {
5995
+ if (record.removed) {
5996
+ this.records.delete(source);
5997
+ continue;
5998
+ }
5999
+ record.state.selectStart = false;
6000
+ record.state.selectEnd = false;
6001
+ record.state.squeezeStart = false;
6002
+ record.state.squeezeEnd = false;
6003
+ }
6004
+ const session = this.session;
6005
+ if (!session || !world.isXrPresenting()) return;
6006
+ const renderer = world.getRenderer();
6007
+ const referenceSpace = renderer.xr.getReferenceSpace();
6008
+ const frame = renderer.xr.getFrame?.();
6009
+ const live = /* @__PURE__ */ new Set();
6010
+ for (const source of session.inputSources) {
6011
+ live.add(source);
6012
+ const record = this.ensureRecord(source);
6013
+ const state = record.state;
6014
+ state.connected = true;
6015
+ state.handedness = handednessOf(source);
6016
+ state.kind = kindOf(source);
6017
+ if (frame && referenceSpace && source.targetRaySpace) {
6018
+ const pose = frame.getPose(source.targetRaySpace, referenceSpace);
6019
+ if (pose) {
6020
+ const m = new THREE7.Matrix4().fromArray(pose.transform.matrix);
6021
+ state.rayOrigin.setFromMatrixPosition(m);
6022
+ state.rayDirection.set(0, 0, -1).applyQuaternion(
6023
+ new THREE7.Quaternion().setFromRotationMatrix(m)
6024
+ ).normalize();
6025
+ }
6026
+ }
6027
+ if (frame && referenceSpace && source.gripSpace) {
6028
+ const gripPose = frame.getPose(source.gripSpace, referenceSpace);
6029
+ if (gripPose) {
6030
+ const m = new THREE7.Matrix4().fromArray(gripPose.transform.matrix);
6031
+ state.gripPosition ?? (state.gripPosition = new THREE7.Vector3());
6032
+ state.gripQuaternion ?? (state.gripQuaternion = new THREE7.Quaternion());
6033
+ state.gripPosition.setFromMatrixPosition(m);
6034
+ state.gripQuaternion.setFromRotationMatrix(m);
6035
+ }
6036
+ } else {
6037
+ state.gripPosition = null;
6038
+ state.gripQuaternion = null;
6039
+ }
6040
+ const gamepad = source.gamepad;
6041
+ if (gamepad) {
6042
+ state.axes = Array.from(gamepad.axes);
6043
+ state.hasAxes = gamepad.axes.some((axis) => Number.isFinite(axis));
6044
+ state.buttons = gamepad.buttons.map((button) => button.pressed);
6045
+ } else {
6046
+ state.axes = [];
6047
+ state.hasAxes = false;
6048
+ state.buttons = [];
6049
+ }
6050
+ if (record.pendingSelectStart) {
6051
+ state.select = true;
6052
+ state.selectStart = true;
6053
+ record.pendingSelectStart = false;
6054
+ }
6055
+ if (record.pendingSelectEnd) {
6056
+ state.select = false;
6057
+ state.selectEnd = true;
6058
+ record.pendingSelectEnd = false;
6059
+ }
6060
+ if (record.pendingSqueezeStart) {
6061
+ state.squeeze = true;
6062
+ state.squeezeStart = true;
6063
+ record.pendingSqueezeStart = false;
6064
+ }
6065
+ if (record.pendingSqueezeEnd) {
6066
+ state.squeeze = false;
6067
+ state.squeezeEnd = true;
6068
+ record.pendingSqueezeEnd = false;
6069
+ }
6070
+ }
6071
+ for (const [source, record] of this.records) {
6072
+ if (live.has(source)) continue;
6073
+ record.state.connected = false;
6074
+ record.removed = true;
6075
+ }
6076
+ }
6077
+ /** Every input source active this frame. */
6078
+ getInputs() {
6079
+ return Array.from(this.records.values(), (record) => record.state);
6080
+ }
6081
+ /** The first source for a hand, or null. Prefer this for hand-specific logic. */
6082
+ getInput(handedness) {
6083
+ for (const record of this.records.values()) {
6084
+ if (record.state.handedness === handedness) return record.state;
6085
+ }
6086
+ return null;
6087
+ }
6088
+ /** True while ANY source is selecting — the portable "is the user acting" check. */
6089
+ isSelecting() {
6090
+ for (const record of this.records.values()) {
6091
+ if (record.state.select) return true;
6092
+ }
6093
+ return false;
6094
+ }
6095
+ dispose() {
6096
+ this.detachSession();
6097
+ }
6098
+ };
6099
+ __name(_XRInputSystem, "XRInputSystem");
6100
+ var XRInputSystem = _XRInputSystem;
6101
+ var RAYLESS_HOLD_DISTANCE = 0.5;
6102
+ var _XRInteractionSystem = class _XRInteractionSystem {
6103
+ constructor(inputSystem) {
6104
+ this.raycaster = new THREE7.Raycaster();
6105
+ /** Which entity each source is holding. Keyed by source state identity. */
6106
+ this.grabs = /* @__PURE__ */ new Map();
6107
+ this.hoveredEntityId = null;
6108
+ this.inputSystem = inputSystem;
6109
+ }
6110
+ update(world) {
6111
+ if (!world.isXrPresenting()) {
6112
+ this.releaseAll(world);
6113
+ return;
6114
+ }
6115
+ const inputs = this.inputSystem.getInputs();
6116
+ const interactables = this.collectInteractables(world);
6117
+ this.hoveredEntityId = null;
6118
+ for (const input of inputs) {
6119
+ if (!input.connected) {
6120
+ this.release(world, input);
6121
+ continue;
6122
+ }
6123
+ const held = this.grabs.get(input);
6124
+ if (held) {
6125
+ this.moveHeld(world, input, held);
6126
+ if (input.selectEnd) this.release(world, input);
6127
+ continue;
6128
+ }
6129
+ const hitEntityId = this.raycast(input, interactables);
6130
+ if (hitEntityId === null) continue;
6131
+ if (this.hoveredEntityId === null) {
6132
+ this.hoveredEntityId = hitEntityId;
6133
+ }
6134
+ const interactable = world.getComponent(
6135
+ hitEntityId,
6136
+ XRInteractableComponent
6137
+ );
6138
+ if (input.selectStart && interactable?.grabbable) {
6139
+ this.grab(world, input, hitEntityId);
6140
+ }
6141
+ }
6142
+ }
6143
+ collectInteractables(world) {
6144
+ const out = [];
6145
+ for (const entityId of world.query(XRInteractableComponent, Object3DRef)) {
6146
+ const interactable = world.getComponent(entityId, XRInteractableComponent);
6147
+ if (!interactable?.enabled || !interactable.pointable) continue;
6148
+ if (!world.isEntityVisible(entityId)) continue;
6149
+ const object = world.getComponent(entityId, Object3DRef)?.object;
6150
+ if (object) out.push({ entityId, object });
6151
+ }
6152
+ return out;
6153
+ }
6154
+ /** Entity id of the nearest pointable hit along this source's ray, or null. */
6155
+ raycast(input, interactables) {
6156
+ if (interactables.length === 0) return null;
6157
+ this.raycaster.set(input.rayOrigin, input.rayDirection);
6158
+ let nearestId = null;
6159
+ let nearestDistance = Number.POSITIVE_INFINITY;
6160
+ for (const entry of interactables) {
6161
+ const hit = this.raycaster.intersectObject(entry.object, true)[0];
6162
+ if (!hit) continue;
6163
+ if (hit.distance < nearestDistance) {
6164
+ nearestDistance = hit.distance;
6165
+ nearestId = entry.entityId;
6166
+ }
6167
+ }
6168
+ return nearestId;
6169
+ }
6170
+ grab(world, input, entityId) {
6171
+ const transform = world.getComponent(entityId, TransformComponent);
6172
+ if (!transform) return;
6173
+ const hand = this.handPosition(input);
6174
+ const offset = new THREE7.Vector3(
6175
+ transform.position.x - hand.x,
6176
+ transform.position.y - hand.y,
6177
+ transform.position.z - hand.z
6178
+ );
6179
+ const physics = world.getComponent(entityId, PhysicsComponent);
6180
+ let restoreBodyType = null;
6181
+ const interactable = world.getComponent(entityId, XRInteractableComponent);
6182
+ if (physics && interactable?.grabMode === "kinematic") {
6183
+ restoreBodyType = physics.values.rigidbody.type;
6184
+ physics.values.rigidbody.type = "kinematicPosition";
6185
+ }
6186
+ this.grabs.set(input, { entityId, offset, restoreBodyType });
6187
+ this.dispatch(world, entityId, (b) => b.onGrab(input));
6188
+ }
6189
+ moveHeld(world, input, held) {
6190
+ const transform = world.getComponent(held.entityId, TransformComponent);
6191
+ if (!transform) return;
6192
+ const hand = this.handPosition(input);
6193
+ transform.position.x = hand.x + held.offset.x;
6194
+ transform.position.y = hand.y + held.offset.y;
6195
+ transform.position.z = hand.z + held.offset.z;
6196
+ }
6197
+ release(world, input) {
6198
+ const held = this.grabs.get(input);
6199
+ if (!held) return;
6200
+ this.grabs.delete(input);
6201
+ if (held.restoreBodyType !== null) {
6202
+ const physics = world.getComponent(held.entityId, PhysicsComponent);
6203
+ if (physics) {
6204
+ physics.values.rigidbody.type = held.restoreBodyType;
6205
+ }
6206
+ }
6207
+ this.dispatch(world, held.entityId, (b) => b.onRelease(input));
6208
+ }
6209
+ releaseAll(world) {
6210
+ for (const input of Array.from(this.grabs.keys())) {
6211
+ this.release(world, input);
6212
+ }
6213
+ }
6214
+ /**
6215
+ * Where the "hand" is for this source. Controllers and tracked hands report a
6216
+ * grip pose; gaze and transient-pointer sources have none, so we hold the
6217
+ * object a fixed distance along the ray instead.
6218
+ */
6219
+ handPosition(input) {
6220
+ if (input.gripPosition) return input.gripPosition;
6221
+ return new THREE7.Vector3().copy(input.rayDirection).multiplyScalar(RAYLESS_HOLD_DISTANCE).add(input.rayOrigin);
6222
+ }
6223
+ dispatch(world, entityId, visit) {
6224
+ const script = world.getComponent(entityId, ScriptComponent);
6225
+ const instances = script?.scriptInstances;
6226
+ if (!instances) return;
6227
+ for (const instance of instances.values()) {
6228
+ if (!(instance instanceof Behaviour)) continue;
6229
+ try {
6230
+ visit(instance);
6231
+ } catch (error) {
6232
+ console.error("[XRInteractionSystem] Behaviour hook failed:", error);
6233
+ }
6234
+ }
6235
+ }
6236
+ /** Entity currently under a pointer this frame, or null. */
6237
+ getHoveredEntityId() {
6238
+ return this.hoveredEntityId;
6239
+ }
6240
+ /** True when any source is holding something. */
6241
+ isGrabbing() {
6242
+ return this.grabs.size > 0;
6243
+ }
6244
+ };
6245
+ __name(_XRInteractionSystem, "XRInteractionSystem");
6246
+ var XRInteractionSystem = _XRInteractionSystem;
6247
+ var UP = new THREE7.Vector3(0, 1, 0);
6248
+ var AXIS_DEADZONE = 0.5;
6249
+ var _XRLocomotionSystem = class _XRLocomotionSystem {
6250
+ constructor(inputSystem) {
6251
+ this.raycaster = new THREE7.Raycaster();
6252
+ /** Per-source latch so one stick push yields exactly one snap turn. */
6253
+ this.turnLatched = /* @__PURE__ */ new WeakSet();
6254
+ /** Where the pointer currently lands, for the consumer to draw a marker. */
6255
+ this.teleportPoint = null;
6256
+ this.inputSystem = inputSystem;
6257
+ }
6258
+ update(world, deltaTime) {
6259
+ this.teleportPoint = null;
6260
+ if (!world.isXrPresenting()) return;
6261
+ const rigEntityId = world.query(XRRigComponent, TransformComponent)[0];
6262
+ if (typeof rigEntityId !== "number") return;
6263
+ const rig = world.getComponent(rigEntityId, XRRigComponent);
6264
+ const rigTransform = world.getComponent(rigEntityId, TransformComponent);
6265
+ if (!rig || !rigTransform) return;
6266
+ const inputs = this.inputSystem.getInputs();
6267
+ if (inputs.length === 0) return;
6268
+ if (rig.snapTurn) {
6269
+ this.applySnapTurn(rig, rigTransform, inputs);
6270
+ }
6271
+ if (rig.locomotion === "teleport") {
6272
+ this.applyTeleport(world, rigTransform, inputs);
6273
+ } else if (rig.locomotion === "smooth") {
6274
+ this.applySmoothMove(world, rig, rigTransform, inputs, deltaTime);
6275
+ }
6276
+ }
6277
+ /**
6278
+ * Snap turning, not smooth turning — rotating the view continuously is the
6279
+ * single biggest cause of VR nausea, so the comfort-safe option is the one
6280
+ * wired to the stick.
6281
+ */
6282
+ applySnapTurn(rig, transform, inputs) {
6283
+ for (const input of inputs) {
6284
+ if (!input.hasAxes || input.axes.length < 3) continue;
6285
+ const x = input.axes[2] ?? 0;
6286
+ const source = input;
6287
+ if (Math.abs(x) < AXIS_DEADZONE) {
6288
+ this.turnLatched.delete(source);
6289
+ continue;
6290
+ }
6291
+ if (this.turnLatched.has(source)) continue;
6292
+ this.turnLatched.add(source);
6293
+ const step = THREE7.MathUtils.degToRad(rig.snapTurnDegrees);
6294
+ transform.rotation.y -= Math.sign(x) * step;
6295
+ }
6296
+ }
6297
+ applySmoothMove(world, rig, transform, inputs, deltaTime) {
6298
+ for (const input of inputs) {
6299
+ if (!input.hasAxes || input.axes.length < 2) continue;
6300
+ const x = input.axes[0] ?? 0;
6301
+ const y = input.axes[1] ?? 0;
6302
+ if (Math.abs(x) < 0.15 && Math.abs(y) < 0.15) continue;
6303
+ const camera = world.getCamera();
6304
+ const forward = new THREE7.Vector3();
6305
+ camera.getWorldDirection(forward);
6306
+ forward.y = 0;
6307
+ if (forward.lengthSq() < 1e-6) continue;
6308
+ forward.normalize();
6309
+ const right = new THREE7.Vector3().crossVectors(forward, UP).normalize();
6310
+ const step = rig.moveSpeed * Math.max(0, deltaTime);
6311
+ transform.position.x += (forward.x * -y + right.x * x) * step;
6312
+ transform.position.z += (forward.z * -y + right.z * x) * step;
6313
+ }
6314
+ }
6315
+ applyTeleport(world, transform, inputs) {
6316
+ const targets = this.collectTeleportTargets(world);
6317
+ if (targets.length === 0) return;
6318
+ for (const input of inputs) {
6319
+ this.raycaster.set(input.rayOrigin, input.rayDirection);
6320
+ const hit = this.raycaster.intersectObjects(targets, true)[0];
6321
+ if (!hit) continue;
6322
+ this.teleportPoint = hit.point.clone();
6323
+ if (input.selectEnd) {
6324
+ transform.position.x = hit.point.x;
6325
+ transform.position.y = hit.point.y;
6326
+ transform.position.z = hit.point.z;
6327
+ }
6328
+ }
6329
+ }
6330
+ collectTeleportTargets(world) {
6331
+ const objects = [];
6332
+ for (const entityId of world.query(VRTeleportAreaComponent, Object3DRef)) {
6333
+ const area = world.getComponent(entityId, VRTeleportAreaComponent);
6334
+ if (!area?.enabled) continue;
6335
+ if (!world.isEntityVisible(entityId)) continue;
6336
+ const object = world.getComponent(entityId, Object3DRef)?.object;
6337
+ if (object) objects.push(object);
6338
+ }
6339
+ return objects;
6340
+ }
6341
+ /**
6342
+ * Where the teleport pointer currently lands this frame, or null.
6343
+ * Consumers use this to render an aim marker.
6344
+ */
6345
+ getTeleportPoint() {
6346
+ return this.teleportPoint;
6347
+ }
6348
+ };
6349
+ __name(_XRLocomotionSystem, "XRLocomotionSystem");
6350
+ var XRLocomotionSystem = _XRLocomotionSystem;
6351
+
6352
+ // core/xr/types.ts
6353
+ var NO_XR_CAPABILITIES = {
6354
+ mode: "inline",
6355
+ handTracking: false,
6356
+ planeDetection: false,
6357
+ meshDetection: false,
6358
+ depthSensing: false,
6359
+ anchors: false,
6360
+ hitTest: false,
6361
+ enabledFeatures: []
6362
+ };
6363
+ function readXrCapabilities(session, mode = "immersive-vr") {
6364
+ if (!session) return NO_XR_CAPABILITIES;
6365
+ const raw = session.enabledFeatures;
6366
+ const features = Array.isArray(raw) ? Array.from(raw) : [];
6367
+ const has = /* @__PURE__ */ __name((name) => features.includes(name), "has");
6368
+ return {
6369
+ mode,
6370
+ handTracking: has("hand-tracking"),
6371
+ planeDetection: has("plane-detection"),
6372
+ meshDetection: has("mesh-detection"),
6373
+ depthSensing: has("depth-sensing"),
6374
+ anchors: has("anchors"),
6375
+ hitTest: has("hit-test"),
6376
+ enabledFeatures: features
6377
+ };
6378
+ }
6379
+ __name(readXrCapabilities, "readXrCapabilities");
6380
+
6381
+ // core/systems/XRSessionSystem.ts
6382
+ var _XRSessionSystem = class _XRSessionSystem {
6383
+ constructor(inputSystem) {
6384
+ this.presenting = false;
6385
+ this.capabilities = NO_XR_CAPABILITIES;
6386
+ this.inputSystem = inputSystem ?? null;
6387
+ }
6388
+ update(world) {
6389
+ const presenting = world.isXrPresenting();
6390
+ if (presenting !== this.presenting) {
6391
+ this.presenting = presenting;
6392
+ if (presenting) {
6393
+ this.handleSessionStart(world);
6394
+ } else {
6395
+ this.handleSessionEnd(world);
6396
+ }
6397
+ }
6398
+ if (presenting) {
6399
+ this.dispatchInputEdges(world);
6400
+ }
6401
+ }
6402
+ handleSessionStart(world) {
6403
+ const session = world.getRenderer().xr.getSession();
6404
+ const mode = session && session.environmentBlendMode !== "opaque" ? "immersive-ar" : "immersive-vr";
6405
+ this.capabilities = readXrCapabilities(session ?? null, mode);
6406
+ if (session && this.inputSystem) {
6407
+ this.inputSystem.attachSession(session);
6408
+ }
6409
+ this.forEachBehaviour(world, (behaviour) => {
6410
+ behaviour.onXRSessionStart(this.capabilities);
6411
+ });
6412
+ }
6413
+ handleSessionEnd(world) {
6414
+ this.capabilities = NO_XR_CAPABILITIES;
6415
+ this.inputSystem?.detachSession();
6416
+ this.forEachBehaviour(world, (behaviour) => {
6417
+ behaviour.onXRSessionEnd();
6418
+ });
6419
+ }
6420
+ /**
6421
+ * Relay this frame's one-frame select edges. Polling the latched state (rather
6422
+ * than dispatching from the DOM event) keeps hook delivery on the ECS tick, so
6423
+ * a Behaviour's `onSelectStart` always observes the same world state its
6424
+ * `onUpdate` would.
6425
+ */
6426
+ dispatchInputEdges(world) {
6427
+ const inputs = this.inputSystem?.getInputs();
6428
+ if (!inputs || inputs.length === 0) return;
6429
+ for (const input of inputs) {
6430
+ if (input.selectStart) {
6431
+ this.forEachBehaviour(world, (b) => b.onSelectStart(input));
6432
+ }
6433
+ if (input.selectEnd) {
6434
+ this.forEachBehaviour(world, (b) => b.onSelectEnd(input));
6435
+ }
6436
+ }
6437
+ }
6438
+ forEachBehaviour(world, visit) {
6439
+ for (const entityId of world.query(ScriptComponent)) {
6440
+ const script = world.getComponent(entityId, ScriptComponent);
6441
+ const instances = script?.scriptInstances;
6442
+ if (!instances) continue;
6443
+ for (const instance of instances.values()) {
6444
+ if (!(instance instanceof Behaviour)) continue;
6445
+ try {
6446
+ visit(instance);
6447
+ } catch (error) {
6448
+ console.error("[XRSessionSystem] Behaviour hook failed:", error);
6449
+ }
6450
+ }
6451
+ }
6452
+ }
6453
+ /** What the active session granted. `NO_XR_CAPABILITIES` when not presenting. */
6454
+ getCapabilities() {
6455
+ return this.capabilities;
6456
+ }
6457
+ /** True while an immersive session is presenting. */
6458
+ isPresenting() {
6459
+ return this.presenting;
6460
+ }
6461
+ /** Convenience passthrough so consumers need only hold this system. */
6462
+ getInputs() {
6463
+ return this.inputSystem?.getInputs() ?? [];
6464
+ }
6465
+ };
6466
+ __name(_XRSessionSystem, "XRSessionSystem");
6467
+ var XRSessionSystem = _XRSessionSystem;
5841
6468
 
5842
- export { ADDED_NODE_MARKER_COMPONENT, AnimationSystem, AudioSystem, Behaviour, CameraSyncSystem, EnvironmentSyncSystem, HYDRATABLE_ENTITY_KINDS, InputSystem, InstancedMeshSystem, KeyboardKeys, LightSyncSystem, MaterialSyncSystem, MeshGeometrySystem, NODE_KEY_SEPARATOR, OVERRIDE_DIFF_EXCLUDED_COMPONENTS, OVERRIDE_ROOT_EXCLUDED_COMPONENTS, ObjectVisibilitySystem, PREFAB_FILE_VERSION, PREFAB_FILE_VERSION_NESTED, PhysicsSyncSystem, PostprocessingSyncSystem, RenderSystem, SCRIPT_OBJECT_ROOT_FLAG, ScriptBehaviourSystem, ScriptObject, ScriptObjectSystem, TransformSyncSystem, VideoSystem, buildComponentValueMaps, buildNodeKeyMapsFromSnapshot, buildNodeMapsFromSnapshot, canonicalizeComponentValue, clearPrefabCache, collectDiffPaths, collectEntitySubtree, collectSnapshotSubtreeIds, computeInstanceOverrides, decanonicalizeComponentValue, despawnPrefab, encodeOverride, findSnapshotRootEntityIds, findTopLevelNestedRegions, groupOverridePaths, hydrateWorldFromSnapshot, hydrateWorldFromSnapshotDetailed, indexSnapshotByEntity, instantiateSubtree, loadPrefab, mergeComponentValue, parseOverride, parsePrefabAsset, prefabFileVersionFor, restoreComponentState, restorePostCreateComponents, setPrefabAssetUrlResolver, spawnPrefab };
5843
- //# sourceMappingURL=chunk-7JFSEV7A.js.map
5844
- //# sourceMappingURL=chunk-7JFSEV7A.js.map
6469
+ export { ADDED_NODE_MARKER_COMPONENT, AnimationSystem, AudioSystem, Behaviour, CameraSyncSystem, EnvironmentSyncSystem, HYDRATABLE_ENTITY_KINDS, InputSystem, InstancedMeshSystem, KeyboardKeys, LightSyncSystem, MaterialSyncSystem, MeshGeometrySystem, NODE_KEY_SEPARATOR, NO_XR_CAPABILITIES, OVERRIDE_DIFF_EXCLUDED_COMPONENTS, OVERRIDE_ROOT_EXCLUDED_COMPONENTS, ObjectVisibilitySystem, PREFAB_FILE_VERSION, PREFAB_FILE_VERSION_NESTED, PhysicsSyncSystem, PostprocessingSyncSystem, RenderSystem, SCRIPT_OBJECT_ROOT_FLAG, ScriptBehaviourSystem, ScriptObject, ScriptObjectSystem, TransformSyncSystem, VideoSystem, XRInputSystem, XRInteractionSystem, XRLocomotionSystem, XRSessionSystem, buildComponentValueMaps, buildNodeKeyMapsFromSnapshot, buildNodeMapsFromSnapshot, canonicalizeComponentValue, clearPrefabCache, collectDiffPaths, collectEntitySubtree, collectSnapshotSubtreeIds, computeInstanceOverrides, decanonicalizeComponentValue, despawnPrefab, encodeOverride, findSnapshotRootEntityIds, findTopLevelNestedRegions, groupOverridePaths, hydrateWorldFromSnapshot, hydrateWorldFromSnapshotDetailed, indexSnapshotByEntity, instantiateSubtree, loadPrefab, mergeComponentValue, parseOverride, parsePrefabAsset, prefabFileVersionFor, readXrCapabilities, restoreComponentState, restorePostCreateComponents, setPrefabAssetUrlResolver, spawnPrefab };
6470
+ //# sourceMappingURL=chunk-VXUVNC7S.js.map
6471
+ //# sourceMappingURL=chunk-VXUVNC7S.js.map