@relu-interactives/spatial-ecs 0.3.4 → 0.3.5

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.
@@ -6316,6 +6316,45 @@ var _XRInteractionSystem = class _XRInteractionSystem {
6316
6316
  };
6317
6317
  __name(_XRInteractionSystem, "XRInteractionSystem");
6318
6318
  var XRInteractionSystem = _XRInteractionSystem;
6319
+ var ARC_GRAVITY = 9.8;
6320
+ var ARC_SPEED = 7;
6321
+ var ARC_STEPS = 24;
6322
+ var ARC_MAX_TIME = 2;
6323
+ function sampleTeleportArc(origin, direction, out = [], options) {
6324
+ const speed = options?.speed ?? ARC_SPEED;
6325
+ const gravity = options?.gravity ?? ARC_GRAVITY;
6326
+ const steps = Math.max(2, options?.steps ?? ARC_STEPS);
6327
+ const maxTime = options?.maxTime ?? ARC_MAX_TIME;
6328
+ out.length = 0;
6329
+ const velocity = direction.clone().normalize().multiplyScalar(speed);
6330
+ for (let i = 0; i < steps; i += 1) {
6331
+ const t = i / (steps - 1) * maxTime;
6332
+ out.push(
6333
+ new THREE7.Vector3(
6334
+ origin.x + velocity.x * t,
6335
+ origin.y + velocity.y * t - 0.5 * gravity * t * t,
6336
+ origin.z + velocity.z * t
6337
+ )
6338
+ );
6339
+ }
6340
+ return out;
6341
+ }
6342
+ __name(sampleTeleportArc, "sampleTeleportArc");
6343
+ function arcGroundCrossing(points, groundY) {
6344
+ for (let i = 1; i < points.length; i += 1) {
6345
+ const previous = points[i - 1];
6346
+ const current = points[i];
6347
+ if (previous.y >= groundY && current.y <= groundY) {
6348
+ const span = previous.y - current.y;
6349
+ const alpha = span > 1e-6 ? (previous.y - groundY) / span : 0;
6350
+ return new THREE7.Vector3().lerpVectors(previous, current, alpha);
6351
+ }
6352
+ }
6353
+ return null;
6354
+ }
6355
+ __name(arcGroundCrossing, "arcGroundCrossing");
6356
+
6357
+ // core/systems/XRLocomotionSystem.ts
6319
6358
  var UP = new THREE7.Vector3(0, 1, 0);
6320
6359
  var AXIS_DEADZONE = 0.5;
6321
6360
  var _XRLocomotionSystem = class _XRLocomotionSystem {
@@ -6323,13 +6362,26 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
6323
6362
  this.raycaster = new THREE7.Raycaster();
6324
6363
  /** Per-source latch so one stick push yields exactly one snap turn. */
6325
6364
  this.turnLatched = /* @__PURE__ */ new WeakSet();
6326
- /** Where the pointer currently lands, for the consumer to draw a marker. */
6327
- this.teleportPoint = null;
6365
+ /** Reused arc buffer — aiming runs every frame. */
6366
+ this.arcPoints = [];
6367
+ /** Curve up to the landing point, for the visual system. Empty when not aiming. */
6368
+ this.aimCurve = [];
6369
+ this.landingPoint = null;
6370
+ /** True once we've tried to self-heal camera parenting for this session. */
6371
+ this.parentingChecked = false;
6328
6372
  this.inputSystem = inputSystem;
6329
6373
  }
6330
6374
  update(world, deltaTime) {
6331
- this.teleportPoint = null;
6332
- if (!world.isXrPresenting()) return;
6375
+ this.aimCurve = [];
6376
+ this.landingPoint = null;
6377
+ if (!world.isXrPresenting()) {
6378
+ this.parentingChecked = false;
6379
+ return;
6380
+ }
6381
+ if (!this.parentingChecked) {
6382
+ this.parentingChecked = true;
6383
+ this.ensureCameraUnderRig(world);
6384
+ }
6333
6385
  const rigEntityId = world.query(XRRigComponent, TransformComponent)[0];
6334
6386
  if (typeof rigEntityId !== "number") return;
6335
6387
  const rig = world.getComponent(rigEntityId, XRRigComponent);
@@ -6346,9 +6398,37 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
6346
6398
  this.applySmoothMove(world, rig, rigTransform, inputs, deltaTime);
6347
6399
  }
6348
6400
  }
6401
+ /**
6402
+ * Guarantee the render camera is a CHILD of the rig.
6403
+ *
6404
+ * Locomotion moves the rig, and three composes the head pose against
6405
+ * `camera.parent` — so if the camera sits at the scene root instead, moving
6406
+ * the rig does nothing at all and the player is frozen in place. Scenes
6407
+ * authored before the rig existed (or with the camera re-parented by hand)
6408
+ * would otherwise silently fail to move, which is exactly the symptom that is
6409
+ * hardest to diagnose from inside a headset. Repairing it via `ParentId` lets
6410
+ * `ObjectManagementSystem` reconcile the scene graph normally.
6411
+ */
6412
+ ensureCameraUnderRig(world) {
6413
+ const rigEntityId = world.query(XRRigComponent)[0];
6414
+ if (typeof rigEntityId !== "number") return;
6415
+ const activeCamera = world.getCamera();
6416
+ for (const entityId of world.query(CameraComponent, Object3DRef)) {
6417
+ if (world.getComponent(entityId, Object3DRef)?.object !== activeCamera) {
6418
+ continue;
6419
+ }
6420
+ const parent = world.getComponent(entityId, ParentId);
6421
+ if (parent) {
6422
+ if (parent.value !== rigEntityId) parent.value = rigEntityId;
6423
+ } else {
6424
+ world.addComponent(entityId, new ParentId(rigEntityId));
6425
+ }
6426
+ return;
6427
+ }
6428
+ }
6349
6429
  /**
6350
6430
  * Snap turning, not smooth turning — rotating the view continuously is the
6351
- * single biggest cause of VR nausea, so the comfort-safe option is the one
6431
+ * single biggest cause of VR nausea, so the comfort-safe option is what's
6352
6432
  * wired to the stick.
6353
6433
  */
6354
6434
  applySnapTurn(rig, transform, inputs) {
@@ -6362,8 +6442,7 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
6362
6442
  }
6363
6443
  if (this.turnLatched.has(source)) continue;
6364
6444
  this.turnLatched.add(source);
6365
- const step = THREE7.MathUtils.degToRad(rig.snapTurnDegrees);
6366
- transform.rotation.y -= Math.sign(x) * step;
6445
+ transform.rotation.y -= Math.sign(x) * THREE7.MathUtils.degToRad(rig.snapTurnDegrees);
6367
6446
  }
6368
6447
  }
6369
6448
  applySmoothMove(world, rig, transform, inputs, deltaTime) {
@@ -6388,17 +6467,42 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
6388
6467
  const targets = this.collectTeleportTargets(world);
6389
6468
  if (targets.length === 0) return;
6390
6469
  for (const input of inputs) {
6391
- this.raycaster.set(input.rayOrigin, input.rayDirection);
6392
- const hit = this.raycaster.intersectObjects(targets, true)[0];
6470
+ if (!input.select && !input.selectEnd) continue;
6471
+ const points = sampleTeleportArc(
6472
+ input.rayOrigin,
6473
+ input.rayDirection,
6474
+ this.arcPoints
6475
+ );
6476
+ const hit = this.raycastArc(points, targets);
6393
6477
  if (!hit) continue;
6394
- this.teleportPoint = hit.point.clone();
6478
+ this.landingPoint = hit.point;
6479
+ this.aimCurve = points.slice(0, hit.segmentIndex + 1).concat(hit.point);
6395
6480
  if (input.selectEnd) {
6396
6481
  transform.position.x = hit.point.x;
6397
6482
  transform.position.y = hit.point.y;
6398
6483
  transform.position.z = hit.point.z;
6484
+ this.aimCurve = [];
6485
+ this.landingPoint = null;
6399
6486
  }
6400
6487
  }
6401
6488
  }
6489
+ /** Walk the arc segment-by-segment, returning the first surface hit. */
6490
+ raycastArc(points, targets) {
6491
+ for (let i = 1; i < points.length; i += 1) {
6492
+ const from = points[i - 1];
6493
+ const to = points[i];
6494
+ const delta = new THREE7.Vector3().subVectors(to, from);
6495
+ const distance = delta.length();
6496
+ if (distance < 1e-6) continue;
6497
+ this.raycaster.set(from, delta.divideScalar(distance));
6498
+ this.raycaster.far = distance;
6499
+ const hit = this.raycaster.intersectObjects(targets, true)[0];
6500
+ if (hit) {
6501
+ return { point: hit.point.clone(), segmentIndex: i - 1 };
6502
+ }
6503
+ }
6504
+ return null;
6505
+ }
6402
6506
  collectTeleportTargets(world) {
6403
6507
  const objects = [];
6404
6508
  for (const entityId of world.query(VRTeleportAreaComponent, Object3DRef)) {
@@ -6410,12 +6514,13 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
6410
6514
  }
6411
6515
  return objects;
6412
6516
  }
6413
- /**
6414
- * Where the teleport pointer currently lands this frame, or null.
6415
- * Consumers use this to render an aim marker.
6416
- */
6417
- getTeleportPoint() {
6418
- return this.teleportPoint;
6517
+ /** Arc points up to the landing spot this frame; empty when not aiming. */
6518
+ getAimCurve() {
6519
+ return this.aimCurve;
6520
+ }
6521
+ /** Where the arc currently lands, or null when not aiming at a valid area. */
6522
+ getLandingPoint() {
6523
+ return this.landingPoint;
6419
6524
  }
6420
6525
  };
6421
6526
  __name(_XRLocomotionSystem, "XRLocomotionSystem");
@@ -6577,7 +6682,7 @@ var _VRTeleportAreaSystem = class _VRTeleportAreaSystem {
6577
6682
  __name(_VRTeleportAreaSystem, "VRTeleportAreaSystem");
6578
6683
  var VRTeleportAreaSystem = _VRTeleportAreaSystem;
6579
6684
  var _XRControllerVisualSystem = class _XRControllerVisualSystem {
6580
- constructor(slots = 2) {
6685
+ constructor(options) {
6581
6686
  this.controllerModelFactory = new XRControllerModelFactory();
6582
6687
  this.handModelFactory = new XRHandModelFactory();
6583
6688
  this.group = new THREE7.Group();
@@ -6585,7 +6690,9 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
6585
6690
  this.attachedTo = null;
6586
6691
  /** Index → whether we've already attached visuals for that slot. */
6587
6692
  this.ready = /* @__PURE__ */ new Set();
6588
- this.slots = slots;
6693
+ this.rays = [];
6694
+ this.slots = options?.slots ?? 2;
6695
+ this.interactionSystem = options?.interactionSystem ?? null;
6589
6696
  this.group.name = "XRControllerVisuals";
6590
6697
  this.group.userData.ecsPreviewHidden = true;
6591
6698
  }
@@ -6593,6 +6700,7 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
6593
6700
  const renderer = world.getRenderer();
6594
6701
  if (!renderer.xr?.enabled) return;
6595
6702
  this.ensureAttached(world);
6703
+ this.updateRayVisibility();
6596
6704
  if (this.built) return;
6597
6705
  for (let index = 0; index < this.slots; index += 1) {
6598
6706
  if (this.ready.has(index)) continue;
@@ -6600,7 +6708,9 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
6600
6708
  const grip = renderer.xr.getControllerGrip(index);
6601
6709
  const hand = renderer.xr.getHand(index);
6602
6710
  if (!controller || !grip || !hand) continue;
6603
- controller.add(this.createRay());
6711
+ const ray = this.createRay();
6712
+ this.rays.push(ray);
6713
+ controller.add(ray);
6604
6714
  this.group.add(controller);
6605
6715
  grip.add(this.controllerModelFactory.createControllerModel(grip));
6606
6716
  this.group.add(grip);
@@ -6612,6 +6722,17 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
6612
6722
  this.built = true;
6613
6723
  }
6614
6724
  }
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
+ }
6615
6736
  /**
6616
6737
  * Parent the visuals to the XR rig when there is one, so locomotion moves the
6617
6738
  * hands with the player. Falls back to the scene root.
@@ -6637,6 +6758,7 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
6637
6758
  const line = new THREE7.Line(geometry, material);
6638
6759
  line.name = "XRPointerRay";
6639
6760
  line.renderOrder = 999;
6761
+ line.visible = this.interactionSystem === null;
6640
6762
  return line;
6641
6763
  }
6642
6764
  dispose() {
@@ -6652,6 +6774,7 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
6652
6774
  }
6653
6775
  });
6654
6776
  this.group.clear();
6777
+ this.rays.length = 0;
6655
6778
  this.ready.clear();
6656
6779
  this.built = false;
6657
6780
  this.attachedTo = null;
@@ -6659,7 +6782,122 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
6659
6782
  };
6660
6783
  __name(_XRControllerVisualSystem, "XRControllerVisualSystem");
6661
6784
  var XRControllerVisualSystem = _XRControllerVisualSystem;
6785
+ var MARKER_RADIUS = 0.28;
6786
+ var ACCENT = new THREE7.Color("#4da3ff");
6787
+ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
6788
+ constructor(locomotionSystem) {
6789
+ this.group = new THREE7.Group();
6790
+ this.arcGeometry = new THREE7.BufferGeometry();
6791
+ this.marker = new THREE7.Group();
6792
+ this.attached = false;
6793
+ this.locomotionSystem = locomotionSystem;
6794
+ this.group.name = "XRTeleportVisuals";
6795
+ this.group.userData.ecsPreviewHidden = true;
6796
+ const arcMaterial = new THREE7.LineBasicMaterial({
6797
+ vertexColors: true,
6798
+ transparent: true,
6799
+ depthTest: false
6800
+ });
6801
+ this.arc = new THREE7.Line(this.arcGeometry, arcMaterial);
6802
+ this.arc.frustumCulled = false;
6803
+ this.arc.renderOrder = 998;
6804
+ this.group.add(this.arc);
6805
+ this.marker.add(this.createRing(), this.createDot());
6806
+ this.marker.renderOrder = 999;
6807
+ this.group.add(this.marker);
6808
+ this.setVisible(false);
6809
+ }
6810
+ update(world) {
6811
+ if (!this.attached) {
6812
+ world.getScene().add(this.group);
6813
+ this.attached = true;
6814
+ }
6815
+ const curve = this.locomotionSystem.getAimCurve();
6816
+ const landing = this.locomotionSystem.getLandingPoint();
6817
+ if (!landing || curve.length < 2) {
6818
+ this.setVisible(false);
6819
+ return;
6820
+ }
6821
+ this.updateArcGeometry(curve);
6822
+ this.marker.position.copy(landing);
6823
+ this.marker.position.y += 0.01;
6824
+ this.setVisible(true);
6825
+ }
6826
+ updateArcGeometry(curve) {
6827
+ const count = curve.length;
6828
+ const positions = new Float32Array(count * 3);
6829
+ const colors = new Float32Array(count * 3);
6830
+ for (let i = 0; i < count; i += 1) {
6831
+ const point = curve[i];
6832
+ positions[i * 3] = point.x;
6833
+ positions[i * 3 + 1] = point.y;
6834
+ positions[i * 3 + 2] = point.z;
6835
+ const t = count > 1 ? i / (count - 1) : 0;
6836
+ 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;
6840
+ }
6841
+ this.arcGeometry.setAttribute(
6842
+ "position",
6843
+ new THREE7.BufferAttribute(positions, 3)
6844
+ );
6845
+ this.arcGeometry.setAttribute("color", new THREE7.BufferAttribute(colors, 3));
6846
+ this.arcGeometry.computeBoundingSphere();
6847
+ }
6848
+ createRing() {
6849
+ const geometry = new THREE7.RingGeometry(
6850
+ MARKER_RADIUS * 0.72,
6851
+ MARKER_RADIUS,
6852
+ 48
6853
+ );
6854
+ geometry.rotateX(-Math.PI / 2);
6855
+ return new THREE7.Mesh(
6856
+ geometry,
6857
+ new THREE7.MeshBasicMaterial({
6858
+ color: ACCENT,
6859
+ transparent: true,
6860
+ opacity: 0.9,
6861
+ side: THREE7.DoubleSide,
6862
+ depthTest: false
6863
+ })
6864
+ );
6865
+ }
6866
+ createDot() {
6867
+ const geometry = new THREE7.CircleGeometry(MARKER_RADIUS * 0.16, 24);
6868
+ geometry.rotateX(-Math.PI / 2);
6869
+ return new THREE7.Mesh(
6870
+ geometry,
6871
+ new THREE7.MeshBasicMaterial({
6872
+ color: ACCENT,
6873
+ transparent: true,
6874
+ opacity: 0.55,
6875
+ side: THREE7.DoubleSide,
6876
+ depthTest: false
6877
+ })
6878
+ );
6879
+ }
6880
+ setVisible(visible) {
6881
+ this.group.visible = visible;
6882
+ }
6883
+ dispose() {
6884
+ this.group.removeFromParent();
6885
+ this.group.traverse((child) => {
6886
+ const mesh = child;
6887
+ mesh.geometry?.dispose?.();
6888
+ const material = mesh.material;
6889
+ if (Array.isArray(material)) {
6890
+ for (const entry of material) entry.dispose?.();
6891
+ } else {
6892
+ material?.dispose?.();
6893
+ }
6894
+ });
6895
+ this.attached = false;
6896
+ }
6897
+ };
6898
+ __name(_XRTeleportVisualSystem, "XRTeleportVisualSystem");
6899
+ var XRTeleportVisualSystem = _XRTeleportVisualSystem;
6662
6900
 
6663
- export { ADDED_NODE_MARKER_COMPONENT, AnimationSystem, AudioSystem, Behaviour, CameraSyncSystem, EnvironmentSyncSystem, HYDRATABLE_ENTITY_KINDS, InputSystem, InstancedMeshSystem, KeyboardKeys, LightSyncSystem, MaterialSyncSystem, MeshGeometrySystem, NODE_KEY_SEPARATOR, NO_XR_CAPABILITIES, OVERRIDE_DIFF_EXCLUDED_COMPONENTS, OVERRIDE_ROOT_EXCLUDED_COMPONENTS, ObjectVisibilitySystem, PREFAB_FILE_VERSION, PREFAB_FILE_VERSION_NESTED, PhysicsSyncSystem, PostprocessingSyncSystem, RenderSystem, SCRIPT_OBJECT_ROOT_FLAG, ScriptBehaviourSystem, ScriptObject, ScriptObjectSystem, TransformSyncSystem, VRTeleportAreaSystem, VideoSystem, XRControllerVisualSystem, XRInputSystem, XRInteractionSystem, XRLocomotionSystem, XRSessionSystem, buildComponentValueMaps, buildNodeKeyMapsFromSnapshot, buildNodeMapsFromSnapshot, canonicalizeComponentValue, clearPrefabCache, collectDiffPaths, collectEntitySubtree, collectSnapshotSubtreeIds, computeInstanceOverrides, decanonicalizeComponentValue, despawnPrefab, encodeOverride, findSnapshotRootEntityIds, findTopLevelNestedRegions, groupOverridePaths, hydrateWorldFromSnapshot, hydrateWorldFromSnapshotDetailed, indexSnapshotByEntity, instantiateSubtree, loadPrefab, mergeComponentValue, parseOverride, parsePrefabAsset, prefabFileVersionFor, readXrCapabilities, restoreComponentState, restorePostCreateComponents, setPrefabAssetUrlResolver, spawnPrefab };
6664
- //# sourceMappingURL=chunk-ID266ZUA.js.map
6665
- //# sourceMappingURL=chunk-ID266ZUA.js.map
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