@relu-interactives/spatial-ecs 0.3.0 → 0.3.2

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-JWTRFJBL.js} +168 -83
  8. package/dist/chunk-JWTRFJBL.js.map +1 -0
  9. package/dist/{chunk-7JFSEV7A.js → chunk-SALBG747.js} +757 -6
  10. package/dist/chunk-SALBG747.js.map +1 -0
  11. package/dist/{chunk-7BPHZWQ6.js → chunk-U3MDYKPV.js} +47 -3
  12. package/dist/chunk-U3MDYKPV.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 +236 -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-Bc0YCz1A.d.cts +514 -0
  24. package/dist/index-CRmfSYdj.d.ts +514 -0
  25. package/dist/{index-Gr8aCPRM.d.cts → index-CUFd71ri.d.cts} +12 -3
  26. package/dist/{index-BwiRJNIT.d.ts → index-w1oqWl_M.d.ts} +12 -3
  27. package/dist/index.cjs +1107 -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 +26 -8
  32. package/dist/index.js.map +1 -1
  33. package/dist/systems/index.cjs +799 -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
@@ -18,6 +18,8 @@ var OutputPass_js = require('three/examples/jsm/postprocessing/OutputPass.js');
18
18
  var HueSaturationShader_js = require('three/examples/jsm/shaders/HueSaturationShader.js');
19
19
  var SepiaShader_js = require('three/examples/jsm/shaders/SepiaShader.js');
20
20
  var VignetteShader_js = require('three/examples/jsm/shaders/VignetteShader.js');
21
+ var XRControllerModelFactory_js = require('three/examples/jsm/webxr/XRControllerModelFactory.js');
22
+ var XRHandModelFactory_js = require('three/examples/jsm/webxr/XRHandModelFactory.js');
21
23
 
22
24
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
23
25
  function _interopNamespace(e) {
@@ -2075,6 +2077,47 @@ var _VideoComponent = class _VideoComponent extends Component {
2075
2077
  __name(_VideoComponent, "VideoComponent");
2076
2078
  var VideoComponent = _VideoComponent;
2077
2079
 
2080
+ // core/components/VRTeleportArea.ts
2081
+ var _VRTeleportAreaComponent = class _VRTeleportAreaComponent extends Component {
2082
+ constructor(options) {
2083
+ super();
2084
+ this.enabled = options?.enabled ?? true;
2085
+ this.shape = options?.shape ?? "plane";
2086
+ this.width = options?.width ?? 10;
2087
+ this.depth = options?.depth ?? 10;
2088
+ }
2089
+ };
2090
+ __name(_VRTeleportAreaComponent, "VRTeleportAreaComponent");
2091
+ var VRTeleportAreaComponent = _VRTeleportAreaComponent;
2092
+
2093
+ // core/components/XRInteractable.ts
2094
+ var _XRInteractableComponent = class _XRInteractableComponent extends Component {
2095
+ constructor(options) {
2096
+ super();
2097
+ this.enabled = options?.enabled ?? true;
2098
+ this.grabbable = options?.grabbable ?? true;
2099
+ this.pointable = options?.pointable ?? true;
2100
+ this.grabMode = options?.grabMode ?? "kinematic";
2101
+ this.highlightOnHover = options?.highlightOnHover ?? true;
2102
+ }
2103
+ };
2104
+ __name(_XRInteractableComponent, "XRInteractableComponent");
2105
+ var XRInteractableComponent = _XRInteractableComponent;
2106
+
2107
+ // core/components/XRRig.ts
2108
+ var _XRRigComponent = class _XRRigComponent extends Component {
2109
+ constructor(options) {
2110
+ super();
2111
+ this.locomotion = options?.locomotion ?? "teleport";
2112
+ this.moveSpeed = options?.moveSpeed ?? 2;
2113
+ this.snapTurn = options?.snapTurn ?? true;
2114
+ this.snapTurnDegrees = options?.snapTurnDegrees ?? 30;
2115
+ this.fallbackEyeHeight = options?.fallbackEyeHeight ?? 1.6;
2116
+ }
2117
+ };
2118
+ __name(_XRRigComponent, "XRRigComponent");
2119
+ var XRRigComponent = _XRRigComponent;
2120
+
2078
2121
  // core/utils/dracoDecoderPath.ts
2079
2122
  var LOCAL_DRACO_DECODER_RELATIVE_PATH = "./assets/draco/";
2080
2123
  var DRACO_DECODER_CDN_PATH = "https://www.gstatic.com/draco/versioned/decoders/1.5.7/";
@@ -2581,6 +2624,7 @@ var _CameraSyncSystem = class _CameraSyncSystem {
2581
2624
  update(world) {
2582
2625
  const entityIds = world.query(CameraComponent, Object3DRef);
2583
2626
  const windowAspect = readWindowAspect();
2627
+ const xrCamera = world.isXrPresenting() ? world.getCamera() : null;
2584
2628
  const alive = new Set(entityIds);
2585
2629
  for (const trackedId of this.signatures.keys()) {
2586
2630
  if (!alive.has(trackedId)) {
@@ -2597,6 +2641,9 @@ var _CameraSyncSystem = class _CameraSyncSystem {
2597
2641
  if (!(camera instanceof THREE14__namespace.Camera)) {
2598
2642
  continue;
2599
2643
  }
2644
+ if (xrCamera !== null && camera === xrCamera) {
2645
+ continue;
2646
+ }
2600
2647
  const nextSignature = buildSignature(component, camera, windowAspect);
2601
2648
  if (this.signatures.get(entityId) === nextSignature) {
2602
2649
  continue;
@@ -5568,7 +5615,9 @@ var HYDRATABLE_ENTITY_KINDS = /* @__PURE__ */ new Set([
5568
5615
  "areaLight",
5569
5616
  "instancedMesh",
5570
5617
  "instanceSlot",
5571
- "scriptObject"
5618
+ "scriptObject",
5619
+ "xrRig",
5620
+ "teleportArea"
5572
5621
  ]);
5573
5622
 
5574
5623
  // core/hydration/hydrateWorld.ts
@@ -6272,6 +6321,49 @@ var _Behaviour = class _Behaviour {
6272
6321
  */
6273
6322
  onTargetLost(target) {
6274
6323
  }
6324
+ // ─── WebXR (VR / MR) hooks ────────────────────────────────────────────────
6325
+ // Dispatched by XRSessionSystem / XRInteractionSystem, following the same
6326
+ // pattern as the AR hooks above: public no-op methods that subclasses opt
6327
+ // into by overriding. They never fire outside an immersive session.
6328
+ /**
6329
+ * Called once when an immersive session starts. Optional XR hook.
6330
+ *
6331
+ * Use it to set up XR-only state. Inspect `capabilities` rather than guessing
6332
+ * by device: feature support differs sharply per headset (for example Vision
6333
+ * Pro grants no passthrough on the web today).
6334
+ *
6335
+ * @param capabilities - What the session actually granted, read from
6336
+ * `XRSession.enabledFeatures`.
6337
+ */
6338
+ onXRSessionStart(capabilities) {
6339
+ }
6340
+ /** Called once when the immersive session ends. Optional XR hook. */
6341
+ onXRSessionEnd() {
6342
+ }
6343
+ /**
6344
+ * Called when a primary action begins — controller trigger, hand pinch, or
6345
+ * Vision Pro gaze-pinch. Optional XR hook.
6346
+ *
6347
+ * @param input - Normalized state for the source that acted.
6348
+ */
6349
+ onSelectStart(input) {
6350
+ }
6351
+ /** Called when the primary action ends. Optional XR hook. */
6352
+ onSelectEnd(input) {
6353
+ }
6354
+ /**
6355
+ * Called when this entity is grabbed. Optional XR hook.
6356
+ *
6357
+ * Only fires for entities carrying an enabled, grabbable
6358
+ * `XRInteractableComponent`.
6359
+ *
6360
+ * @param input - The source doing the grabbing.
6361
+ */
6362
+ onGrab(input) {
6363
+ }
6364
+ /** Called when this entity is released. Optional XR hook. */
6365
+ onRelease(input) {
6366
+ }
6275
6367
  };
6276
6368
  __name(_Behaviour, "Behaviour");
6277
6369
  /**
@@ -7119,12 +7211,16 @@ var ScriptObjectSystem = _ScriptObjectSystem;
7119
7211
  var _TransformSyncSystem = class _TransformSyncSystem {
7120
7212
  update(world) {
7121
7213
  const entities = world.query(TransformComponent, Object3DRef);
7214
+ const xrCamera = world.isXrPresenting() ? world.getCamera() : null;
7122
7215
  for (const entityId of entities) {
7123
7216
  const transform = world.getComponent(entityId, TransformComponent);
7124
7217
  const object = world.getComponent(entityId, Object3DRef)?.object;
7125
7218
  if (!transform || !object) {
7126
7219
  continue;
7127
7220
  }
7221
+ if (xrCamera !== null && object === xrCamera) {
7222
+ continue;
7223
+ }
7128
7224
  applyTransform(object, transform);
7129
7225
  object.updateMatrixWorld(true);
7130
7226
  }
@@ -7375,6 +7471,702 @@ var _VideoSystem = class _VideoSystem {
7375
7471
  };
7376
7472
  __name(_VideoSystem, "VideoSystem");
7377
7473
  var VideoSystem = _VideoSystem;
7474
+ function handednessOf(source) {
7475
+ const value = source.handedness;
7476
+ return value === "left" || value === "right" ? value : "none";
7477
+ }
7478
+ __name(handednessOf, "handednessOf");
7479
+ function kindOf(source) {
7480
+ const mode = source.targetRayMode;
7481
+ if (mode === "transient-pointer") return "transient-pointer";
7482
+ if (mode === "gaze") return "gaze";
7483
+ if (source.hand) return "hand";
7484
+ return "controller";
7485
+ }
7486
+ __name(kindOf, "kindOf");
7487
+ function blankState(source) {
7488
+ return {
7489
+ handedness: handednessOf(source),
7490
+ kind: kindOf(source),
7491
+ connected: true,
7492
+ rayOrigin: new THREE14__namespace.Vector3(),
7493
+ rayDirection: new THREE14__namespace.Vector3(0, 0, -1),
7494
+ gripPosition: null,
7495
+ gripQuaternion: null,
7496
+ select: false,
7497
+ selectStart: false,
7498
+ selectEnd: false,
7499
+ squeeze: false,
7500
+ squeezeStart: false,
7501
+ squeezeEnd: false,
7502
+ axes: [],
7503
+ hasAxes: false,
7504
+ buttons: []
7505
+ };
7506
+ }
7507
+ __name(blankState, "blankState");
7508
+ var _XRInputSystem = class _XRInputSystem {
7509
+ constructor() {
7510
+ this.records = /* @__PURE__ */ new Map();
7511
+ this.session = null;
7512
+ this.onSelectStart = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSelectStart"), "onSelectStart");
7513
+ this.onSelectEnd = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSelectEnd"), "onSelectEnd");
7514
+ this.onSqueezeStart = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSqueezeStart"), "onSqueezeStart");
7515
+ this.onSqueezeEnd = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSqueezeEnd"), "onSqueezeEnd");
7516
+ }
7517
+ mark(event, flag) {
7518
+ const record = this.ensureRecord(event.inputSource);
7519
+ record[flag] = true;
7520
+ }
7521
+ ensureRecord(source) {
7522
+ let record = this.records.get(source);
7523
+ if (!record) {
7524
+ record = {
7525
+ source,
7526
+ state: blankState(source),
7527
+ pendingSelectStart: false,
7528
+ pendingSelectEnd: false,
7529
+ pendingSqueezeStart: false,
7530
+ pendingSqueezeEnd: false,
7531
+ removed: false
7532
+ };
7533
+ this.records.set(source, record);
7534
+ }
7535
+ return record;
7536
+ }
7537
+ /** Bind to a session. Called by {@link XRSessionSystem} on session start. */
7538
+ attachSession(session) {
7539
+ this.detachSession();
7540
+ this.session = session;
7541
+ session.addEventListener("selectstart", this.onSelectStart);
7542
+ session.addEventListener("selectend", this.onSelectEnd);
7543
+ session.addEventListener(
7544
+ "squeezestart",
7545
+ this.onSqueezeStart
7546
+ );
7547
+ session.addEventListener("squeezeend", this.onSqueezeEnd);
7548
+ }
7549
+ /** Unbind and drop all per-source state. */
7550
+ detachSession() {
7551
+ const session = this.session;
7552
+ if (session) {
7553
+ session.removeEventListener(
7554
+ "selectstart",
7555
+ this.onSelectStart
7556
+ );
7557
+ session.removeEventListener(
7558
+ "selectend",
7559
+ this.onSelectEnd
7560
+ );
7561
+ session.removeEventListener(
7562
+ "squeezestart",
7563
+ this.onSqueezeStart
7564
+ );
7565
+ session.removeEventListener(
7566
+ "squeezeend",
7567
+ this.onSqueezeEnd
7568
+ );
7569
+ }
7570
+ this.session = null;
7571
+ this.records.clear();
7572
+ }
7573
+ update(world) {
7574
+ for (const [source, record] of this.records) {
7575
+ if (record.removed) {
7576
+ this.records.delete(source);
7577
+ continue;
7578
+ }
7579
+ record.state.selectStart = false;
7580
+ record.state.selectEnd = false;
7581
+ record.state.squeezeStart = false;
7582
+ record.state.squeezeEnd = false;
7583
+ }
7584
+ const session = this.session;
7585
+ if (!session || !world.isXrPresenting()) return;
7586
+ const renderer = world.getRenderer();
7587
+ const referenceSpace = renderer.xr.getReferenceSpace();
7588
+ const frame = renderer.xr.getFrame?.();
7589
+ const live = /* @__PURE__ */ new Set();
7590
+ for (const source of session.inputSources) {
7591
+ live.add(source);
7592
+ const record = this.ensureRecord(source);
7593
+ const state = record.state;
7594
+ state.connected = true;
7595
+ state.handedness = handednessOf(source);
7596
+ state.kind = kindOf(source);
7597
+ if (frame && referenceSpace && source.targetRaySpace) {
7598
+ const pose = frame.getPose(source.targetRaySpace, referenceSpace);
7599
+ if (pose) {
7600
+ const m = new THREE14__namespace.Matrix4().fromArray(pose.transform.matrix);
7601
+ state.rayOrigin.setFromMatrixPosition(m);
7602
+ state.rayDirection.set(0, 0, -1).applyQuaternion(
7603
+ new THREE14__namespace.Quaternion().setFromRotationMatrix(m)
7604
+ ).normalize();
7605
+ }
7606
+ }
7607
+ if (frame && referenceSpace && source.gripSpace) {
7608
+ const gripPose = frame.getPose(source.gripSpace, referenceSpace);
7609
+ if (gripPose) {
7610
+ const m = new THREE14__namespace.Matrix4().fromArray(gripPose.transform.matrix);
7611
+ state.gripPosition ?? (state.gripPosition = new THREE14__namespace.Vector3());
7612
+ state.gripQuaternion ?? (state.gripQuaternion = new THREE14__namespace.Quaternion());
7613
+ state.gripPosition.setFromMatrixPosition(m);
7614
+ state.gripQuaternion.setFromRotationMatrix(m);
7615
+ }
7616
+ } else {
7617
+ state.gripPosition = null;
7618
+ state.gripQuaternion = null;
7619
+ }
7620
+ const gamepad = source.gamepad;
7621
+ if (gamepad) {
7622
+ state.axes = Array.from(gamepad.axes);
7623
+ state.hasAxes = gamepad.axes.some((axis) => Number.isFinite(axis));
7624
+ state.buttons = gamepad.buttons.map((button) => button.pressed);
7625
+ } else {
7626
+ state.axes = [];
7627
+ state.hasAxes = false;
7628
+ state.buttons = [];
7629
+ }
7630
+ if (record.pendingSelectStart) {
7631
+ state.select = true;
7632
+ state.selectStart = true;
7633
+ record.pendingSelectStart = false;
7634
+ }
7635
+ if (record.pendingSelectEnd) {
7636
+ state.select = false;
7637
+ state.selectEnd = true;
7638
+ record.pendingSelectEnd = false;
7639
+ }
7640
+ if (record.pendingSqueezeStart) {
7641
+ state.squeeze = true;
7642
+ state.squeezeStart = true;
7643
+ record.pendingSqueezeStart = false;
7644
+ }
7645
+ if (record.pendingSqueezeEnd) {
7646
+ state.squeeze = false;
7647
+ state.squeezeEnd = true;
7648
+ record.pendingSqueezeEnd = false;
7649
+ }
7650
+ }
7651
+ for (const [source, record] of this.records) {
7652
+ if (live.has(source)) continue;
7653
+ record.state.connected = false;
7654
+ record.removed = true;
7655
+ }
7656
+ }
7657
+ /** Every input source active this frame. */
7658
+ getInputs() {
7659
+ return Array.from(this.records.values(), (record) => record.state);
7660
+ }
7661
+ /** The first source for a hand, or null. Prefer this for hand-specific logic. */
7662
+ getInput(handedness) {
7663
+ for (const record of this.records.values()) {
7664
+ if (record.state.handedness === handedness) return record.state;
7665
+ }
7666
+ return null;
7667
+ }
7668
+ /** True while ANY source is selecting — the portable "is the user acting" check. */
7669
+ isSelecting() {
7670
+ for (const record of this.records.values()) {
7671
+ if (record.state.select) return true;
7672
+ }
7673
+ return false;
7674
+ }
7675
+ dispose() {
7676
+ this.detachSession();
7677
+ }
7678
+ };
7679
+ __name(_XRInputSystem, "XRInputSystem");
7680
+ var XRInputSystem = _XRInputSystem;
7681
+ var RAYLESS_HOLD_DISTANCE = 0.5;
7682
+ var _XRInteractionSystem = class _XRInteractionSystem {
7683
+ constructor(inputSystem) {
7684
+ this.raycaster = new THREE14__namespace.Raycaster();
7685
+ /** Which entity each source is holding. Keyed by source state identity. */
7686
+ this.grabs = /* @__PURE__ */ new Map();
7687
+ this.hoveredEntityId = null;
7688
+ this.inputSystem = inputSystem;
7689
+ }
7690
+ update(world) {
7691
+ if (!world.isXrPresenting()) {
7692
+ this.releaseAll(world);
7693
+ return;
7694
+ }
7695
+ const inputs = this.inputSystem.getInputs();
7696
+ const interactables = this.collectInteractables(world);
7697
+ this.hoveredEntityId = null;
7698
+ for (const input of inputs) {
7699
+ if (!input.connected) {
7700
+ this.release(world, input);
7701
+ continue;
7702
+ }
7703
+ const held = this.grabs.get(input);
7704
+ if (held) {
7705
+ this.moveHeld(world, input, held);
7706
+ if (input.selectEnd) this.release(world, input);
7707
+ continue;
7708
+ }
7709
+ const hitEntityId = this.raycast(input, interactables);
7710
+ if (hitEntityId === null) continue;
7711
+ if (this.hoveredEntityId === null) {
7712
+ this.hoveredEntityId = hitEntityId;
7713
+ }
7714
+ const interactable = world.getComponent(
7715
+ hitEntityId,
7716
+ XRInteractableComponent
7717
+ );
7718
+ if (input.selectStart && interactable?.grabbable) {
7719
+ this.grab(world, input, hitEntityId);
7720
+ }
7721
+ }
7722
+ }
7723
+ collectInteractables(world) {
7724
+ const out = [];
7725
+ for (const entityId of world.query(XRInteractableComponent, Object3DRef)) {
7726
+ const interactable = world.getComponent(entityId, XRInteractableComponent);
7727
+ if (!interactable?.enabled || !interactable.pointable) continue;
7728
+ if (!world.isEntityVisible(entityId)) continue;
7729
+ const object = world.getComponent(entityId, Object3DRef)?.object;
7730
+ if (object) out.push({ entityId, object });
7731
+ }
7732
+ return out;
7733
+ }
7734
+ /** Entity id of the nearest pointable hit along this source's ray, or null. */
7735
+ raycast(input, interactables) {
7736
+ if (interactables.length === 0) return null;
7737
+ this.raycaster.set(input.rayOrigin, input.rayDirection);
7738
+ let nearestId = null;
7739
+ let nearestDistance = Number.POSITIVE_INFINITY;
7740
+ for (const entry of interactables) {
7741
+ const hit = this.raycaster.intersectObject(entry.object, true)[0];
7742
+ if (!hit) continue;
7743
+ if (hit.distance < nearestDistance) {
7744
+ nearestDistance = hit.distance;
7745
+ nearestId = entry.entityId;
7746
+ }
7747
+ }
7748
+ return nearestId;
7749
+ }
7750
+ grab(world, input, entityId) {
7751
+ const transform = world.getComponent(entityId, TransformComponent);
7752
+ if (!transform) return;
7753
+ const hand = this.handPosition(input);
7754
+ const offset = new THREE14__namespace.Vector3(
7755
+ transform.position.x - hand.x,
7756
+ transform.position.y - hand.y,
7757
+ transform.position.z - hand.z
7758
+ );
7759
+ const physics = world.getComponent(entityId, PhysicsComponent);
7760
+ let restoreBodyType = null;
7761
+ const interactable = world.getComponent(entityId, XRInteractableComponent);
7762
+ if (physics && interactable?.grabMode === "kinematic") {
7763
+ restoreBodyType = physics.values.rigidbody.type;
7764
+ physics.values.rigidbody.type = "kinematicPosition";
7765
+ }
7766
+ this.grabs.set(input, { entityId, offset, restoreBodyType });
7767
+ this.dispatch(world, entityId, (b) => b.onGrab(input));
7768
+ }
7769
+ moveHeld(world, input, held) {
7770
+ const transform = world.getComponent(held.entityId, TransformComponent);
7771
+ if (!transform) return;
7772
+ const hand = this.handPosition(input);
7773
+ transform.position.x = hand.x + held.offset.x;
7774
+ transform.position.y = hand.y + held.offset.y;
7775
+ transform.position.z = hand.z + held.offset.z;
7776
+ }
7777
+ release(world, input) {
7778
+ const held = this.grabs.get(input);
7779
+ if (!held) return;
7780
+ this.grabs.delete(input);
7781
+ if (held.restoreBodyType !== null) {
7782
+ const physics = world.getComponent(held.entityId, PhysicsComponent);
7783
+ if (physics) {
7784
+ physics.values.rigidbody.type = held.restoreBodyType;
7785
+ }
7786
+ }
7787
+ this.dispatch(world, held.entityId, (b) => b.onRelease(input));
7788
+ }
7789
+ releaseAll(world) {
7790
+ for (const input of Array.from(this.grabs.keys())) {
7791
+ this.release(world, input);
7792
+ }
7793
+ }
7794
+ /**
7795
+ * Where the "hand" is for this source. Controllers and tracked hands report a
7796
+ * grip pose; gaze and transient-pointer sources have none, so we hold the
7797
+ * object a fixed distance along the ray instead.
7798
+ */
7799
+ handPosition(input) {
7800
+ if (input.gripPosition) return input.gripPosition;
7801
+ return new THREE14__namespace.Vector3().copy(input.rayDirection).multiplyScalar(RAYLESS_HOLD_DISTANCE).add(input.rayOrigin);
7802
+ }
7803
+ dispatch(world, entityId, visit) {
7804
+ const script = world.getComponent(entityId, ScriptComponent);
7805
+ const instances = script?.scriptInstances;
7806
+ if (!instances) return;
7807
+ for (const instance of instances.values()) {
7808
+ if (!(instance instanceof Behaviour)) continue;
7809
+ try {
7810
+ visit(instance);
7811
+ } catch (error) {
7812
+ console.error("[XRInteractionSystem] Behaviour hook failed:", error);
7813
+ }
7814
+ }
7815
+ }
7816
+ /** Entity currently under a pointer this frame, or null. */
7817
+ getHoveredEntityId() {
7818
+ return this.hoveredEntityId;
7819
+ }
7820
+ /** True when any source is holding something. */
7821
+ isGrabbing() {
7822
+ return this.grabs.size > 0;
7823
+ }
7824
+ };
7825
+ __name(_XRInteractionSystem, "XRInteractionSystem");
7826
+ var XRInteractionSystem = _XRInteractionSystem;
7827
+ var UP = new THREE14__namespace.Vector3(0, 1, 0);
7828
+ var AXIS_DEADZONE = 0.5;
7829
+ var _XRLocomotionSystem = class _XRLocomotionSystem {
7830
+ constructor(inputSystem) {
7831
+ this.raycaster = new THREE14__namespace.Raycaster();
7832
+ /** Per-source latch so one stick push yields exactly one snap turn. */
7833
+ this.turnLatched = /* @__PURE__ */ new WeakSet();
7834
+ /** Where the pointer currently lands, for the consumer to draw a marker. */
7835
+ this.teleportPoint = null;
7836
+ this.inputSystem = inputSystem;
7837
+ }
7838
+ update(world, deltaTime) {
7839
+ this.teleportPoint = null;
7840
+ if (!world.isXrPresenting()) return;
7841
+ const rigEntityId = world.query(XRRigComponent, TransformComponent)[0];
7842
+ if (typeof rigEntityId !== "number") return;
7843
+ const rig = world.getComponent(rigEntityId, XRRigComponent);
7844
+ const rigTransform = world.getComponent(rigEntityId, TransformComponent);
7845
+ if (!rig || !rigTransform) return;
7846
+ const inputs = this.inputSystem.getInputs();
7847
+ if (inputs.length === 0) return;
7848
+ if (rig.snapTurn) {
7849
+ this.applySnapTurn(rig, rigTransform, inputs);
7850
+ }
7851
+ if (rig.locomotion === "teleport") {
7852
+ this.applyTeleport(world, rigTransform, inputs);
7853
+ } else if (rig.locomotion === "smooth") {
7854
+ this.applySmoothMove(world, rig, rigTransform, inputs, deltaTime);
7855
+ }
7856
+ }
7857
+ /**
7858
+ * Snap turning, not smooth turning — rotating the view continuously is the
7859
+ * single biggest cause of VR nausea, so the comfort-safe option is the one
7860
+ * wired to the stick.
7861
+ */
7862
+ applySnapTurn(rig, transform, inputs) {
7863
+ for (const input of inputs) {
7864
+ if (!input.hasAxes || input.axes.length < 3) continue;
7865
+ const x = input.axes[2] ?? 0;
7866
+ const source = input;
7867
+ if (Math.abs(x) < AXIS_DEADZONE) {
7868
+ this.turnLatched.delete(source);
7869
+ continue;
7870
+ }
7871
+ if (this.turnLatched.has(source)) continue;
7872
+ this.turnLatched.add(source);
7873
+ const step = THREE14__namespace.MathUtils.degToRad(rig.snapTurnDegrees);
7874
+ transform.rotation.y -= Math.sign(x) * step;
7875
+ }
7876
+ }
7877
+ applySmoothMove(world, rig, transform, inputs, deltaTime) {
7878
+ for (const input of inputs) {
7879
+ if (!input.hasAxes || input.axes.length < 2) continue;
7880
+ const x = input.axes[0] ?? 0;
7881
+ const y = input.axes[1] ?? 0;
7882
+ if (Math.abs(x) < 0.15 && Math.abs(y) < 0.15) continue;
7883
+ const camera = world.getCamera();
7884
+ const forward = new THREE14__namespace.Vector3();
7885
+ camera.getWorldDirection(forward);
7886
+ forward.y = 0;
7887
+ if (forward.lengthSq() < 1e-6) continue;
7888
+ forward.normalize();
7889
+ const right = new THREE14__namespace.Vector3().crossVectors(forward, UP).normalize();
7890
+ const step = rig.moveSpeed * Math.max(0, deltaTime);
7891
+ transform.position.x += (forward.x * -y + right.x * x) * step;
7892
+ transform.position.z += (forward.z * -y + right.z * x) * step;
7893
+ }
7894
+ }
7895
+ applyTeleport(world, transform, inputs) {
7896
+ const targets = this.collectTeleportTargets(world);
7897
+ if (targets.length === 0) return;
7898
+ for (const input of inputs) {
7899
+ this.raycaster.set(input.rayOrigin, input.rayDirection);
7900
+ const hit = this.raycaster.intersectObjects(targets, true)[0];
7901
+ if (!hit) continue;
7902
+ this.teleportPoint = hit.point.clone();
7903
+ if (input.selectEnd) {
7904
+ transform.position.x = hit.point.x;
7905
+ transform.position.y = hit.point.y;
7906
+ transform.position.z = hit.point.z;
7907
+ }
7908
+ }
7909
+ }
7910
+ collectTeleportTargets(world) {
7911
+ const objects = [];
7912
+ for (const entityId of world.query(VRTeleportAreaComponent, Object3DRef)) {
7913
+ const area = world.getComponent(entityId, VRTeleportAreaComponent);
7914
+ if (!area?.enabled) continue;
7915
+ if (!world.isEntityVisible(entityId)) continue;
7916
+ const object = world.getComponent(entityId, Object3DRef)?.object;
7917
+ if (object) objects.push(object);
7918
+ }
7919
+ return objects;
7920
+ }
7921
+ /**
7922
+ * Where the teleport pointer currently lands this frame, or null.
7923
+ * Consumers use this to render an aim marker.
7924
+ */
7925
+ getTeleportPoint() {
7926
+ return this.teleportPoint;
7927
+ }
7928
+ };
7929
+ __name(_XRLocomotionSystem, "XRLocomotionSystem");
7930
+ var XRLocomotionSystem = _XRLocomotionSystem;
7931
+
7932
+ // core/xr/types.ts
7933
+ var NO_XR_CAPABILITIES = {
7934
+ mode: "inline",
7935
+ handTracking: false,
7936
+ planeDetection: false,
7937
+ meshDetection: false,
7938
+ depthSensing: false,
7939
+ anchors: false,
7940
+ hitTest: false,
7941
+ enabledFeatures: []
7942
+ };
7943
+ function readXrCapabilities(session, mode = "immersive-vr") {
7944
+ if (!session) return NO_XR_CAPABILITIES;
7945
+ const raw = session.enabledFeatures;
7946
+ const features = Array.isArray(raw) ? Array.from(raw) : [];
7947
+ const has = /* @__PURE__ */ __name((name) => features.includes(name), "has");
7948
+ return {
7949
+ mode,
7950
+ handTracking: has("hand-tracking"),
7951
+ planeDetection: has("plane-detection"),
7952
+ meshDetection: has("mesh-detection"),
7953
+ depthSensing: has("depth-sensing"),
7954
+ anchors: has("anchors"),
7955
+ hitTest: has("hit-test"),
7956
+ enabledFeatures: features
7957
+ };
7958
+ }
7959
+ __name(readXrCapabilities, "readXrCapabilities");
7960
+
7961
+ // core/systems/XRSessionSystem.ts
7962
+ var _XRSessionSystem = class _XRSessionSystem {
7963
+ constructor(inputSystem) {
7964
+ this.presenting = false;
7965
+ this.capabilities = NO_XR_CAPABILITIES;
7966
+ this.inputSystem = inputSystem ?? null;
7967
+ }
7968
+ update(world) {
7969
+ const presenting = world.isXrPresenting();
7970
+ if (presenting !== this.presenting) {
7971
+ this.presenting = presenting;
7972
+ if (presenting) {
7973
+ this.handleSessionStart(world);
7974
+ } else {
7975
+ this.handleSessionEnd(world);
7976
+ }
7977
+ }
7978
+ if (presenting) {
7979
+ this.dispatchInputEdges(world);
7980
+ }
7981
+ }
7982
+ handleSessionStart(world) {
7983
+ const session = world.getRenderer().xr.getSession();
7984
+ const mode = session && session.environmentBlendMode !== "opaque" ? "immersive-ar" : "immersive-vr";
7985
+ this.capabilities = readXrCapabilities(session ?? null, mode);
7986
+ if (session && this.inputSystem) {
7987
+ this.inputSystem.attachSession(session);
7988
+ }
7989
+ this.forEachBehaviour(world, (behaviour) => {
7990
+ behaviour.onXRSessionStart(this.capabilities);
7991
+ });
7992
+ }
7993
+ handleSessionEnd(world) {
7994
+ this.capabilities = NO_XR_CAPABILITIES;
7995
+ this.inputSystem?.detachSession();
7996
+ this.forEachBehaviour(world, (behaviour) => {
7997
+ behaviour.onXRSessionEnd();
7998
+ });
7999
+ }
8000
+ /**
8001
+ * Relay this frame's one-frame select edges. Polling the latched state (rather
8002
+ * than dispatching from the DOM event) keeps hook delivery on the ECS tick, so
8003
+ * a Behaviour's `onSelectStart` always observes the same world state its
8004
+ * `onUpdate` would.
8005
+ */
8006
+ dispatchInputEdges(world) {
8007
+ const inputs = this.inputSystem?.getInputs();
8008
+ if (!inputs || inputs.length === 0) return;
8009
+ for (const input of inputs) {
8010
+ if (input.selectStart) {
8011
+ this.forEachBehaviour(world, (b) => b.onSelectStart(input));
8012
+ }
8013
+ if (input.selectEnd) {
8014
+ this.forEachBehaviour(world, (b) => b.onSelectEnd(input));
8015
+ }
8016
+ }
8017
+ }
8018
+ forEachBehaviour(world, visit) {
8019
+ for (const entityId of world.query(ScriptComponent)) {
8020
+ const script = world.getComponent(entityId, ScriptComponent);
8021
+ const instances = script?.scriptInstances;
8022
+ if (!instances) continue;
8023
+ for (const instance of instances.values()) {
8024
+ if (!(instance instanceof Behaviour)) continue;
8025
+ try {
8026
+ visit(instance);
8027
+ } catch (error) {
8028
+ console.error("[XRSessionSystem] Behaviour hook failed:", error);
8029
+ }
8030
+ }
8031
+ }
8032
+ }
8033
+ /** What the active session granted. `NO_XR_CAPABILITIES` when not presenting. */
8034
+ getCapabilities() {
8035
+ return this.capabilities;
8036
+ }
8037
+ /** True while an immersive session is presenting. */
8038
+ isPresenting() {
8039
+ return this.presenting;
8040
+ }
8041
+ /** Convenience passthrough so consumers need only hold this system. */
8042
+ getInputs() {
8043
+ return this.inputSystem?.getInputs() ?? [];
8044
+ }
8045
+ };
8046
+ __name(_XRSessionSystem, "XRSessionSystem");
8047
+ var XRSessionSystem = _XRSessionSystem;
8048
+ function buildGeometry2(area) {
8049
+ if (area.shape === "cylinder") {
8050
+ const radius = Math.max(5e-3, area.width / 2);
8051
+ return new THREE14__namespace.CircleGeometry(radius, 48);
8052
+ }
8053
+ return new THREE14__namespace.PlaneGeometry(
8054
+ Math.max(0.01, area.width),
8055
+ Math.max(0.01, area.depth)
8056
+ );
8057
+ }
8058
+ __name(buildGeometry2, "buildGeometry");
8059
+ function signatureOf(area) {
8060
+ return `${area.shape}:${area.width}:${area.depth}`;
8061
+ }
8062
+ __name(signatureOf, "signatureOf");
8063
+ var _VRTeleportAreaSystem = class _VRTeleportAreaSystem {
8064
+ constructor() {
8065
+ this.signatures = /* @__PURE__ */ new Map();
8066
+ }
8067
+ update(world) {
8068
+ const entityIds = world.query(VRTeleportAreaComponent, Object3DRef);
8069
+ const alive = new Set(entityIds);
8070
+ for (const tracked of this.signatures.keys()) {
8071
+ if (!alive.has(tracked)) this.signatures.delete(tracked);
8072
+ }
8073
+ for (const entityId of entityIds) {
8074
+ const area = world.getComponent(entityId, VRTeleportAreaComponent);
8075
+ const object = world.getComponent(entityId, Object3DRef)?.object;
8076
+ if (!area || !(object instanceof THREE14__namespace.Mesh)) continue;
8077
+ const signature = signatureOf(area);
8078
+ if (this.signatures.get(entityId) === signature) continue;
8079
+ this.signatures.set(entityId, signature);
8080
+ object.geometry?.dispose?.();
8081
+ object.geometry = buildGeometry2(area);
8082
+ }
8083
+ }
8084
+ };
8085
+ __name(_VRTeleportAreaSystem, "VRTeleportAreaSystem");
8086
+ var VRTeleportAreaSystem = _VRTeleportAreaSystem;
8087
+ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
8088
+ constructor(slots = 2) {
8089
+ this.controllerModelFactory = new XRControllerModelFactory_js.XRControllerModelFactory();
8090
+ this.handModelFactory = new XRHandModelFactory_js.XRHandModelFactory();
8091
+ this.group = new THREE14__namespace.Group();
8092
+ this.built = false;
8093
+ this.attachedTo = null;
8094
+ /** Index → whether we've already attached visuals for that slot. */
8095
+ this.ready = /* @__PURE__ */ new Set();
8096
+ this.slots = slots;
8097
+ this.group.name = "XRControllerVisuals";
8098
+ this.group.userData.ecsPreviewHidden = true;
8099
+ }
8100
+ update(world) {
8101
+ const renderer = world.getRenderer();
8102
+ if (!renderer.xr?.enabled) return;
8103
+ this.ensureAttached(world);
8104
+ if (this.built) return;
8105
+ for (let index = 0; index < this.slots; index += 1) {
8106
+ if (this.ready.has(index)) continue;
8107
+ const controller = renderer.xr.getController(index);
8108
+ const grip = renderer.xr.getControllerGrip(index);
8109
+ const hand = renderer.xr.getHand(index);
8110
+ if (!controller || !grip || !hand) continue;
8111
+ controller.add(this.createRay());
8112
+ this.group.add(controller);
8113
+ grip.add(this.controllerModelFactory.createControllerModel(grip));
8114
+ this.group.add(grip);
8115
+ hand.add(this.handModelFactory.createHandModel(hand, "mesh"));
8116
+ this.group.add(hand);
8117
+ this.ready.add(index);
8118
+ }
8119
+ if (this.ready.size >= this.slots) {
8120
+ this.built = true;
8121
+ }
8122
+ }
8123
+ /**
8124
+ * Parent the visuals to the XR rig when there is one, so locomotion moves the
8125
+ * hands with the player. Falls back to the scene root.
8126
+ */
8127
+ ensureAttached(world) {
8128
+ const camera = world.getCamera();
8129
+ const target = camera.parent ?? world.getScene();
8130
+ if (this.attachedTo === target) return;
8131
+ target.add(this.group);
8132
+ this.attachedTo = target;
8133
+ }
8134
+ createRay() {
8135
+ const geometry = new THREE14__namespace.BufferGeometry().setFromPoints([
8136
+ new THREE14__namespace.Vector3(0, 0, 0),
8137
+ new THREE14__namespace.Vector3(0, 0, -5)
8138
+ ]);
8139
+ const material = new THREE14__namespace.LineBasicMaterial({
8140
+ color: "#4da3ff",
8141
+ transparent: true,
8142
+ opacity: 0.6,
8143
+ depthTest: false
8144
+ });
8145
+ const line = new THREE14__namespace.Line(geometry, material);
8146
+ line.name = "XRPointerRay";
8147
+ line.renderOrder = 999;
8148
+ return line;
8149
+ }
8150
+ dispose() {
8151
+ this.group.removeFromParent();
8152
+ this.group.traverse((child) => {
8153
+ const mesh = child;
8154
+ mesh.geometry?.dispose?.();
8155
+ const material = mesh.material;
8156
+ if (Array.isArray(material)) {
8157
+ for (const entry of material) entry.dispose?.();
8158
+ } else {
8159
+ material?.dispose?.();
8160
+ }
8161
+ });
8162
+ this.group.clear();
8163
+ this.ready.clear();
8164
+ this.built = false;
8165
+ this.attachedTo = null;
8166
+ }
8167
+ };
8168
+ __name(_XRControllerVisualSystem, "XRControllerVisualSystem");
8169
+ var XRControllerVisualSystem = _XRControllerVisualSystem;
7378
8170
 
7379
8171
  exports.AnimationSystem = AnimationSystem;
7380
8172
  exports.AudioSystem = AudioSystem;
@@ -7395,6 +8187,12 @@ exports.SCRIPT_OBJECT_ROOT_FLAG = SCRIPT_OBJECT_ROOT_FLAG;
7395
8187
  exports.ScriptBehaviourSystem = ScriptBehaviourSystem;
7396
8188
  exports.ScriptObjectSystem = ScriptObjectSystem;
7397
8189
  exports.TransformSyncSystem = TransformSyncSystem;
8190
+ exports.VRTeleportAreaSystem = VRTeleportAreaSystem;
7398
8191
  exports.VideoSystem = VideoSystem;
8192
+ exports.XRControllerVisualSystem = XRControllerVisualSystem;
8193
+ exports.XRInputSystem = XRInputSystem;
8194
+ exports.XRInteractionSystem = XRInteractionSystem;
8195
+ exports.XRLocomotionSystem = XRLocomotionSystem;
8196
+ exports.XRSessionSystem = XRSessionSystem;
7399
8197
  //# sourceMappingURL=index.cjs.map
7400
8198
  //# sourceMappingURL=index.cjs.map