@relu-interactives/spatial-ecs 0.3.6 → 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.
@@ -434,10 +434,22 @@ declare class XRLocomotionSystem implements EcsSystem {
434
434
  /** Curve up to the landing point, for the visual system. Empty when not aiming. */
435
435
  private aimCurve;
436
436
  private landingPoint;
437
+ /** True when the current aim lands somewhere the player may actually stand. */
438
+ private aimValid;
439
+ /** Sources currently aiming via thumbstick (hysteresis, so it doesn't chatter). */
440
+ private readonly stickAiming;
437
441
  /** True once we've tried to self-heal camera parenting for this session. */
438
442
  private parentingChecked;
439
443
  constructor(inputSystem: XRInputSystem);
440
444
  update(world: World, deltaTime: number): void;
445
+ /**
446
+ * Name what is missing, once per session.
447
+ *
448
+ * Inside a headset there is no console and no inspector, so a scene that
449
+ * simply cannot move gives the author nothing to go on. These messages are
450
+ * visible over remote debugging and say exactly which entity to add.
451
+ */
452
+ private warnOnIncompleteSetup;
441
453
  /**
442
454
  * Guarantee the render camera is a CHILD of the rig.
443
455
  *
@@ -458,11 +470,21 @@ declare class XRLocomotionSystem implements EcsSystem {
458
470
  private applySnapTurn;
459
471
  private applySmoothMove;
460
472
  private applyTeleport;
473
+ /**
474
+ * Thumbstick-forward aiming with hysteresis.
475
+ *
476
+ * Engaging at 0.6 and releasing at 0.3 stops a stick resting near the
477
+ * threshold from flickering the arc on and off — and the release edge is what
478
+ * commits the teleport, so chatter there would teleport the player randomly.
479
+ */
480
+ private readStickAim;
461
481
  /** Walk the arc segment-by-segment, returning the first surface hit. */
462
482
  private raycastArc;
463
483
  private collectTeleportTargets;
464
484
  /** Arc points up to the landing spot this frame; empty when not aiming. */
465
485
  getAimCurve(): THREE.Vector3[];
486
+ /** True when the aim lands on a valid teleport area. */
487
+ isAimValid(): boolean;
466
488
  /** Where the arc currently lands, or null when not aiming at a valid area. */
467
489
  getLandingPoint(): THREE.Vector3 | null;
468
490
  }
@@ -434,10 +434,22 @@ declare class XRLocomotionSystem implements EcsSystem {
434
434
  /** Curve up to the landing point, for the visual system. Empty when not aiming. */
435
435
  private aimCurve;
436
436
  private landingPoint;
437
+ /** True when the current aim lands somewhere the player may actually stand. */
438
+ private aimValid;
439
+ /** Sources currently aiming via thumbstick (hysteresis, so it doesn't chatter). */
440
+ private readonly stickAiming;
437
441
  /** True once we've tried to self-heal camera parenting for this session. */
438
442
  private parentingChecked;
439
443
  constructor(inputSystem: XRInputSystem);
440
444
  update(world: World, deltaTime: number): void;
445
+ /**
446
+ * Name what is missing, once per session.
447
+ *
448
+ * Inside a headset there is no console and no inspector, so a scene that
449
+ * simply cannot move gives the author nothing to go on. These messages are
450
+ * visible over remote debugging and say exactly which entity to add.
451
+ */
452
+ private warnOnIncompleteSetup;
441
453
  /**
442
454
  * Guarantee the render camera is a CHILD of the rig.
443
455
  *
@@ -458,11 +470,21 @@ declare class XRLocomotionSystem implements EcsSystem {
458
470
  private applySnapTurn;
459
471
  private applySmoothMove;
460
472
  private applyTeleport;
473
+ /**
474
+ * Thumbstick-forward aiming with hysteresis.
475
+ *
476
+ * Engaging at 0.6 and releasing at 0.3 stops a stick resting near the
477
+ * threshold from flickering the arc on and off — and the release edge is what
478
+ * commits the teleport, so chatter there would teleport the player randomly.
479
+ */
480
+ private readStickAim;
461
481
  /** Walk the arc segment-by-segment, returning the first surface hit. */
462
482
  private raycastArc;
463
483
  private collectTeleportTargets;
464
484
  /** Arc points up to the landing spot this frame; empty when not aiming. */
465
485
  getAimCurve(): THREE.Vector3[];
486
+ /** True when the aim lands on a valid teleport area. */
487
+ isAimValid(): boolean;
466
488
  /** Where the arc currently lands, or null when not aiming at a valid area. */
467
489
  getLandingPoint(): THREE.Vector3 | null;
468
490
  }
package/dist/index.cjs CHANGED
@@ -10697,6 +10697,10 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
10697
10697
  /** Curve up to the landing point, for the visual system. Empty when not aiming. */
10698
10698
  this.aimCurve = [];
10699
10699
  this.landingPoint = null;
10700
+ /** True when the current aim lands somewhere the player may actually stand. */
10701
+ this.aimValid = false;
10702
+ /** Sources currently aiming via thumbstick (hysteresis, so it doesn't chatter). */
10703
+ this.stickAiming = /* @__PURE__ */ new WeakSet();
10700
10704
  /** True once we've tried to self-heal camera parenting for this session. */
10701
10705
  this.parentingChecked = false;
10702
10706
  this.inputSystem = inputSystem;
@@ -10704,6 +10708,7 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
10704
10708
  update(world, deltaTime) {
10705
10709
  this.aimCurve = [];
10706
10710
  this.landingPoint = null;
10711
+ this.aimValid = false;
10707
10712
  if (!world.isXrPresenting()) {
10708
10713
  this.parentingChecked = false;
10709
10714
  return;
@@ -10711,6 +10716,7 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
10711
10716
  if (!this.parentingChecked) {
10712
10717
  this.parentingChecked = true;
10713
10718
  this.ensureCameraUnderRig(world);
10719
+ this.warnOnIncompleteSetup(world);
10714
10720
  }
10715
10721
  const rigEntityId = world.query(XRRigComponent, TransformComponent)[0];
10716
10722
  if (typeof rigEntityId !== "number") return;
@@ -10728,6 +10734,26 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
10728
10734
  this.applySmoothMove(world, rig, rigTransform, inputs, deltaTime);
10729
10735
  }
10730
10736
  }
10737
+ /**
10738
+ * Name what is missing, once per session.
10739
+ *
10740
+ * Inside a headset there is no console and no inspector, so a scene that
10741
+ * simply cannot move gives the author nothing to go on. These messages are
10742
+ * visible over remote debugging and say exactly which entity to add.
10743
+ */
10744
+ warnOnIncompleteSetup(world) {
10745
+ if (typeof world.query(XRRigComponent)[0] !== "number") {
10746
+ console.warn(
10747
+ "[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)."
10748
+ );
10749
+ return;
10750
+ }
10751
+ if (world.query(VRTeleportAreaComponent).length === 0) {
10752
+ console.warn(
10753
+ "[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."
10754
+ );
10755
+ }
10756
+ }
10731
10757
  /**
10732
10758
  * Guarantee the render camera is a CHILD of the rig.
10733
10759
  *
@@ -10795,29 +10821,60 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
10795
10821
  }
10796
10822
  applyTeleport(world, transform, inputs) {
10797
10823
  const targets = this.collectTeleportTargets(world);
10798
- if (targets.length === 0) return;
10799
10824
  for (const input of inputs) {
10800
- if (!input.select && !input.selectEnd) continue;
10825
+ const source = input;
10826
+ const stick = this.readStickAim(input, source);
10827
+ const aiming = input.select || stick.aiming;
10828
+ const commit = input.selectEnd || stick.released;
10829
+ if (!aiming && !commit) continue;
10801
10830
  const points = sampleTeleportArc(
10802
10831
  input.rayOrigin,
10803
10832
  input.rayDirection,
10804
10833
  this.arcPoints
10805
10834
  );
10806
10835
  const hit = this.raycastArc(points, targets);
10807
- if (!hit) continue;
10808
- this.landingPoint = hit.point;
10809
- this.aimCurve = points.slice(0, hit.segmentIndex + 1).concat(hit.point);
10810
- if (input.selectEnd) {
10836
+ this.aimCurve = hit ? points.slice(0, hit.segmentIndex + 1).concat(hit.point) : points.slice();
10837
+ this.landingPoint = hit ? hit.point : null;
10838
+ this.aimValid = hit !== null;
10839
+ if (commit && hit) {
10811
10840
  transform.position.x = hit.point.x;
10812
10841
  transform.position.y = hit.point.y;
10813
10842
  transform.position.z = hit.point.z;
10814
10843
  this.aimCurve = [];
10815
10844
  this.landingPoint = null;
10845
+ this.aimValid = false;
10846
+ } else if (commit) {
10847
+ this.aimCurve = [];
10848
+ this.aimValid = false;
10816
10849
  }
10817
10850
  }
10818
10851
  }
10852
+ /**
10853
+ * Thumbstick-forward aiming with hysteresis.
10854
+ *
10855
+ * Engaging at 0.6 and releasing at 0.3 stops a stick resting near the
10856
+ * threshold from flickering the arc on and off — and the release edge is what
10857
+ * commits the teleport, so chatter there would teleport the player randomly.
10858
+ */
10859
+ readStickAim(input, source) {
10860
+ if (!input.hasAxes || input.axes.length < 4) {
10861
+ return { aiming: false, released: false };
10862
+ }
10863
+ const forward = -(input.axes[3] ?? 0);
10864
+ const wasAiming = this.stickAiming.has(source);
10865
+ if (!wasAiming && forward > 0.6) {
10866
+ this.stickAiming.add(source);
10867
+ return { aiming: true, released: false };
10868
+ }
10869
+ if (wasAiming && forward < 0.3) {
10870
+ this.stickAiming.delete(source);
10871
+ return { aiming: false, released: true };
10872
+ }
10873
+ return { aiming: wasAiming, released: false };
10874
+ }
10819
10875
  /** Walk the arc segment-by-segment, returning the first surface hit. */
10820
10876
  raycastArc(points, targets) {
10877
+ if (targets.length === 0) return null;
10821
10878
  for (let i = 1; i < points.length; i += 1) {
10822
10879
  const from = points[i - 1];
10823
10880
  const to = points[i];
@@ -10848,6 +10905,10 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
10848
10905
  getAimCurve() {
10849
10906
  return this.aimCurve;
10850
10907
  }
10908
+ /** True when the aim lands on a valid teleport area. */
10909
+ isAimValid() {
10910
+ return this.aimValid;
10911
+ }
10851
10912
  /** Where the arc currently lands, or null when not aiming at a valid area. */
10852
10913
  getLandingPoint() {
10853
10914
  return this.landingPoint;
@@ -11294,6 +11355,7 @@ __name(_XRControllerVisualSystem, "XRControllerVisualSystem");
11294
11355
  var XRControllerVisualSystem = _XRControllerVisualSystem;
11295
11356
  var MARKER_RADIUS = 0.28;
11296
11357
  var ACCENT = new THREE39__namespace.Color("#4da3ff");
11358
+ var BLOCKED = new THREE39__namespace.Color("#ff5a5a");
11297
11359
  var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
11298
11360
  constructor(locomotionSystem) {
11299
11361
  this.group = new THREE39__namespace.Group();
@@ -11324,16 +11386,22 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
11324
11386
  }
11325
11387
  const curve = this.locomotionSystem.getAimCurve();
11326
11388
  const landing = this.locomotionSystem.getLandingPoint();
11327
- if (!landing || curve.length < 2) {
11389
+ const valid = this.locomotionSystem.isAimValid();
11390
+ if (curve.length < 2) {
11328
11391
  this.setVisible(false);
11329
11392
  return;
11330
11393
  }
11331
- this.updateArcGeometry(curve);
11332
- this.marker.position.copy(landing);
11333
- this.marker.position.y += 0.01;
11394
+ this.updateArcGeometry(curve, valid ? ACCENT : BLOCKED);
11395
+ if (valid && landing) {
11396
+ this.marker.position.copy(landing);
11397
+ this.marker.position.y += 0.01;
11398
+ this.marker.visible = true;
11399
+ } else {
11400
+ this.marker.visible = false;
11401
+ }
11334
11402
  this.setVisible(true);
11335
11403
  }
11336
- updateArcGeometry(curve) {
11404
+ updateArcGeometry(curve, tint) {
11337
11405
  const count = curve.length;
11338
11406
  const positions = new Float32Array(count * 3);
11339
11407
  const colors = new Float32Array(count * 3);
@@ -11344,9 +11412,9 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
11344
11412
  positions[i * 3 + 2] = point.z;
11345
11413
  const t = count > 1 ? i / (count - 1) : 0;
11346
11414
  const fade = 1 - t * 0.75;
11347
- colors[i * 3] = ACCENT.r * fade;
11348
- colors[i * 3 + 1] = ACCENT.g * fade;
11349
- colors[i * 3 + 2] = ACCENT.b * fade;
11415
+ colors[i * 3] = tint.r * fade;
11416
+ colors[i * 3 + 1] = tint.g * fade;
11417
+ colors[i * 3 + 2] = tint.b * fade;
11350
11418
  }
11351
11419
  this.arcGeometry.setAttribute(
11352
11420
  "position",