@relu-interactives/spatial-ecs 0.3.17 → 0.3.19

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 (40) hide show
  1. package/dist/{XRHandPose-LR4EhboN.d.cts → XRHandPose-CLl4MiHx.d.cts} +24 -2
  2. package/dist/{XRHandPose-SZ7ou63b.d.ts → XRHandPose-CrzI57JA.d.ts} +24 -2
  3. package/dist/{chunk-PNQ7HDQC.js → chunk-D6KXWNYT.js} +3 -3
  4. package/dist/{chunk-PNQ7HDQC.js.map → chunk-D6KXWNYT.js.map} +1 -1
  5. package/dist/{chunk-3LDQ4GVZ.js → chunk-H6DQ7JRE.js} +51 -18
  6. package/dist/chunk-H6DQ7JRE.js.map +1 -0
  7. package/dist/{chunk-CE2CPZSP.js → chunk-JYKFLQB5.js} +155 -36
  8. package/dist/chunk-JYKFLQB5.js.map +1 -0
  9. package/dist/{chunk-UHJ4MDIM.js → chunk-P42OE3RU.js} +4 -4
  10. package/dist/{chunk-UHJ4MDIM.js.map → chunk-P42OE3RU.js.map} +1 -1
  11. package/dist/components/index.cjs +36 -10
  12. package/dist/components/index.cjs.map +1 -1
  13. package/dist/components/index.d.cts +3 -3
  14. package/dist/components/index.d.ts +3 -3
  15. package/dist/components/index.js +1 -1
  16. package/dist/docs/guide/vr-and-hand-poses.md +68 -1
  17. package/dist/factories/index.cjs +11 -0
  18. package/dist/factories/index.cjs.map +1 -1
  19. package/dist/factories/index.d.cts +2 -2
  20. package/dist/factories/index.d.ts +2 -2
  21. package/dist/factories/index.js +3 -3
  22. package/dist/{handPose-dFxqbFFY.d.cts → handPose-DlcTtqgx.d.cts} +26 -1
  23. package/dist/{handPose-dFxqbFFY.d.ts → handPose-DlcTtqgx.d.ts} +26 -1
  24. package/dist/{index-ekS4iypo.d.cts → index-3pNbH0iP.d.cts} +23 -2
  25. package/dist/{index-uKFxVCmr.d.ts → index-C_589fc4.d.ts} +23 -2
  26. package/dist/{index-ByiUoIoh.d.ts → index-JRcoel9F.d.ts} +3 -2
  27. package/dist/{index-BL5EWkwY.d.cts → index-g777AmFv.d.cts} +3 -2
  28. package/dist/index.cjs +199 -46
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +11 -8
  31. package/dist/index.d.ts +11 -8
  32. package/dist/index.js +8 -8
  33. package/dist/systems/index.cjs +190 -46
  34. package/dist/systems/index.cjs.map +1 -1
  35. package/dist/systems/index.d.cts +2 -2
  36. package/dist/systems/index.d.ts +2 -2
  37. package/dist/systems/index.js +3 -3
  38. package/package.json +1 -1
  39. package/dist/chunk-3LDQ4GVZ.js.map +0 -1
  40. package/dist/chunk-CE2CPZSP.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -2576,6 +2576,9 @@ var _XRRigComponent = class _XRRigComponent extends Component {
2576
2576
  this.snapTurn = options?.snapTurn ?? true;
2577
2577
  this.snapTurnDegrees = options?.snapTurnDegrees ?? 30;
2578
2578
  this.fallbackEyeHeight = options?.fallbackEyeHeight ?? 1.6;
2579
+ this.teleportArcColor = options?.teleportArcColor ?? "#4da3ff";
2580
+ this.teleportArcInvalidColor = options?.teleportArcInvalidColor ?? "#ff5a5a";
2581
+ this.teleportArcThickness = options?.teleportArcThickness ?? 0.012;
2579
2582
  }
2580
2583
  };
2581
2584
  __name(_XRRigComponent, "XRRigComponent");
@@ -2726,15 +2729,22 @@ function getHandPosePreset(id) {
2726
2729
  return cloneHandPose(preset);
2727
2730
  }
2728
2731
  __name(getHandPosePreset, "getHandPosePreset");
2732
+ function cloneTarget(target) {
2733
+ return {
2734
+ ...target,
2735
+ ...target.directions ? { directions: [...target.directions] } : {}
2736
+ };
2737
+ }
2738
+ __name(cloneTarget, "cloneTarget");
2729
2739
  function cloneHandPose(definition) {
2730
2740
  return {
2731
2741
  ...definition,
2732
2742
  fingers: {
2733
- thumb: { ...definition.fingers.thumb },
2734
- index: { ...definition.fingers.index },
2735
- middle: { ...definition.fingers.middle },
2736
- ring: { ...definition.fingers.ring },
2737
- pinky: { ...definition.fingers.pinky }
2743
+ thumb: cloneTarget(definition.fingers.thumb),
2744
+ index: cloneTarget(definition.fingers.index),
2745
+ middle: cloneTarget(definition.fingers.middle),
2746
+ ring: cloneTarget(definition.fingers.ring),
2747
+ pinky: cloneTarget(definition.fingers.pinky)
2738
2748
  },
2739
2749
  pinch: definition.pinch ? { ...definition.pinch } : void 0,
2740
2750
  orientation: definition.orientation ? { ...definition.orientation } : void 0
@@ -2743,22 +2753,27 @@ function cloneHandPose(definition) {
2743
2753
  __name(cloneHandPose, "cloneHandPose");
2744
2754
  function handPoseFromCurls(id, name, curls, options) {
2745
2755
  const tolerance = options?.tolerance ?? NARROW;
2746
- const target = /* @__PURE__ */ __name((curl) => ({
2747
- curl: Math.min(1, Math.max(0, curl)),
2748
- tolerance
2749
- }), "target");
2756
+ const target = /* @__PURE__ */ __name((finger, curl) => {
2757
+ const directions = options?.directions?.[finger];
2758
+ return {
2759
+ curl: Math.min(1, Math.max(0, curl)),
2760
+ tolerance,
2761
+ ...directions && directions.length > 0 ? { directions } : {}
2762
+ };
2763
+ }, "target");
2750
2764
  return {
2751
2765
  id,
2752
2766
  name,
2753
2767
  fingers: {
2754
- thumb: target(curls.thumb),
2755
- index: target(curls.index),
2756
- middle: target(curls.middle),
2757
- ring: target(curls.ring),
2758
- pinky: target(curls.pinky)
2768
+ thumb: target("thumb", curls.thumb),
2769
+ index: target("index", curls.index),
2770
+ middle: target("middle", curls.middle),
2771
+ ring: target("ring", curls.ring),
2772
+ pinky: target("pinky", curls.pinky)
2759
2773
  },
2760
2774
  pinch: options?.pinch,
2761
2775
  orientation: options?.orientation,
2776
+ directionToleranceDegrees: options?.directionToleranceDegrees,
2762
2777
  hand: options?.hand
2763
2778
  };
2764
2779
  }
@@ -2772,6 +2787,13 @@ var DEFAULT_POSE_CURLS = {
2772
2787
  ring: 0,
2773
2788
  pinky: 0
2774
2789
  };
2790
+ var DEFAULT_POSE_DIRECTIONS = {
2791
+ thumb: [],
2792
+ index: [],
2793
+ middle: [],
2794
+ ring: [],
2795
+ pinky: []
2796
+ };
2775
2797
  function createHandPoseEntry(partial) {
2776
2798
  return {
2777
2799
  id: partial.id,
@@ -2779,6 +2801,14 @@ function createHandPoseEntry(partial) {
2779
2801
  preset: partial.preset ?? null,
2780
2802
  curls: { ...DEFAULT_POSE_CURLS, ...partial.curls },
2781
2803
  tolerance: partial.tolerance ?? 0.25,
2804
+ directions: {
2805
+ thumb: [...partial.directions?.thumb ?? []],
2806
+ index: [...partial.directions?.index ?? []],
2807
+ middle: [...partial.directions?.middle ?? []],
2808
+ ring: [...partial.directions?.ring ?? []],
2809
+ pinky: [...partial.directions?.pinky ?? []]
2810
+ },
2811
+ directionTolerance: partial.directionTolerance ?? 45,
2782
2812
  hand: partial.hand ?? null,
2783
2813
  orientation: partial.orientation ?? null,
2784
2814
  minHoldSeconds: partial.minHoldSeconds ?? 0.15,
@@ -2801,7 +2831,9 @@ function handPoseDefinitionFor(entry, resolvePreset) {
2801
2831
  return handPoseFromCurls(entry.id, entry.name, entry.curls, {
2802
2832
  tolerance: entry.tolerance,
2803
2833
  orientation: entry.orientation ?? void 0,
2804
- hand: entry.hand ?? void 0
2834
+ hand: entry.hand ?? void 0,
2835
+ directions: entry.directions,
2836
+ directionToleranceDegrees: entry.directionTolerance
2805
2837
  });
2806
2838
  }
2807
2839
  __name(handPoseDefinitionFor, "handPoseDefinitionFor");
@@ -3957,6 +3989,7 @@ __export(components_exports, {
3957
3989
  DEFAULT_MESH_GEOMETRY_PARAMETERS: () => DEFAULT_MESH_GEOMETRY_PARAMETERS,
3958
3990
  DEFAULT_PHYSICS_STATE: () => DEFAULT_PHYSICS_STATE,
3959
3991
  DEFAULT_POSE_CURLS: () => DEFAULT_POSE_CURLS,
3992
+ DEFAULT_POSE_DIRECTIONS: () => DEFAULT_POSE_DIRECTIONS,
3960
3993
  DEFAULT_POSTPROCESSING_STATE: () => DEFAULT_POSTPROCESSING_STATE,
3961
3994
  DEFAULT_RIGIDBODY_STATE: () => DEFAULT_RIGIDBODY_STATE,
3962
3995
  EntityType: () => EntityType,
@@ -4831,6 +4864,15 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
4831
4864
  if (typeof sourceXrRig.snapTurnDegrees === "number") {
4832
4865
  rig.snapTurnDegrees = sourceXrRig.snapTurnDegrees;
4833
4866
  }
4867
+ if (typeof sourceXrRig.teleportArcColor === "string") {
4868
+ rig.teleportArcColor = sourceXrRig.teleportArcColor;
4869
+ }
4870
+ if (typeof sourceXrRig.teleportArcInvalidColor === "string") {
4871
+ rig.teleportArcInvalidColor = sourceXrRig.teleportArcInvalidColor;
4872
+ }
4873
+ if (typeof sourceXrRig.teleportArcThickness === "number") {
4874
+ rig.teleportArcThickness = sourceXrRig.teleportArcThickness;
4875
+ }
4834
4876
  if (typeof sourceXrRig.fallbackEyeHeight === "number") {
4835
4877
  rig.fallbackEyeHeight = sourceXrRig.fallbackEyeHeight;
4836
4878
  }
@@ -12625,24 +12667,33 @@ __name(_XRControllerVisualSystem, "XRControllerVisualSystem");
12625
12667
  var XRControllerVisualSystem = _XRControllerVisualSystem;
12626
12668
  var MARKER_RADIUS = 0.28;
12627
12669
  var ACCENT = new THREE51__namespace.Color("#4da3ff");
12628
- var BLOCKED = new THREE51__namespace.Color("#ff5a5a");
12670
+ var SEGMENT_AXIS = new THREE51__namespace.Vector3(0, 1, 0);
12671
+ var HEAD_CLEARANCE = 0.25;
12629
12672
  var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
12630
12673
  constructor(locomotionSystem) {
12631
12674
  this.group = new THREE51__namespace.Group();
12632
- this.arcGeometry = new THREE51__namespace.BufferGeometry();
12675
+ /** Holds the segment chain that draws the arc. */
12676
+ this.arc = new THREE51__namespace.Group();
12677
+ /**
12678
+ * Unit cylinder along +Y, reused by every segment.
12679
+ *
12680
+ * The arc was a `THREE.Line`, which WebGL always renders one pixel wide —
12681
+ * effectively invisible on a headset, and not thickenable at all. A chain of
12682
+ * scaled cylinders gives real thickness while rebuilding no geometry: each
12683
+ * frame only moves, rotates and scales meshes that already exist.
12684
+ */
12685
+ this.segmentGeometry = new THREE51__namespace.CylinderGeometry(1, 1, 1, 6, 1);
12686
+ this.segments = [];
12633
12687
  this.marker = new THREE51__namespace.Group();
12634
12688
  this.attached = false;
12689
+ this.scratchDirection = new THREE51__namespace.Vector3();
12690
+ this.scratchHead = new THREE51__namespace.Vector3();
12691
+ this.validColor = new THREE51__namespace.Color();
12692
+ this.invalidColor = new THREE51__namespace.Color();
12635
12693
  this.locomotionSystem = locomotionSystem;
12636
12694
  this.group.name = "XRTeleportVisuals";
12637
12695
  this.group.userData.ecsPreviewHidden = true;
12638
- const arcMaterial = new THREE51__namespace.LineBasicMaterial({
12639
- vertexColors: true,
12640
- transparent: true,
12641
- depthTest: false
12642
- });
12643
- this.arc = new THREE51__namespace.Line(this.arcGeometry, arcMaterial);
12644
- this.arc.frustumCulled = false;
12645
- this.arc.renderOrder = 998;
12696
+ this.arc.name = "XRTeleportArc";
12646
12697
  this.group.add(this.arc);
12647
12698
  this.marker.add(this.createRing(), this.createDot());
12648
12699
  this.marker.renderOrder = 999;
@@ -12654,15 +12705,27 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
12654
12705
  world.getScene().add(this.group);
12655
12706
  this.attached = true;
12656
12707
  }
12657
- const curve = this.locomotionSystem.getAimCurve();
12708
+ const raw = this.locomotionSystem.getAimCurve();
12658
12709
  const landing = this.locomotionSystem.getLandingPoint();
12659
12710
  const valid = this.locomotionSystem.isAimValid();
12711
+ if (raw.length < 2) {
12712
+ this.setVisible(false);
12713
+ return;
12714
+ }
12715
+ const curve = this.trimNearHead(world, raw);
12660
12716
  if (curve.length < 2) {
12661
12717
  this.setVisible(false);
12662
12718
  return;
12663
12719
  }
12664
- this.updateArcGeometry(curve, valid ? ACCENT : BLOCKED);
12720
+ const rig = this.resolveRig(world);
12721
+ const thickness = Math.min(
12722
+ 0.1,
12723
+ Math.max(1e-3, rig?.teleportArcThickness ?? 0.012)
12724
+ );
12725
+ const tint = valid ? this.validColor.set(rig?.teleportArcColor ?? "#4da3ff") : this.invalidColor.set(rig?.teleportArcInvalidColor ?? "#ff5a5a");
12726
+ this.updateArcGeometry(curve, tint, thickness);
12665
12727
  if (valid && landing) {
12728
+ this.tintMarker(tint);
12666
12729
  this.marker.position.copy(landing);
12667
12730
  this.marker.position.y += 0.01;
12668
12731
  this.marker.visible = true;
@@ -12671,27 +12734,66 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
12671
12734
  }
12672
12735
  this.setVisible(true);
12673
12736
  }
12674
- updateArcGeometry(curve, tint) {
12675
- const count = curve.length;
12676
- const positions = new Float32Array(count * 3);
12677
- const colors = new Float32Array(count * 3);
12678
- for (let i = 0; i < count; i += 1) {
12679
- const point = curve[i];
12680
- positions[i * 3] = point.x;
12681
- positions[i * 3 + 1] = point.y;
12682
- positions[i * 3 + 2] = point.z;
12683
- const t = count > 1 ? i / (count - 1) : 0;
12737
+ updateArcGeometry(curve, tint, thickness) {
12738
+ const needed = curve.length - 1;
12739
+ this.ensureSegments(needed);
12740
+ const radius = thickness / 2;
12741
+ for (let i = 0; i < this.segments.length; i += 1) {
12742
+ const segment = this.segments[i];
12743
+ if (i >= needed) {
12744
+ segment.visible = false;
12745
+ continue;
12746
+ }
12747
+ const from = curve[i];
12748
+ const to = curve[i + 1];
12749
+ this.scratchDirection.subVectors(to, from);
12750
+ const length = this.scratchDirection.length();
12751
+ if (length < 1e-6) {
12752
+ segment.visible = false;
12753
+ continue;
12754
+ }
12755
+ segment.visible = true;
12756
+ segment.position.copy(from).addScaledVector(this.scratchDirection, 0.5);
12757
+ segment.quaternion.setFromUnitVectors(
12758
+ SEGMENT_AXIS,
12759
+ this.scratchDirection.divideScalar(length)
12760
+ );
12761
+ segment.scale.set(radius, length, radius);
12762
+ const t = needed > 1 ? i / (needed - 1) : 0;
12684
12763
  const fade = 1 - t * 0.75;
12685
- colors[i * 3] = tint.r * fade;
12686
- colors[i * 3 + 1] = tint.g * fade;
12687
- colors[i * 3 + 2] = tint.b * fade;
12764
+ segment.material.color.copy(tint).multiplyScalar(fade);
12765
+ }
12766
+ }
12767
+ /** Grow the segment pool to cover the current arc. */
12768
+ ensureSegments(count) {
12769
+ while (this.segments.length < count) {
12770
+ const material = new THREE51__namespace.MeshBasicMaterial({
12771
+ transparent: true,
12772
+ depthTest: false,
12773
+ depthWrite: false
12774
+ });
12775
+ const mesh = new THREE51__namespace.Mesh(this.segmentGeometry, material);
12776
+ mesh.frustumCulled = false;
12777
+ mesh.renderOrder = 998;
12778
+ mesh.raycast = () => {
12779
+ };
12780
+ this.segments.push(mesh);
12781
+ this.arc.add(mesh);
12688
12782
  }
12689
- this.arcGeometry.setAttribute(
12690
- "position",
12691
- new THREE51__namespace.BufferAttribute(positions, 3)
12692
- );
12693
- this.arcGeometry.setAttribute("color", new THREE51__namespace.BufferAttribute(colors, 3));
12694
- this.arcGeometry.computeBoundingSphere();
12783
+ }
12784
+ /** Drop leading points that sit too close to the viewer's head. */
12785
+ trimNearHead(world, curve) {
12786
+ world.getCamera().getWorldPosition(this.scratchHead);
12787
+ let start = 0;
12788
+ while (start < curve.length - 2 && curve[start].distanceTo(this.scratchHead) < HEAD_CLEARANCE) {
12789
+ start += 1;
12790
+ }
12791
+ return start === 0 ? curve : curve.slice(start);
12792
+ }
12793
+ resolveRig(world) {
12794
+ const entityId = world.query(XRRigComponent)[0];
12795
+ if (typeof entityId !== "number") return null;
12796
+ return world.getComponent(entityId, XRRigComponent) ?? null;
12695
12797
  }
12696
12798
  createRing() {
12697
12799
  const geometry = new THREE51__namespace.RingGeometry(
@@ -12725,6 +12827,15 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
12725
12827
  })
12726
12828
  );
12727
12829
  }
12830
+ /** Recolour the landing marker to match the arc. */
12831
+ tintMarker(color) {
12832
+ this.marker.traverse((child) => {
12833
+ const material = child.material;
12834
+ if (material && !Array.isArray(material)) {
12835
+ material.color?.copy(color);
12836
+ }
12837
+ });
12838
+ }
12728
12839
  setVisible(visible) {
12729
12840
  this.group.visible = visible;
12730
12841
  }
@@ -12951,10 +13062,22 @@ function computeHandShape(joints, handedness) {
12951
13062
  const direction = new THREE51__namespace.Vector3().subVectors(thumbTip, thumbBase);
12952
13063
  if (direction.lengthSq() > 1e-12) thumbDirection = direction.normalize();
12953
13064
  }
13065
+ const fingerDirections = {};
13066
+ for (const finger of XR_FINGERS) {
13067
+ const base = joints[METACARPAL_JOINTS[finger]];
13068
+ const tip = joints[TIP_JOINTS[finger]];
13069
+ if (!base || !tip) {
13070
+ fingerDirections[finger] = null;
13071
+ continue;
13072
+ }
13073
+ const direction = new THREE51__namespace.Vector3().subVectors(tip, base);
13074
+ fingerDirections[finger] = direction.lengthSq() > 1e-12 ? direction.normalize() : null;
13075
+ }
12954
13076
  return {
12955
13077
  handedness,
12956
13078
  fingers: fingers2,
12957
13079
  pinch,
13080
+ fingerDirections,
12958
13081
  spread: {
12959
13082
  thumbIndex: computeSpread(joints, "thumb", "index") ?? 0,
12960
13083
  indexMiddle: computeSpread(joints, "index", "middle") ?? 0,
@@ -12979,6 +13102,7 @@ function scoreAgainst(actual, target, tolerance) {
12979
13102
  return clamp01(1 - (error - tolerance) / MATCH_FALLOFF);
12980
13103
  }
12981
13104
  __name(scoreAgainst, "scoreAgainst");
13105
+ var DEFAULT_DIRECTION_TOLERANCE = 45;
12982
13106
  var WORLD_DIRECTIONS = {
12983
13107
  up: new THREE51__namespace.Vector3(0, 1, 0),
12984
13108
  down: new THREE51__namespace.Vector3(0, -1, 0),
@@ -13006,6 +13130,22 @@ function resolveDirection(direction, shape, context) {
13006
13130
  return direction === "toward-head" ? toHead : toHead.multiplyScalar(-1);
13007
13131
  }
13008
13132
  __name(resolveDirection, "resolveDirection");
13133
+ function scoreDirections(actual, accepted, shape, toleranceDegrees, context) {
13134
+ if (!actual) return 0;
13135
+ let best = 0;
13136
+ for (const direction of accepted) {
13137
+ const wanted = resolveDirection(direction, shape, context);
13138
+ if (!wanted) continue;
13139
+ const angle = THREE51__namespace.MathUtils.radToDeg(
13140
+ Math.acos(THREE51__namespace.MathUtils.clamp(actual.dot(wanted), -1, 1))
13141
+ );
13142
+ const normalizedError = Math.max(0, angle - toleranceDegrees) / 90;
13143
+ const score = clamp01(1 - normalizedError / MATCH_FALLOFF);
13144
+ if (score > best) best = score;
13145
+ }
13146
+ return best;
13147
+ }
13148
+ __name(scoreDirections, "scoreDirections");
13009
13149
  function matchHandPose(shape, definition, context) {
13010
13150
  if (definition.hand && definition.hand !== shape.handedness) return NO_MATCH;
13011
13151
  let confidence = 1;
@@ -13035,6 +13175,18 @@ function matchHandPose(shape, definition, context) {
13035
13175
  `${finger} tip`
13036
13176
  );
13037
13177
  }
13178
+ if (target.directions && target.directions.length > 0) {
13179
+ consider(
13180
+ scoreDirections(
13181
+ shape.fingerDirections[finger],
13182
+ target.directions,
13183
+ shape,
13184
+ definition.directionToleranceDegrees ?? DEFAULT_DIRECTION_TOLERANCE,
13185
+ context
13186
+ ),
13187
+ `${finger} direction`
13188
+ );
13189
+ }
13038
13190
  }
13039
13191
  if (definition.pinch) {
13040
13192
  const { finger, target, tolerance } = definition.pinch;
@@ -14783,6 +14935,7 @@ exports.CURLED_COS = CURLED_COS;
14783
14935
  exports.CameraSyncSystem = CameraSyncSystem;
14784
14936
  exports.Component = Component;
14785
14937
  exports.CoreComponents = components_exports;
14938
+ exports.DEFAULT_DIRECTION_TOLERANCE = DEFAULT_DIRECTION_TOLERANCE;
14786
14939
  exports.DEFAULT_ENTER_CONFIDENCE = DEFAULT_ENTER_CONFIDENCE;
14787
14940
  exports.DEFAULT_EXIT_CONFIDENCE = DEFAULT_EXIT_CONFIDENCE;
14788
14941
  exports.DEFAULT_LOST_GRACE_SECONDS = DEFAULT_LOST_GRACE_SECONDS;