@relu-interactives/spatial-ecs 0.3.5 → 0.3.7

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 (44) hide show
  1. package/dist/{System-vrU46Vuw.d.cts → System-TmC2EijF.d.cts} +2 -2
  2. package/dist/{System-vrU46Vuw.d.ts → System-TmC2EijF.d.ts} +2 -2
  3. package/dist/{XRRig-2686IqLQ.d.cts → XRInteractor-CCUzSqg9.d.ts} +96 -2
  4. package/dist/{XRRig-D2QURXl1.d.ts → XRInteractor-I2q6TKtF.d.cts} +96 -2
  5. package/dist/{chunk-4BZYK3JP.js → chunk-FQTOC7HK.js} +135 -88
  6. package/dist/chunk-FQTOC7HK.js.map +1 -0
  7. package/dist/{chunk-2LVFGVWI.js → chunk-JRJ3CL5H.js} +3 -3
  8. package/dist/{chunk-2LVFGVWI.js.map → chunk-JRJ3CL5H.js.map} +1 -1
  9. package/dist/{chunk-6QC2PJLD.js → chunk-N4GKZ7IV.js} +29 -3
  10. package/dist/chunk-N4GKZ7IV.js.map +1 -0
  11. package/dist/{chunk-G53MZVKQ.js → chunk-O6FJA4XW.js} +367 -53
  12. package/dist/chunk-O6FJA4XW.js.map +1 -0
  13. package/dist/components/index.cjs +26 -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 +185 -112
  19. package/dist/factories/index.cjs.map +1 -1
  20. package/dist/factories/index.d.cts +7 -3
  21. package/dist/factories/index.d.ts +7 -3
  22. package/dist/factories/index.js +3 -3
  23. package/dist/{index-BvCmmuJ0.d.cts → index-B9nwRDf9.d.cts} +65 -14
  24. package/dist/{index-BtKOaHfY.d.ts → index-BltjPEhg.d.cts} +10 -3
  25. package/dist/{index-Xu238jLn.d.cts → index-W8pUajCW.d.ts} +10 -3
  26. package/dist/{index-B-CViRMx.d.ts → index-_q9TVDI5.d.ts} +65 -14
  27. package/dist/index.cjs +684 -293
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +35 -9
  30. package/dist/index.d.ts +35 -9
  31. package/dist/index.js +8 -8
  32. package/dist/systems/index.cjs +386 -48
  33. package/dist/systems/index.cjs.map +1 -1
  34. package/dist/systems/index.d.cts +2 -2
  35. package/dist/systems/index.d.ts +2 -2
  36. package/dist/systems/index.js +3 -3
  37. package/dist/types.d.cts +1 -1
  38. package/dist/types.d.ts +1 -1
  39. package/dist/worldSnapshotUtils.d.cts +1 -1
  40. package/dist/worldSnapshotUtils.d.ts +1 -1
  41. package/package.json +1 -1
  42. package/dist/chunk-4BZYK3JP.js.map +0 -1
  43. package/dist/chunk-6QC2PJLD.js.map +0 -1
  44. package/dist/chunk-G53MZVKQ.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { resolveDracoDecoderPath, applyTransform, SHARED_MODEL_RESOURCE_FLAG } from './chunk-2LVFGVWI.js';
2
- import { AnimationComponent, ModelComponent, CameraComponent, Object3DRef, Environment, Postprocessing, TransformComponent, PhysicsComponent, setPhysicsLiveRef, LightComponent, MaterialComponent, DEFAULT_MESH_GEOMETRY_PARAMETERS, MeshGeometryComponent, InstancedMeshComponent, ParentId, ScriptComponent, ScriptObjectComponent, XRRigComponent, VRTeleportAreaComponent, XRInteractableComponent, InstanceSlotComponent, Name, ImageComponent, SpriteComponent, VideoComponent, AudioComponent, ImageTarget, Selectable, PrefabNode, PrefabInstance } from './chunk-6QC2PJLD.js';
1
+ import { resolveDracoDecoderPath, applyTransform, SHARED_MODEL_RESOURCE_FLAG } from './chunk-JRJ3CL5H.js';
2
+ import { AnimationComponent, ModelComponent, CameraComponent, Object3DRef, Environment, Postprocessing, TransformComponent, PhysicsComponent, setPhysicsLiveRef, LightComponent, MaterialComponent, DEFAULT_MESH_GEOMETRY_PARAMETERS, MeshGeometryComponent, InstancedMeshComponent, ParentId, ScriptComponent, ScriptObjectComponent, XRRigComponent, VRTeleportAreaComponent, XRInteractableComponent, XRInteractorComponent, XRInteractorSideComponent, InstanceSlotComponent, Name, ImageComponent, SpriteComponent, VideoComponent, AudioComponent, ImageTarget, Selectable, PrefabNode, PrefabInstance } from './chunk-N4GKZ7IV.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';
@@ -2791,6 +2791,14 @@ function buildComponentValueMaps(snapshot) {
2791
2791
  snapshot.components,
2792
2792
  "XRInteractableComponent"
2793
2793
  ),
2794
+ xrInteractorValues: readByComponent(
2795
+ snapshot.components,
2796
+ "XRInteractorComponent"
2797
+ ),
2798
+ xrInteractorSideValues: readByComponent(
2799
+ snapshot.components,
2800
+ "XRInteractorSideComponent"
2801
+ ),
2794
2802
  instancedMeshValues: readByComponent(snapshot.components, "InstancedMeshComponent"),
2795
2803
  instanceSlotValues: readByComponent(snapshot.components, "InstanceSlotComponent"),
2796
2804
  prefabNodeValues: readByComponent(snapshot.components, "PrefabNode"),
@@ -2977,6 +2985,60 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
2977
2985
  }
2978
2986
  }
2979
2987
  }
2988
+ {
2989
+ const sourceInteractor = asRecord(
2990
+ values.xrInteractorValues.get(sourceEntityId)
2991
+ );
2992
+ const interactor = world.getComponent(targetEntityId, XRInteractorComponent);
2993
+ if (sourceInteractor && interactor) {
2994
+ if (typeof sourceInteractor.interactorType === "string") {
2995
+ interactor.interactorType = sourceInteractor.interactorType;
2996
+ }
2997
+ if (typeof sourceInteractor.handPreset === "string") {
2998
+ interactor.handPreset = sourceInteractor.handPreset;
2999
+ }
3000
+ if (typeof sourceInteractor.handColor === "string") {
3001
+ interactor.handColor = sourceInteractor.handColor;
3002
+ }
3003
+ if (typeof sourceInteractor.handOpacity === "number") {
3004
+ interactor.handOpacity = sourceInteractor.handOpacity;
3005
+ }
3006
+ if (typeof sourceInteractor.handEmissiveIntensity === "number") {
3007
+ interactor.handEmissiveIntensity = sourceInteractor.handEmissiveIntensity;
3008
+ }
3009
+ if (typeof sourceInteractor.showRay === "boolean") {
3010
+ interactor.showRay = sourceInteractor.showRay;
3011
+ }
3012
+ }
3013
+ }
3014
+ {
3015
+ const sourceSide = asRecord(
3016
+ values.xrInteractorSideValues.get(sourceEntityId)
3017
+ );
3018
+ if (sourceSide && typeof sourceSide.side === "string") {
3019
+ const existing = world.getComponent(
3020
+ targetEntityId,
3021
+ XRInteractorSideComponent
3022
+ );
3023
+ if (existing) {
3024
+ existing.side = sourceSide.side;
3025
+ } else {
3026
+ world.addComponent(
3027
+ targetEntityId,
3028
+ new XRInteractorSideComponent({
3029
+ side: sourceSide.side
3030
+ })
3031
+ );
3032
+ }
3033
+ }
3034
+ }
3035
+ {
3036
+ const sourceRigOrigin = asRecord(values.xrRigValues.get(sourceEntityId));
3037
+ const rigForOrigin = world.getComponent(targetEntityId, XRRigComponent);
3038
+ if (sourceRigOrigin && rigForOrigin && typeof sourceRigOrigin.trackingOrigin === "string") {
3039
+ rigForOrigin.trackingOrigin = sourceRigOrigin.trackingOrigin;
3040
+ }
3041
+ }
2980
3042
  {
2981
3043
  const imComponent = world.getComponent(targetEntityId, InstancedMeshComponent);
2982
3044
  if (imComponent && imComponent.instanceEntityIds.length > 0) {
@@ -3566,6 +3628,7 @@ var HYDRATABLE_ENTITY_KINDS = /* @__PURE__ */ new Set([
3566
3628
  "instanceSlot",
3567
3629
  "scriptObject",
3568
3630
  "xrRig",
3631
+ "xrInteractor",
3569
3632
  "teleportArea"
3570
3633
  ]);
3571
3634
 
@@ -6119,6 +6182,9 @@ var _XRInputSystem = class _XRInputSystem {
6119
6182
  state.hasAxes = false;
6120
6183
  state.buttons = [];
6121
6184
  }
6185
+ }
6186
+ for (const record of this.records.values()) {
6187
+ const state = record.state;
6122
6188
  if (record.pendingSelectStart) {
6123
6189
  state.select = true;
6124
6190
  state.selectStart = true;
@@ -6367,6 +6433,10 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
6367
6433
  /** Curve up to the landing point, for the visual system. Empty when not aiming. */
6368
6434
  this.aimCurve = [];
6369
6435
  this.landingPoint = null;
6436
+ /** True when the current aim lands somewhere the player may actually stand. */
6437
+ this.aimValid = false;
6438
+ /** Sources currently aiming via thumbstick (hysteresis, so it doesn't chatter). */
6439
+ this.stickAiming = /* @__PURE__ */ new WeakSet();
6370
6440
  /** True once we've tried to self-heal camera parenting for this session. */
6371
6441
  this.parentingChecked = false;
6372
6442
  this.inputSystem = inputSystem;
@@ -6374,6 +6444,7 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
6374
6444
  update(world, deltaTime) {
6375
6445
  this.aimCurve = [];
6376
6446
  this.landingPoint = null;
6447
+ this.aimValid = false;
6377
6448
  if (!world.isXrPresenting()) {
6378
6449
  this.parentingChecked = false;
6379
6450
  return;
@@ -6381,6 +6452,7 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
6381
6452
  if (!this.parentingChecked) {
6382
6453
  this.parentingChecked = true;
6383
6454
  this.ensureCameraUnderRig(world);
6455
+ this.warnOnIncompleteSetup(world);
6384
6456
  }
6385
6457
  const rigEntityId = world.query(XRRigComponent, TransformComponent)[0];
6386
6458
  if (typeof rigEntityId !== "number") return;
@@ -6398,6 +6470,26 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
6398
6470
  this.applySmoothMove(world, rig, rigTransform, inputs, deltaTime);
6399
6471
  }
6400
6472
  }
6473
+ /**
6474
+ * Name what is missing, once per session.
6475
+ *
6476
+ * Inside a headset there is no console and no inspector, so a scene that
6477
+ * simply cannot move gives the author nothing to go on. These messages are
6478
+ * visible over remote debugging and say exactly which entity to add.
6479
+ */
6480
+ warnOnIncompleteSetup(world) {
6481
+ if (typeof world.query(XRRigComponent)[0] !== "number") {
6482
+ console.warn(
6483
+ "[XRLocomotion] No XR Rig in this scene \u2014 the player cannot move. Add one via Add Object \u2192 VR \u2192 XR Rig (the scene camera is parented to it automatically)."
6484
+ );
6485
+ return;
6486
+ }
6487
+ if (world.query(VRTeleportAreaComponent).length === 0) {
6488
+ console.warn(
6489
+ "[XRLocomotion] No Teleport Area in this scene \u2014 teleport has nowhere to land. Add one via Add Object \u2192 VR \u2192 Teleport Area, or add a Teleport Area component to an existing floor."
6490
+ );
6491
+ }
6492
+ }
6401
6493
  /**
6402
6494
  * Guarantee the render camera is a CHILD of the rig.
6403
6495
  *
@@ -6465,29 +6557,60 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
6465
6557
  }
6466
6558
  applyTeleport(world, transform, inputs) {
6467
6559
  const targets = this.collectTeleportTargets(world);
6468
- if (targets.length === 0) return;
6469
6560
  for (const input of inputs) {
6470
- if (!input.select && !input.selectEnd) continue;
6561
+ const source = input;
6562
+ const stick = this.readStickAim(input, source);
6563
+ const aiming = input.select || stick.aiming;
6564
+ const commit = input.selectEnd || stick.released;
6565
+ if (!aiming && !commit) continue;
6471
6566
  const points = sampleTeleportArc(
6472
6567
  input.rayOrigin,
6473
6568
  input.rayDirection,
6474
6569
  this.arcPoints
6475
6570
  );
6476
6571
  const hit = this.raycastArc(points, targets);
6477
- if (!hit) continue;
6478
- this.landingPoint = hit.point;
6479
- this.aimCurve = points.slice(0, hit.segmentIndex + 1).concat(hit.point);
6480
- if (input.selectEnd) {
6572
+ this.aimCurve = hit ? points.slice(0, hit.segmentIndex + 1).concat(hit.point) : points.slice();
6573
+ this.landingPoint = hit ? hit.point : null;
6574
+ this.aimValid = hit !== null;
6575
+ if (commit && hit) {
6481
6576
  transform.position.x = hit.point.x;
6482
6577
  transform.position.y = hit.point.y;
6483
6578
  transform.position.z = hit.point.z;
6484
6579
  this.aimCurve = [];
6485
6580
  this.landingPoint = null;
6581
+ this.aimValid = false;
6582
+ } else if (commit) {
6583
+ this.aimCurve = [];
6584
+ this.aimValid = false;
6486
6585
  }
6487
6586
  }
6488
6587
  }
6588
+ /**
6589
+ * Thumbstick-forward aiming with hysteresis.
6590
+ *
6591
+ * Engaging at 0.6 and releasing at 0.3 stops a stick resting near the
6592
+ * threshold from flickering the arc on and off — and the release edge is what
6593
+ * commits the teleport, so chatter there would teleport the player randomly.
6594
+ */
6595
+ readStickAim(input, source) {
6596
+ if (!input.hasAxes || input.axes.length < 4) {
6597
+ return { aiming: false, released: false };
6598
+ }
6599
+ const forward = -(input.axes[3] ?? 0);
6600
+ const wasAiming = this.stickAiming.has(source);
6601
+ if (!wasAiming && forward > 0.6) {
6602
+ this.stickAiming.add(source);
6603
+ return { aiming: true, released: false };
6604
+ }
6605
+ if (wasAiming && forward < 0.3) {
6606
+ this.stickAiming.delete(source);
6607
+ return { aiming: false, released: true };
6608
+ }
6609
+ return { aiming: wasAiming, released: false };
6610
+ }
6489
6611
  /** Walk the arc segment-by-segment, returning the first surface hit. */
6490
6612
  raycastArc(points, targets) {
6613
+ if (targets.length === 0) return null;
6491
6614
  for (let i = 1; i < points.length; i += 1) {
6492
6615
  const from = points[i - 1];
6493
6616
  const to = points[i];
@@ -6518,6 +6641,10 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
6518
6641
  getAimCurve() {
6519
6642
  return this.aimCurve;
6520
6643
  }
6644
+ /** True when the aim lands on a valid teleport area. */
6645
+ isAimValid() {
6646
+ return this.aimValid;
6647
+ }
6521
6648
  /** Where the arc currently lands, or null when not aiming at a valid area. */
6522
6649
  getLandingPoint() {
6523
6650
  return this.landingPoint;
@@ -6560,6 +6687,8 @@ var _XRSessionSystem = class _XRSessionSystem {
6560
6687
  constructor(inputSystem) {
6561
6688
  this.presenting = false;
6562
6689
  this.capabilities = NO_XR_CAPABILITIES;
6690
+ /** Y compensation applied when a floor-relative space was requested but not granted. */
6691
+ this.appliedOriginOffset = 0;
6563
6692
  this.inputSystem = inputSystem ?? null;
6564
6693
  }
6565
6694
  update(world) {
@@ -6583,11 +6712,59 @@ var _XRSessionSystem = class _XRSessionSystem {
6583
6712
  if (session && this.inputSystem) {
6584
6713
  this.inputSystem.attachSession(session);
6585
6714
  }
6715
+ this.applyTrackingOrigin(world, session ?? null);
6586
6716
  this.forEachBehaviour(world, (behaviour) => {
6587
6717
  behaviour.onXRSessionStart(this.capabilities);
6588
6718
  });
6589
6719
  }
6720
+ /**
6721
+ * Apply the rig's authored tracking origin, and compensate when the device
6722
+ * cannot honour it.
6723
+ *
6724
+ * `floor` (`local-floor`) puts Y=0 on the user's real floor, which is what a
6725
+ * scene authored standing on the ground expects. `eye` (`local`) puts Y=0 at
6726
+ * the headset, for seated experiences.
6727
+ *
6728
+ * The compensation matters: `local-floor` is only ever REQUESTED as an
6729
+ * optional feature (requiring it would make `requestSession` reject outright
6730
+ * on devices that lack it). When it isn't granted, the runtime silently falls
6731
+ * back to `local` — the origin jumps to head height and a floor-authored
6732
+ * scene swallows the user, which is exactly the "I'm standing under the
6733
+ * building" symptom. Offsetting the rig down by the eye height restores the
6734
+ * intended floor relationship instead of leaving it broken.
6735
+ */
6736
+ applyTrackingOrigin(world, session) {
6737
+ const rigEntityId = world.query(XRRigComponent, TransformComponent)[0];
6738
+ if (typeof rigEntityId !== "number") return;
6739
+ const rig = world.getComponent(rigEntityId, XRRigComponent);
6740
+ const transform = world.getComponent(rigEntityId, TransformComponent);
6741
+ if (!rig || !transform) return;
6742
+ const renderer = world.getRenderer();
6743
+ const wantsFloor = rig.trackingOrigin === "floor";
6744
+ try {
6745
+ renderer.xr.setReferenceSpaceType(wantsFloor ? "local-floor" : "local");
6746
+ } catch {
6747
+ }
6748
+ const granted = session?.enabledFeatures;
6749
+ const floorGranted = Array.isArray(granted) ? granted.includes("local-floor") : (
6750
+ // No report at all: trust the request, since a wrong offset is worse
6751
+ // than none on devices that did honour it.
6752
+ true
6753
+ );
6754
+ this.appliedOriginOffset = wantsFloor && !floorGranted ? rig.fallbackEyeHeight : 0;
6755
+ if (this.appliedOriginOffset !== 0) {
6756
+ transform.position.y -= this.appliedOriginOffset;
6757
+ }
6758
+ }
6590
6759
  handleSessionEnd(world) {
6760
+ if (this.appliedOriginOffset !== 0) {
6761
+ const rigEntityId = world.query(XRRigComponent, TransformComponent)[0];
6762
+ if (typeof rigEntityId === "number") {
6763
+ const transform = world.getComponent(rigEntityId, TransformComponent);
6764
+ if (transform) transform.position.y += this.appliedOriginOffset;
6765
+ }
6766
+ this.appliedOriginOffset = 0;
6767
+ }
6591
6768
  this.capabilities = NO_XR_CAPABILITIES;
6592
6769
  this.inputSystem?.detachSession();
6593
6770
  this.forEachBehaviour(world, (behaviour) => {
@@ -6681,17 +6858,74 @@ var _VRTeleportAreaSystem = class _VRTeleportAreaSystem {
6681
6858
  };
6682
6859
  __name(_VRTeleportAreaSystem, "VRTeleportAreaSystem");
6683
6860
  var VRTeleportAreaSystem = _VRTeleportAreaSystem;
6861
+ function createHandMaterial(preset, options) {
6862
+ const color = new THREE7.Color(options.color);
6863
+ switch (preset) {
6864
+ case "ghost":
6865
+ return new THREE7.MeshBasicMaterial({
6866
+ color,
6867
+ transparent: true,
6868
+ opacity: Math.min(1, Math.max(0.05, options.opacity * 0.45)),
6869
+ depthWrite: false,
6870
+ side: THREE7.DoubleSide
6871
+ });
6872
+ case "neon":
6873
+ return new THREE7.MeshStandardMaterial({
6874
+ color,
6875
+ emissive: color,
6876
+ emissiveIntensity: Math.max(0.5, options.emissiveIntensity || 2),
6877
+ roughness: 0.35,
6878
+ metalness: 0,
6879
+ transparent: options.opacity < 1,
6880
+ opacity: options.opacity
6881
+ });
6882
+ case "skeleton":
6883
+ return new THREE7.MeshBasicMaterial({
6884
+ color,
6885
+ wireframe: true,
6886
+ transparent: true,
6887
+ opacity: options.opacity
6888
+ });
6889
+ case "custom":
6890
+ case "default":
6891
+ return new THREE7.MeshStandardMaterial({
6892
+ color,
6893
+ roughness: 0.85,
6894
+ metalness: 0,
6895
+ emissive: color,
6896
+ emissiveIntensity: Math.max(0, options.emissiveIntensity),
6897
+ transparent: options.opacity < 1,
6898
+ opacity: options.opacity,
6899
+ side: THREE7.DoubleSide
6900
+ });
6901
+ default: {
6902
+ return new THREE7.MeshStandardMaterial({ color });
6903
+ }
6904
+ }
6905
+ }
6906
+ __name(createHandMaterial, "createHandMaterial");
6907
+ function applyHandMaterial(root, material) {
6908
+ root.traverse((child) => {
6909
+ const mesh = child;
6910
+ if (!mesh.isMesh) return;
6911
+ mesh.material = material;
6912
+ });
6913
+ }
6914
+ __name(applyHandMaterial, "applyHandMaterial");
6915
+ var SLOTS = [
6916
+ { index: 0, side: "left" },
6917
+ { index: 1, side: "right" }
6918
+ ];
6684
6919
  var _XRControllerVisualSystem = class _XRControllerVisualSystem {
6685
6920
  constructor(options) {
6686
6921
  this.controllerModelFactory = new XRControllerModelFactory();
6687
6922
  this.handModelFactory = new XRHandModelFactory();
6688
6923
  this.group = new THREE7.Group();
6689
- this.built = false;
6924
+ this.slots = /* @__PURE__ */ new Map();
6690
6925
  this.attachedTo = null;
6691
- /** Index → whether we've already attached visuals for that slot. */
6692
- this.ready = /* @__PURE__ */ new Set();
6693
- this.rays = [];
6694
- this.slots = options?.slots ?? 2;
6926
+ /** Last applied preset signature, so materials rebuild only on real change. */
6927
+ this.handStyleSignature = "";
6928
+ this.handMaterial = null;
6695
6929
  this.interactionSystem = options?.interactionSystem ?? null;
6696
6930
  this.group.name = "XRControllerVisuals";
6697
6931
  this.group.userData.ecsPreviewHidden = true;
@@ -6700,39 +6934,111 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
6700
6934
  const renderer = world.getRenderer();
6701
6935
  if (!renderer.xr?.enabled) return;
6702
6936
  this.ensureAttached(world);
6703
- this.updateRayVisibility();
6704
- if (this.built) return;
6705
- for (let index = 0; index < this.slots; index += 1) {
6706
- if (this.ready.has(index)) continue;
6937
+ this.ensureSlots(world);
6938
+ const interactor = this.resolveInteractor(world);
6939
+ const type = interactor?.interactorType ?? "both";
6940
+ const showControllers = type === "controllers" || type === "both";
6941
+ const showHands = type === "hands" || type === "both";
6942
+ this.syncHandMaterial(interactor);
6943
+ for (const { index, side } of SLOTS) {
6944
+ const visuals = this.slots.get(index);
6945
+ if (!visuals) continue;
6946
+ visuals.controllerModel.visible = showControllers;
6947
+ visuals.handModel.visible = showHands;
6948
+ visuals.ray.visible = this.shouldShowRay(interactor);
6949
+ if (showHands && this.handMaterial) {
6950
+ applyHandMaterial(visuals.handModel, this.handMaterial);
6951
+ }
6952
+ this.syncCustomContent(world, visuals, side);
6953
+ }
6954
+ }
6955
+ /** The scene's interactor config, if one was authored. */
6956
+ resolveInteractor(world) {
6957
+ const entityId = world.query(XRInteractorComponent)[0];
6958
+ if (typeof entityId !== "number") return null;
6959
+ return world.getComponent(entityId, XRInteractorComponent) ?? null;
6960
+ }
6961
+ /**
6962
+ * Re-parent the author's left/right interactor content onto the tracked grip
6963
+ * space so it follows the hand.
6964
+ *
6965
+ * `ObjectManagementSystem` reconciles ECS objects back under their ParentId
6966
+ * every frame, so this re-attach has to be re-asserted — the same coexistence
6967
+ * dance the AR anchor system performs.
6968
+ */
6969
+ syncCustomContent(world, visuals, side) {
6970
+ let content = null;
6971
+ for (const entityId of world.query(
6972
+ XRInteractorSideComponent,
6973
+ Object3DRef
6974
+ )) {
6975
+ const slot = world.getComponent(entityId, XRInteractorSideComponent);
6976
+ if (slot?.side !== side) continue;
6977
+ content = world.getComponent(entityId, Object3DRef)?.object ?? null;
6978
+ break;
6979
+ }
6980
+ if (!content) {
6981
+ visuals.attachedCustom = null;
6982
+ return;
6983
+ }
6984
+ if (content.parent !== visuals.grip) {
6985
+ visuals.grip.add(content);
6986
+ }
6987
+ visuals.attachedCustom = content;
6988
+ }
6989
+ /** Rebuild the hand material only when the authored style actually changes. */
6990
+ syncHandMaterial(interactor) {
6991
+ const preset = interactor?.handPreset ?? "default";
6992
+ const color = interactor?.handColor ?? "#e8b48a";
6993
+ const opacity = interactor?.handOpacity ?? 1;
6994
+ const emissive = interactor?.handEmissiveIntensity ?? 0;
6995
+ const signature = `${preset}:${color}:${opacity}:${emissive}`;
6996
+ if (signature === this.handStyleSignature && this.handMaterial) return;
6997
+ this.handStyleSignature = signature;
6998
+ this.handMaterial?.dispose();
6999
+ this.handMaterial = createHandMaterial(preset, {
7000
+ color,
7001
+ opacity,
7002
+ emissiveIntensity: emissive
7003
+ });
7004
+ }
7005
+ /**
7006
+ * The ray is meaningful only when it points at something. Authors can turn it
7007
+ * off entirely; otherwise it appears on hover/grab so it isn't permanent
7008
+ * clutter implying the whole scene is clickable.
7009
+ */
7010
+ shouldShowRay(interactor) {
7011
+ if (interactor && !interactor.showRay) return false;
7012
+ if (!this.interactionSystem) return true;
7013
+ return this.interactionSystem.getHoveredEntityId() !== null || this.interactionSystem.isGrabbing();
7014
+ }
7015
+ /** Lazily build per-slot visuals; the device reports profiles late. */
7016
+ ensureSlots(world) {
7017
+ const renderer = world.getRenderer();
7018
+ for (const { index } of SLOTS) {
7019
+ if (this.slots.has(index)) continue;
6707
7020
  const controller = renderer.xr.getController(index);
6708
7021
  const grip = renderer.xr.getControllerGrip(index);
6709
7022
  const hand = renderer.xr.getHand(index);
6710
7023
  if (!controller || !grip || !hand) continue;
6711
7024
  const ray = this.createRay();
6712
- this.rays.push(ray);
6713
7025
  controller.add(ray);
6714
- this.group.add(controller);
6715
- grip.add(this.controllerModelFactory.createControllerModel(grip));
6716
- this.group.add(grip);
6717
- hand.add(this.handModelFactory.createHandModel(hand, "mesh"));
6718
- this.group.add(hand);
6719
- this.ready.add(index);
6720
- }
6721
- if (this.ready.size >= this.slots) {
6722
- this.built = true;
7026
+ const controllerModel = this.controllerModelFactory.createControllerModel(grip);
7027
+ grip.add(controllerModel);
7028
+ const handModel = this.handModelFactory.createHandModel(hand, "mesh");
7029
+ hand.add(handModel);
7030
+ this.group.add(controller, grip, hand);
7031
+ this.slots.set(index, {
7032
+ controller,
7033
+ grip,
7034
+ hand,
7035
+ ray,
7036
+ controllerModel,
7037
+ handModel,
7038
+ attachedCustom: null
7039
+ });
6723
7040
  }
6724
7041
  }
6725
- /**
6726
- * Show the aim ray only when it is meaningful: hovering something
6727
- * interactive, or mid-grab. A permanently-drawn ray is visual noise, and it
6728
- * implies everything is clickable when most of the scene is not. Teleport
6729
- * aiming has its own arc, so the straight ray stays hidden for that.
6730
- */
6731
- updateRayVisibility() {
6732
- if (!this.interactionSystem) return;
6733
- const active = this.interactionSystem.getHoveredEntityId() !== null || this.interactionSystem.isGrabbing();
6734
- for (const ray of this.rays) ray.visible = active;
6735
- }
6736
7042
  /**
6737
7043
  * Parent the visuals to the XR rig when there is one, so locomotion moves the
6738
7044
  * hands with the player. Falls back to the scene root.
@@ -6758,7 +7064,7 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
6758
7064
  const line = new THREE7.Line(geometry, material);
6759
7065
  line.name = "XRPointerRay";
6760
7066
  line.renderOrder = 999;
6761
- line.visible = this.interactionSystem === null;
7067
+ line.visible = false;
6762
7068
  return line;
6763
7069
  }
6764
7070
  dispose() {
@@ -6774,9 +7080,10 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
6774
7080
  }
6775
7081
  });
6776
7082
  this.group.clear();
6777
- this.rays.length = 0;
6778
- this.ready.clear();
6779
- this.built = false;
7083
+ this.slots.clear();
7084
+ this.handMaterial?.dispose();
7085
+ this.handMaterial = null;
7086
+ this.handStyleSignature = "";
6780
7087
  this.attachedTo = null;
6781
7088
  }
6782
7089
  };
@@ -6784,6 +7091,7 @@ __name(_XRControllerVisualSystem, "XRControllerVisualSystem");
6784
7091
  var XRControllerVisualSystem = _XRControllerVisualSystem;
6785
7092
  var MARKER_RADIUS = 0.28;
6786
7093
  var ACCENT = new THREE7.Color("#4da3ff");
7094
+ var BLOCKED = new THREE7.Color("#ff5a5a");
6787
7095
  var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
6788
7096
  constructor(locomotionSystem) {
6789
7097
  this.group = new THREE7.Group();
@@ -6814,16 +7122,22 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
6814
7122
  }
6815
7123
  const curve = this.locomotionSystem.getAimCurve();
6816
7124
  const landing = this.locomotionSystem.getLandingPoint();
6817
- if (!landing || curve.length < 2) {
7125
+ const valid = this.locomotionSystem.isAimValid();
7126
+ if (curve.length < 2) {
6818
7127
  this.setVisible(false);
6819
7128
  return;
6820
7129
  }
6821
- this.updateArcGeometry(curve);
6822
- this.marker.position.copy(landing);
6823
- this.marker.position.y += 0.01;
7130
+ this.updateArcGeometry(curve, valid ? ACCENT : BLOCKED);
7131
+ if (valid && landing) {
7132
+ this.marker.position.copy(landing);
7133
+ this.marker.position.y += 0.01;
7134
+ this.marker.visible = true;
7135
+ } else {
7136
+ this.marker.visible = false;
7137
+ }
6824
7138
  this.setVisible(true);
6825
7139
  }
6826
- updateArcGeometry(curve) {
7140
+ updateArcGeometry(curve, tint) {
6827
7141
  const count = curve.length;
6828
7142
  const positions = new Float32Array(count * 3);
6829
7143
  const colors = new Float32Array(count * 3);
@@ -6834,9 +7148,9 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
6834
7148
  positions[i * 3 + 2] = point.z;
6835
7149
  const t = count > 1 ? i / (count - 1) : 0;
6836
7150
  const fade = 1 - t * 0.75;
6837
- colors[i * 3] = ACCENT.r * fade;
6838
- colors[i * 3 + 1] = ACCENT.g * fade;
6839
- colors[i * 3 + 2] = ACCENT.b * fade;
7151
+ colors[i * 3] = tint.r * fade;
7152
+ colors[i * 3 + 1] = tint.g * fade;
7153
+ colors[i * 3 + 2] = tint.b * fade;
6840
7154
  }
6841
7155
  this.arcGeometry.setAttribute(
6842
7156
  "position",
@@ -6898,6 +7212,6 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
6898
7212
  __name(_XRTeleportVisualSystem, "XRTeleportVisualSystem");
6899
7213
  var XRTeleportVisualSystem = _XRTeleportVisualSystem;
6900
7214
 
6901
- export { ADDED_NODE_MARKER_COMPONENT, ARC_GRAVITY, ARC_MAX_TIME, ARC_SPEED, ARC_STEPS, 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, VRTeleportAreaSystem, VideoSystem, XRControllerVisualSystem, XRInputSystem, XRInteractionSystem, XRLocomotionSystem, XRSessionSystem, XRTeleportVisualSystem, arcGroundCrossing, 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, sampleTeleportArc, setPrefabAssetUrlResolver, spawnPrefab };
6902
- //# sourceMappingURL=chunk-G53MZVKQ.js.map
6903
- //# sourceMappingURL=chunk-G53MZVKQ.js.map
7215
+ export { ADDED_NODE_MARKER_COMPONENT, ARC_GRAVITY, ARC_MAX_TIME, ARC_SPEED, ARC_STEPS, 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, VRTeleportAreaSystem, VideoSystem, XRControllerVisualSystem, XRInputSystem, XRInteractionSystem, XRLocomotionSystem, XRSessionSystem, XRTeleportVisualSystem, applyHandMaterial, arcGroundCrossing, buildComponentValueMaps, buildNodeKeyMapsFromSnapshot, buildNodeMapsFromSnapshot, canonicalizeComponentValue, clearPrefabCache, collectDiffPaths, collectEntitySubtree, collectSnapshotSubtreeIds, computeInstanceOverrides, createHandMaterial, decanonicalizeComponentValue, despawnPrefab, encodeOverride, findSnapshotRootEntityIds, findTopLevelNestedRegions, groupOverridePaths, hydrateWorldFromSnapshot, hydrateWorldFromSnapshotDetailed, indexSnapshotByEntity, instantiateSubtree, loadPrefab, mergeComponentValue, parseOverride, parsePrefabAsset, prefabFileVersionFor, readXrCapabilities, restoreComponentState, restorePostCreateComponents, sampleTeleportArc, setPrefabAssetUrlResolver, spawnPrefab };
7216
+ //# sourceMappingURL=chunk-O6FJA4XW.js.map
7217
+ //# sourceMappingURL=chunk-O6FJA4XW.js.map