@relu-interactives/spatial-ecs 0.3.4 → 0.3.6

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-ID266ZUA.js → chunk-WRJ2K3LZ.js} +525 -41
  12. package/dist/chunk-WRJ2K3LZ.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-BtKOaHfY.d.ts → index-BltjPEhg.d.cts} +10 -3
  24. package/dist/{index-CRmfSYdj.d.ts → index-BpbmZuOz.d.ts} +92 -16
  25. package/dist/{index-Bc0YCz1A.d.cts → index-C5FrDt3z.d.cts} +92 -16
  26. package/dist/{index-Xu238jLn.d.cts → index-W8pUajCW.d.ts} +10 -3
  27. package/dist/index.cjs +829 -261
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +76 -9
  30. package/dist/index.d.ts +76 -9
  31. package/dist/index.js +8 -8
  32. package/dist/systems/index.cjs +532 -36
  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-ID266ZUA.js.map +0 -1
@@ -2109,6 +2109,7 @@ var XRInteractableComponent = _XRInteractableComponent;
2109
2109
  var _XRRigComponent = class _XRRigComponent extends Component {
2110
2110
  constructor(options) {
2111
2111
  super();
2112
+ this.trackingOrigin = options?.trackingOrigin ?? "floor";
2112
2113
  this.locomotion = options?.locomotion ?? "teleport";
2113
2114
  this.moveSpeed = options?.moveSpeed ?? 2;
2114
2115
  this.snapTurn = options?.snapTurn ?? true;
@@ -2119,6 +2120,29 @@ var _XRRigComponent = class _XRRigComponent extends Component {
2119
2120
  __name(_XRRigComponent, "XRRigComponent");
2120
2121
  var XRRigComponent = _XRRigComponent;
2121
2122
 
2123
+ // core/components/XRInteractor.ts
2124
+ var _XRInteractorComponent = class _XRInteractorComponent extends Component {
2125
+ constructor(options) {
2126
+ super();
2127
+ this.interactorType = options?.interactorType ?? "both";
2128
+ this.handPreset = options?.handPreset ?? "default";
2129
+ this.handColor = options?.handColor ?? "#e8b48a";
2130
+ this.handOpacity = options?.handOpacity ?? 1;
2131
+ this.handEmissiveIntensity = options?.handEmissiveIntensity ?? 0;
2132
+ this.showRay = options?.showRay ?? true;
2133
+ }
2134
+ };
2135
+ __name(_XRInteractorComponent, "XRInteractorComponent");
2136
+ var XRInteractorComponent = _XRInteractorComponent;
2137
+ var _XRInteractorSideComponent = class _XRInteractorSideComponent extends Component {
2138
+ constructor(options) {
2139
+ super();
2140
+ this.side = options?.side ?? "left";
2141
+ }
2142
+ };
2143
+ __name(_XRInteractorSideComponent, "XRInteractorSideComponent");
2144
+ var XRInteractorSideComponent = _XRInteractorSideComponent;
2145
+
2122
2146
  // core/utils/dracoDecoderPath.ts
2123
2147
  var LOCAL_DRACO_DECODER_RELATIVE_PATH = "./assets/draco/";
2124
2148
  var DRACO_DECODER_CDN_PATH = "https://www.gstatic.com/draco/versioned/decoders/1.5.7/";
@@ -4913,6 +4937,14 @@ function buildComponentValueMaps(snapshot) {
4913
4937
  snapshot.components,
4914
4938
  "XRInteractableComponent"
4915
4939
  ),
4940
+ xrInteractorValues: readByComponent(
4941
+ snapshot.components,
4942
+ "XRInteractorComponent"
4943
+ ),
4944
+ xrInteractorSideValues: readByComponent(
4945
+ snapshot.components,
4946
+ "XRInteractorSideComponent"
4947
+ ),
4916
4948
  instancedMeshValues: readByComponent(snapshot.components, "InstancedMeshComponent"),
4917
4949
  instanceSlotValues: readByComponent(snapshot.components, "InstanceSlotComponent"),
4918
4950
  prefabNodeValues: readByComponent(snapshot.components, "PrefabNode"),
@@ -5099,6 +5131,60 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
5099
5131
  }
5100
5132
  }
5101
5133
  }
5134
+ {
5135
+ const sourceInteractor = asRecord3(
5136
+ values.xrInteractorValues.get(sourceEntityId)
5137
+ );
5138
+ const interactor = world.getComponent(targetEntityId, XRInteractorComponent);
5139
+ if (sourceInteractor && interactor) {
5140
+ if (typeof sourceInteractor.interactorType === "string") {
5141
+ interactor.interactorType = sourceInteractor.interactorType;
5142
+ }
5143
+ if (typeof sourceInteractor.handPreset === "string") {
5144
+ interactor.handPreset = sourceInteractor.handPreset;
5145
+ }
5146
+ if (typeof sourceInteractor.handColor === "string") {
5147
+ interactor.handColor = sourceInteractor.handColor;
5148
+ }
5149
+ if (typeof sourceInteractor.handOpacity === "number") {
5150
+ interactor.handOpacity = sourceInteractor.handOpacity;
5151
+ }
5152
+ if (typeof sourceInteractor.handEmissiveIntensity === "number") {
5153
+ interactor.handEmissiveIntensity = sourceInteractor.handEmissiveIntensity;
5154
+ }
5155
+ if (typeof sourceInteractor.showRay === "boolean") {
5156
+ interactor.showRay = sourceInteractor.showRay;
5157
+ }
5158
+ }
5159
+ }
5160
+ {
5161
+ const sourceSide = asRecord3(
5162
+ values.xrInteractorSideValues.get(sourceEntityId)
5163
+ );
5164
+ if (sourceSide && typeof sourceSide.side === "string") {
5165
+ const existing = world.getComponent(
5166
+ targetEntityId,
5167
+ XRInteractorSideComponent
5168
+ );
5169
+ if (existing) {
5170
+ existing.side = sourceSide.side;
5171
+ } else {
5172
+ world.addComponent(
5173
+ targetEntityId,
5174
+ new XRInteractorSideComponent({
5175
+ side: sourceSide.side
5176
+ })
5177
+ );
5178
+ }
5179
+ }
5180
+ }
5181
+ {
5182
+ const sourceRigOrigin = asRecord3(values.xrRigValues.get(sourceEntityId));
5183
+ const rigForOrigin = world.getComponent(targetEntityId, XRRigComponent);
5184
+ if (sourceRigOrigin && rigForOrigin && typeof sourceRigOrigin.trackingOrigin === "string") {
5185
+ rigForOrigin.trackingOrigin = sourceRigOrigin.trackingOrigin;
5186
+ }
5187
+ }
5102
5188
  {
5103
5189
  const imComponent = world.getComponent(targetEntityId, InstancedMeshComponent);
5104
5190
  if (imComponent && imComponent.instanceEntityIds.length > 0) {
@@ -5688,6 +5774,7 @@ var HYDRATABLE_ENTITY_KINDS = /* @__PURE__ */ new Set([
5688
5774
  "instanceSlot",
5689
5775
  "scriptObject",
5690
5776
  "xrRig",
5777
+ "xrInteractor",
5691
5778
  "teleportArea"
5692
5779
  ]);
5693
5780
 
@@ -7698,6 +7785,9 @@ var _XRInputSystem = class _XRInputSystem {
7698
7785
  state.hasAxes = false;
7699
7786
  state.buttons = [];
7700
7787
  }
7788
+ }
7789
+ for (const record of this.records.values()) {
7790
+ const state = record.state;
7701
7791
  if (record.pendingSelectStart) {
7702
7792
  state.select = true;
7703
7793
  state.selectStart = true;
@@ -7895,6 +7985,32 @@ var _XRInteractionSystem = class _XRInteractionSystem {
7895
7985
  };
7896
7986
  __name(_XRInteractionSystem, "XRInteractionSystem");
7897
7987
  var XRInteractionSystem = _XRInteractionSystem;
7988
+ var ARC_GRAVITY = 9.8;
7989
+ var ARC_SPEED = 7;
7990
+ var ARC_STEPS = 24;
7991
+ var ARC_MAX_TIME = 2;
7992
+ function sampleTeleportArc(origin, direction, out = [], options) {
7993
+ const speed = options?.speed ?? ARC_SPEED;
7994
+ const gravity = options?.gravity ?? ARC_GRAVITY;
7995
+ const steps = Math.max(2, options?.steps ?? ARC_STEPS);
7996
+ const maxTime = options?.maxTime ?? ARC_MAX_TIME;
7997
+ out.length = 0;
7998
+ const velocity = direction.clone().normalize().multiplyScalar(speed);
7999
+ for (let i = 0; i < steps; i += 1) {
8000
+ const t = i / (steps - 1) * maxTime;
8001
+ out.push(
8002
+ new THREE14__namespace.Vector3(
8003
+ origin.x + velocity.x * t,
8004
+ origin.y + velocity.y * t - 0.5 * gravity * t * t,
8005
+ origin.z + velocity.z * t
8006
+ )
8007
+ );
8008
+ }
8009
+ return out;
8010
+ }
8011
+ __name(sampleTeleportArc, "sampleTeleportArc");
8012
+
8013
+ // core/systems/XRLocomotionSystem.ts
7898
8014
  var UP = new THREE14__namespace.Vector3(0, 1, 0);
7899
8015
  var AXIS_DEADZONE = 0.5;
7900
8016
  var _XRLocomotionSystem = class _XRLocomotionSystem {
@@ -7902,13 +8018,26 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
7902
8018
  this.raycaster = new THREE14__namespace.Raycaster();
7903
8019
  /** Per-source latch so one stick push yields exactly one snap turn. */
7904
8020
  this.turnLatched = /* @__PURE__ */ new WeakSet();
7905
- /** Where the pointer currently lands, for the consumer to draw a marker. */
7906
- this.teleportPoint = null;
8021
+ /** Reused arc buffer — aiming runs every frame. */
8022
+ this.arcPoints = [];
8023
+ /** Curve up to the landing point, for the visual system. Empty when not aiming. */
8024
+ this.aimCurve = [];
8025
+ this.landingPoint = null;
8026
+ /** True once we've tried to self-heal camera parenting for this session. */
8027
+ this.parentingChecked = false;
7907
8028
  this.inputSystem = inputSystem;
7908
8029
  }
7909
8030
  update(world, deltaTime) {
7910
- this.teleportPoint = null;
7911
- if (!world.isXrPresenting()) return;
8031
+ this.aimCurve = [];
8032
+ this.landingPoint = null;
8033
+ if (!world.isXrPresenting()) {
8034
+ this.parentingChecked = false;
8035
+ return;
8036
+ }
8037
+ if (!this.parentingChecked) {
8038
+ this.parentingChecked = true;
8039
+ this.ensureCameraUnderRig(world);
8040
+ }
7912
8041
  const rigEntityId = world.query(XRRigComponent, TransformComponent)[0];
7913
8042
  if (typeof rigEntityId !== "number") return;
7914
8043
  const rig = world.getComponent(rigEntityId, XRRigComponent);
@@ -7925,9 +8054,37 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
7925
8054
  this.applySmoothMove(world, rig, rigTransform, inputs, deltaTime);
7926
8055
  }
7927
8056
  }
8057
+ /**
8058
+ * Guarantee the render camera is a CHILD of the rig.
8059
+ *
8060
+ * Locomotion moves the rig, and three composes the head pose against
8061
+ * `camera.parent` — so if the camera sits at the scene root instead, moving
8062
+ * the rig does nothing at all and the player is frozen in place. Scenes
8063
+ * authored before the rig existed (or with the camera re-parented by hand)
8064
+ * would otherwise silently fail to move, which is exactly the symptom that is
8065
+ * hardest to diagnose from inside a headset. Repairing it via `ParentId` lets
8066
+ * `ObjectManagementSystem` reconcile the scene graph normally.
8067
+ */
8068
+ ensureCameraUnderRig(world) {
8069
+ const rigEntityId = world.query(XRRigComponent)[0];
8070
+ if (typeof rigEntityId !== "number") return;
8071
+ const activeCamera = world.getCamera();
8072
+ for (const entityId of world.query(CameraComponent, Object3DRef)) {
8073
+ if (world.getComponent(entityId, Object3DRef)?.object !== activeCamera) {
8074
+ continue;
8075
+ }
8076
+ const parent = world.getComponent(entityId, ParentId);
8077
+ if (parent) {
8078
+ if (parent.value !== rigEntityId) parent.value = rigEntityId;
8079
+ } else {
8080
+ world.addComponent(entityId, new ParentId(rigEntityId));
8081
+ }
8082
+ return;
8083
+ }
8084
+ }
7928
8085
  /**
7929
8086
  * Snap turning, not smooth turning — rotating the view continuously is the
7930
- * single biggest cause of VR nausea, so the comfort-safe option is the one
8087
+ * single biggest cause of VR nausea, so the comfort-safe option is what's
7931
8088
  * wired to the stick.
7932
8089
  */
7933
8090
  applySnapTurn(rig, transform, inputs) {
@@ -7941,8 +8098,7 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
7941
8098
  }
7942
8099
  if (this.turnLatched.has(source)) continue;
7943
8100
  this.turnLatched.add(source);
7944
- const step = THREE14__namespace.MathUtils.degToRad(rig.snapTurnDegrees);
7945
- transform.rotation.y -= Math.sign(x) * step;
8101
+ transform.rotation.y -= Math.sign(x) * THREE14__namespace.MathUtils.degToRad(rig.snapTurnDegrees);
7946
8102
  }
7947
8103
  }
7948
8104
  applySmoothMove(world, rig, transform, inputs, deltaTime) {
@@ -7967,16 +8123,41 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
7967
8123
  const targets = this.collectTeleportTargets(world);
7968
8124
  if (targets.length === 0) return;
7969
8125
  for (const input of inputs) {
7970
- this.raycaster.set(input.rayOrigin, input.rayDirection);
7971
- const hit = this.raycaster.intersectObjects(targets, true)[0];
8126
+ if (!input.select && !input.selectEnd) continue;
8127
+ const points = sampleTeleportArc(
8128
+ input.rayOrigin,
8129
+ input.rayDirection,
8130
+ this.arcPoints
8131
+ );
8132
+ const hit = this.raycastArc(points, targets);
7972
8133
  if (!hit) continue;
7973
- this.teleportPoint = hit.point.clone();
8134
+ this.landingPoint = hit.point;
8135
+ this.aimCurve = points.slice(0, hit.segmentIndex + 1).concat(hit.point);
7974
8136
  if (input.selectEnd) {
7975
8137
  transform.position.x = hit.point.x;
7976
8138
  transform.position.y = hit.point.y;
7977
8139
  transform.position.z = hit.point.z;
8140
+ this.aimCurve = [];
8141
+ this.landingPoint = null;
8142
+ }
8143
+ }
8144
+ }
8145
+ /** Walk the arc segment-by-segment, returning the first surface hit. */
8146
+ raycastArc(points, targets) {
8147
+ for (let i = 1; i < points.length; i += 1) {
8148
+ const from = points[i - 1];
8149
+ const to = points[i];
8150
+ const delta = new THREE14__namespace.Vector3().subVectors(to, from);
8151
+ const distance = delta.length();
8152
+ if (distance < 1e-6) continue;
8153
+ this.raycaster.set(from, delta.divideScalar(distance));
8154
+ this.raycaster.far = distance;
8155
+ const hit = this.raycaster.intersectObjects(targets, true)[0];
8156
+ if (hit) {
8157
+ return { point: hit.point.clone(), segmentIndex: i - 1 };
7978
8158
  }
7979
8159
  }
8160
+ return null;
7980
8161
  }
7981
8162
  collectTeleportTargets(world) {
7982
8163
  const objects = [];
@@ -7989,12 +8170,13 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
7989
8170
  }
7990
8171
  return objects;
7991
8172
  }
7992
- /**
7993
- * Where the teleport pointer currently lands this frame, or null.
7994
- * Consumers use this to render an aim marker.
7995
- */
7996
- getTeleportPoint() {
7997
- return this.teleportPoint;
8173
+ /** Arc points up to the landing spot this frame; empty when not aiming. */
8174
+ getAimCurve() {
8175
+ return this.aimCurve;
8176
+ }
8177
+ /** Where the arc currently lands, or null when not aiming at a valid area. */
8178
+ getLandingPoint() {
8179
+ return this.landingPoint;
7998
8180
  }
7999
8181
  };
8000
8182
  __name(_XRLocomotionSystem, "XRLocomotionSystem");
@@ -8034,6 +8216,8 @@ var _XRSessionSystem = class _XRSessionSystem {
8034
8216
  constructor(inputSystem) {
8035
8217
  this.presenting = false;
8036
8218
  this.capabilities = NO_XR_CAPABILITIES;
8219
+ /** Y compensation applied when a floor-relative space was requested but not granted. */
8220
+ this.appliedOriginOffset = 0;
8037
8221
  this.inputSystem = inputSystem ?? null;
8038
8222
  }
8039
8223
  update(world) {
@@ -8057,11 +8241,59 @@ var _XRSessionSystem = class _XRSessionSystem {
8057
8241
  if (session && this.inputSystem) {
8058
8242
  this.inputSystem.attachSession(session);
8059
8243
  }
8244
+ this.applyTrackingOrigin(world, session ?? null);
8060
8245
  this.forEachBehaviour(world, (behaviour) => {
8061
8246
  behaviour.onXRSessionStart(this.capabilities);
8062
8247
  });
8063
8248
  }
8249
+ /**
8250
+ * Apply the rig's authored tracking origin, and compensate when the device
8251
+ * cannot honour it.
8252
+ *
8253
+ * `floor` (`local-floor`) puts Y=0 on the user's real floor, which is what a
8254
+ * scene authored standing on the ground expects. `eye` (`local`) puts Y=0 at
8255
+ * the headset, for seated experiences.
8256
+ *
8257
+ * The compensation matters: `local-floor` is only ever REQUESTED as an
8258
+ * optional feature (requiring it would make `requestSession` reject outright
8259
+ * on devices that lack it). When it isn't granted, the runtime silently falls
8260
+ * back to `local` — the origin jumps to head height and a floor-authored
8261
+ * scene swallows the user, which is exactly the "I'm standing under the
8262
+ * building" symptom. Offsetting the rig down by the eye height restores the
8263
+ * intended floor relationship instead of leaving it broken.
8264
+ */
8265
+ applyTrackingOrigin(world, session) {
8266
+ const rigEntityId = world.query(XRRigComponent, TransformComponent)[0];
8267
+ if (typeof rigEntityId !== "number") return;
8268
+ const rig = world.getComponent(rigEntityId, XRRigComponent);
8269
+ const transform = world.getComponent(rigEntityId, TransformComponent);
8270
+ if (!rig || !transform) return;
8271
+ const renderer = world.getRenderer();
8272
+ const wantsFloor = rig.trackingOrigin === "floor";
8273
+ try {
8274
+ renderer.xr.setReferenceSpaceType(wantsFloor ? "local-floor" : "local");
8275
+ } catch {
8276
+ }
8277
+ const granted = session?.enabledFeatures;
8278
+ const floorGranted = Array.isArray(granted) ? granted.includes("local-floor") : (
8279
+ // No report at all: trust the request, since a wrong offset is worse
8280
+ // than none on devices that did honour it.
8281
+ true
8282
+ );
8283
+ this.appliedOriginOffset = wantsFloor && !floorGranted ? rig.fallbackEyeHeight : 0;
8284
+ if (this.appliedOriginOffset !== 0) {
8285
+ transform.position.y -= this.appliedOriginOffset;
8286
+ }
8287
+ }
8064
8288
  handleSessionEnd(world) {
8289
+ if (this.appliedOriginOffset !== 0) {
8290
+ const rigEntityId = world.query(XRRigComponent, TransformComponent)[0];
8291
+ if (typeof rigEntityId === "number") {
8292
+ const transform = world.getComponent(rigEntityId, TransformComponent);
8293
+ if (transform) transform.position.y += this.appliedOriginOffset;
8294
+ }
8295
+ this.appliedOriginOffset = 0;
8296
+ }
8065
8297
  this.capabilities = NO_XR_CAPABILITIES;
8066
8298
  this.inputSystem?.detachSession();
8067
8299
  this.forEachBehaviour(world, (behaviour) => {
@@ -8155,16 +8387,75 @@ var _VRTeleportAreaSystem = class _VRTeleportAreaSystem {
8155
8387
  };
8156
8388
  __name(_VRTeleportAreaSystem, "VRTeleportAreaSystem");
8157
8389
  var VRTeleportAreaSystem = _VRTeleportAreaSystem;
8390
+ function createHandMaterial(preset, options) {
8391
+ const color = new THREE14__namespace.Color(options.color);
8392
+ switch (preset) {
8393
+ case "ghost":
8394
+ return new THREE14__namespace.MeshBasicMaterial({
8395
+ color,
8396
+ transparent: true,
8397
+ opacity: Math.min(1, Math.max(0.05, options.opacity * 0.45)),
8398
+ depthWrite: false,
8399
+ side: THREE14__namespace.DoubleSide
8400
+ });
8401
+ case "neon":
8402
+ return new THREE14__namespace.MeshStandardMaterial({
8403
+ color,
8404
+ emissive: color,
8405
+ emissiveIntensity: Math.max(0.5, options.emissiveIntensity || 2),
8406
+ roughness: 0.35,
8407
+ metalness: 0,
8408
+ transparent: options.opacity < 1,
8409
+ opacity: options.opacity
8410
+ });
8411
+ case "skeleton":
8412
+ return new THREE14__namespace.MeshBasicMaterial({
8413
+ color,
8414
+ wireframe: true,
8415
+ transparent: true,
8416
+ opacity: options.opacity
8417
+ });
8418
+ case "custom":
8419
+ case "default":
8420
+ return new THREE14__namespace.MeshStandardMaterial({
8421
+ color,
8422
+ roughness: 0.85,
8423
+ metalness: 0,
8424
+ emissive: color,
8425
+ emissiveIntensity: Math.max(0, options.emissiveIntensity),
8426
+ transparent: options.opacity < 1,
8427
+ opacity: options.opacity,
8428
+ side: THREE14__namespace.DoubleSide
8429
+ });
8430
+ default: {
8431
+ return new THREE14__namespace.MeshStandardMaterial({ color });
8432
+ }
8433
+ }
8434
+ }
8435
+ __name(createHandMaterial, "createHandMaterial");
8436
+ function applyHandMaterial(root, material) {
8437
+ root.traverse((child) => {
8438
+ const mesh = child;
8439
+ if (!mesh.isMesh) return;
8440
+ mesh.material = material;
8441
+ });
8442
+ }
8443
+ __name(applyHandMaterial, "applyHandMaterial");
8444
+ var SLOTS = [
8445
+ { index: 0, side: "left" },
8446
+ { index: 1, side: "right" }
8447
+ ];
8158
8448
  var _XRControllerVisualSystem = class _XRControllerVisualSystem {
8159
- constructor(slots = 2) {
8449
+ constructor(options) {
8160
8450
  this.controllerModelFactory = new XRControllerModelFactory_js.XRControllerModelFactory();
8161
8451
  this.handModelFactory = new XRHandModelFactory_js.XRHandModelFactory();
8162
8452
  this.group = new THREE14__namespace.Group();
8163
- this.built = false;
8453
+ this.slots = /* @__PURE__ */ new Map();
8164
8454
  this.attachedTo = null;
8165
- /** Index → whether we've already attached visuals for that slot. */
8166
- this.ready = /* @__PURE__ */ new Set();
8167
- this.slots = slots;
8455
+ /** Last applied preset signature, so materials rebuild only on real change. */
8456
+ this.handStyleSignature = "";
8457
+ this.handMaterial = null;
8458
+ this.interactionSystem = options?.interactionSystem ?? null;
8168
8459
  this.group.name = "XRControllerVisuals";
8169
8460
  this.group.userData.ecsPreviewHidden = true;
8170
8461
  }
@@ -8172,23 +8463,109 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
8172
8463
  const renderer = world.getRenderer();
8173
8464
  if (!renderer.xr?.enabled) return;
8174
8465
  this.ensureAttached(world);
8175
- if (this.built) return;
8176
- for (let index = 0; index < this.slots; index += 1) {
8177
- if (this.ready.has(index)) continue;
8466
+ this.ensureSlots(world);
8467
+ const interactor = this.resolveInteractor(world);
8468
+ const type = interactor?.interactorType ?? "both";
8469
+ const showControllers = type === "controllers" || type === "both";
8470
+ const showHands = type === "hands" || type === "both";
8471
+ this.syncHandMaterial(interactor);
8472
+ for (const { index, side } of SLOTS) {
8473
+ const visuals = this.slots.get(index);
8474
+ if (!visuals) continue;
8475
+ visuals.controllerModel.visible = showControllers;
8476
+ visuals.handModel.visible = showHands;
8477
+ visuals.ray.visible = this.shouldShowRay(interactor);
8478
+ if (showHands && this.handMaterial) {
8479
+ applyHandMaterial(visuals.handModel, this.handMaterial);
8480
+ }
8481
+ this.syncCustomContent(world, visuals, side);
8482
+ }
8483
+ }
8484
+ /** The scene's interactor config, if one was authored. */
8485
+ resolveInteractor(world) {
8486
+ const entityId = world.query(XRInteractorComponent)[0];
8487
+ if (typeof entityId !== "number") return null;
8488
+ return world.getComponent(entityId, XRInteractorComponent) ?? null;
8489
+ }
8490
+ /**
8491
+ * Re-parent the author's left/right interactor content onto the tracked grip
8492
+ * space so it follows the hand.
8493
+ *
8494
+ * `ObjectManagementSystem` reconciles ECS objects back under their ParentId
8495
+ * every frame, so this re-attach has to be re-asserted — the same coexistence
8496
+ * dance the AR anchor system performs.
8497
+ */
8498
+ syncCustomContent(world, visuals, side) {
8499
+ let content = null;
8500
+ for (const entityId of world.query(
8501
+ XRInteractorSideComponent,
8502
+ Object3DRef
8503
+ )) {
8504
+ const slot = world.getComponent(entityId, XRInteractorSideComponent);
8505
+ if (slot?.side !== side) continue;
8506
+ content = world.getComponent(entityId, Object3DRef)?.object ?? null;
8507
+ break;
8508
+ }
8509
+ if (!content) {
8510
+ visuals.attachedCustom = null;
8511
+ return;
8512
+ }
8513
+ if (content.parent !== visuals.grip) {
8514
+ visuals.grip.add(content);
8515
+ }
8516
+ visuals.attachedCustom = content;
8517
+ }
8518
+ /** Rebuild the hand material only when the authored style actually changes. */
8519
+ syncHandMaterial(interactor) {
8520
+ const preset = interactor?.handPreset ?? "default";
8521
+ const color = interactor?.handColor ?? "#e8b48a";
8522
+ const opacity = interactor?.handOpacity ?? 1;
8523
+ const emissive = interactor?.handEmissiveIntensity ?? 0;
8524
+ const signature = `${preset}:${color}:${opacity}:${emissive}`;
8525
+ if (signature === this.handStyleSignature && this.handMaterial) return;
8526
+ this.handStyleSignature = signature;
8527
+ this.handMaterial?.dispose();
8528
+ this.handMaterial = createHandMaterial(preset, {
8529
+ color,
8530
+ opacity,
8531
+ emissiveIntensity: emissive
8532
+ });
8533
+ }
8534
+ /**
8535
+ * The ray is meaningful only when it points at something. Authors can turn it
8536
+ * off entirely; otherwise it appears on hover/grab so it isn't permanent
8537
+ * clutter implying the whole scene is clickable.
8538
+ */
8539
+ shouldShowRay(interactor) {
8540
+ if (interactor && !interactor.showRay) return false;
8541
+ if (!this.interactionSystem) return true;
8542
+ return this.interactionSystem.getHoveredEntityId() !== null || this.interactionSystem.isGrabbing();
8543
+ }
8544
+ /** Lazily build per-slot visuals; the device reports profiles late. */
8545
+ ensureSlots(world) {
8546
+ const renderer = world.getRenderer();
8547
+ for (const { index } of SLOTS) {
8548
+ if (this.slots.has(index)) continue;
8178
8549
  const controller = renderer.xr.getController(index);
8179
8550
  const grip = renderer.xr.getControllerGrip(index);
8180
8551
  const hand = renderer.xr.getHand(index);
8181
8552
  if (!controller || !grip || !hand) continue;
8182
- controller.add(this.createRay());
8183
- this.group.add(controller);
8184
- grip.add(this.controllerModelFactory.createControllerModel(grip));
8185
- this.group.add(grip);
8186
- hand.add(this.handModelFactory.createHandModel(hand, "mesh"));
8187
- this.group.add(hand);
8188
- this.ready.add(index);
8189
- }
8190
- if (this.ready.size >= this.slots) {
8191
- this.built = true;
8553
+ const ray = this.createRay();
8554
+ controller.add(ray);
8555
+ const controllerModel = this.controllerModelFactory.createControllerModel(grip);
8556
+ grip.add(controllerModel);
8557
+ const handModel = this.handModelFactory.createHandModel(hand, "mesh");
8558
+ hand.add(handModel);
8559
+ this.group.add(controller, grip, hand);
8560
+ this.slots.set(index, {
8561
+ controller,
8562
+ grip,
8563
+ hand,
8564
+ ray,
8565
+ controllerModel,
8566
+ handModel,
8567
+ attachedCustom: null
8568
+ });
8192
8569
  }
8193
8570
  }
8194
8571
  /**
@@ -8216,6 +8593,7 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
8216
8593
  const line = new THREE14__namespace.Line(geometry, material);
8217
8594
  line.name = "XRPointerRay";
8218
8595
  line.renderOrder = 999;
8596
+ line.visible = false;
8219
8597
  return line;
8220
8598
  }
8221
8599
  dispose() {
@@ -8231,13 +8609,130 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
8231
8609
  }
8232
8610
  });
8233
8611
  this.group.clear();
8234
- this.ready.clear();
8235
- this.built = false;
8612
+ this.slots.clear();
8613
+ this.handMaterial?.dispose();
8614
+ this.handMaterial = null;
8615
+ this.handStyleSignature = "";
8236
8616
  this.attachedTo = null;
8237
8617
  }
8238
8618
  };
8239
8619
  __name(_XRControllerVisualSystem, "XRControllerVisualSystem");
8240
8620
  var XRControllerVisualSystem = _XRControllerVisualSystem;
8621
+ var MARKER_RADIUS = 0.28;
8622
+ var ACCENT = new THREE14__namespace.Color("#4da3ff");
8623
+ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
8624
+ constructor(locomotionSystem) {
8625
+ this.group = new THREE14__namespace.Group();
8626
+ this.arcGeometry = new THREE14__namespace.BufferGeometry();
8627
+ this.marker = new THREE14__namespace.Group();
8628
+ this.attached = false;
8629
+ this.locomotionSystem = locomotionSystem;
8630
+ this.group.name = "XRTeleportVisuals";
8631
+ this.group.userData.ecsPreviewHidden = true;
8632
+ const arcMaterial = new THREE14__namespace.LineBasicMaterial({
8633
+ vertexColors: true,
8634
+ transparent: true,
8635
+ depthTest: false
8636
+ });
8637
+ this.arc = new THREE14__namespace.Line(this.arcGeometry, arcMaterial);
8638
+ this.arc.frustumCulled = false;
8639
+ this.arc.renderOrder = 998;
8640
+ this.group.add(this.arc);
8641
+ this.marker.add(this.createRing(), this.createDot());
8642
+ this.marker.renderOrder = 999;
8643
+ this.group.add(this.marker);
8644
+ this.setVisible(false);
8645
+ }
8646
+ update(world) {
8647
+ if (!this.attached) {
8648
+ world.getScene().add(this.group);
8649
+ this.attached = true;
8650
+ }
8651
+ const curve = this.locomotionSystem.getAimCurve();
8652
+ const landing = this.locomotionSystem.getLandingPoint();
8653
+ if (!landing || curve.length < 2) {
8654
+ this.setVisible(false);
8655
+ return;
8656
+ }
8657
+ this.updateArcGeometry(curve);
8658
+ this.marker.position.copy(landing);
8659
+ this.marker.position.y += 0.01;
8660
+ this.setVisible(true);
8661
+ }
8662
+ updateArcGeometry(curve) {
8663
+ const count = curve.length;
8664
+ const positions = new Float32Array(count * 3);
8665
+ const colors = new Float32Array(count * 3);
8666
+ for (let i = 0; i < count; i += 1) {
8667
+ const point = curve[i];
8668
+ positions[i * 3] = point.x;
8669
+ positions[i * 3 + 1] = point.y;
8670
+ positions[i * 3 + 2] = point.z;
8671
+ const t = count > 1 ? i / (count - 1) : 0;
8672
+ const fade = 1 - t * 0.75;
8673
+ colors[i * 3] = ACCENT.r * fade;
8674
+ colors[i * 3 + 1] = ACCENT.g * fade;
8675
+ colors[i * 3 + 2] = ACCENT.b * fade;
8676
+ }
8677
+ this.arcGeometry.setAttribute(
8678
+ "position",
8679
+ new THREE14__namespace.BufferAttribute(positions, 3)
8680
+ );
8681
+ this.arcGeometry.setAttribute("color", new THREE14__namespace.BufferAttribute(colors, 3));
8682
+ this.arcGeometry.computeBoundingSphere();
8683
+ }
8684
+ createRing() {
8685
+ const geometry = new THREE14__namespace.RingGeometry(
8686
+ MARKER_RADIUS * 0.72,
8687
+ MARKER_RADIUS,
8688
+ 48
8689
+ );
8690
+ geometry.rotateX(-Math.PI / 2);
8691
+ return new THREE14__namespace.Mesh(
8692
+ geometry,
8693
+ new THREE14__namespace.MeshBasicMaterial({
8694
+ color: ACCENT,
8695
+ transparent: true,
8696
+ opacity: 0.9,
8697
+ side: THREE14__namespace.DoubleSide,
8698
+ depthTest: false
8699
+ })
8700
+ );
8701
+ }
8702
+ createDot() {
8703
+ const geometry = new THREE14__namespace.CircleGeometry(MARKER_RADIUS * 0.16, 24);
8704
+ geometry.rotateX(-Math.PI / 2);
8705
+ return new THREE14__namespace.Mesh(
8706
+ geometry,
8707
+ new THREE14__namespace.MeshBasicMaterial({
8708
+ color: ACCENT,
8709
+ transparent: true,
8710
+ opacity: 0.55,
8711
+ side: THREE14__namespace.DoubleSide,
8712
+ depthTest: false
8713
+ })
8714
+ );
8715
+ }
8716
+ setVisible(visible) {
8717
+ this.group.visible = visible;
8718
+ }
8719
+ dispose() {
8720
+ this.group.removeFromParent();
8721
+ this.group.traverse((child) => {
8722
+ const mesh = child;
8723
+ mesh.geometry?.dispose?.();
8724
+ const material = mesh.material;
8725
+ if (Array.isArray(material)) {
8726
+ for (const entry of material) entry.dispose?.();
8727
+ } else {
8728
+ material?.dispose?.();
8729
+ }
8730
+ });
8731
+ this.attached = false;
8732
+ }
8733
+ };
8734
+ __name(_XRTeleportVisualSystem, "XRTeleportVisualSystem");
8735
+ var XRTeleportVisualSystem = _XRTeleportVisualSystem;
8241
8736
 
8242
8737
  exports.AnimationSystem = AnimationSystem;
8243
8738
  exports.AudioSystem = AudioSystem;
@@ -8265,5 +8760,6 @@ exports.XRInputSystem = XRInputSystem;
8265
8760
  exports.XRInteractionSystem = XRInteractionSystem;
8266
8761
  exports.XRLocomotionSystem = XRLocomotionSystem;
8267
8762
  exports.XRSessionSystem = XRSessionSystem;
8763
+ exports.XRTeleportVisualSystem = XRTeleportVisualSystem;
8268
8764
  //# sourceMappingURL=index.cjs.map
8269
8765
  //# sourceMappingURL=index.cjs.map