@relu-interactives/spatial-ecs 0.3.6 → 0.3.8

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 (47) hide show
  1. package/dist/{System-TmC2EijF.d.cts → System-31wk-A0g.d.cts} +1 -1
  2. package/dist/{System-TmC2EijF.d.ts → System-31wk-A0g.d.ts} +1 -1
  3. package/dist/{XRInteractor-CCUzSqg9.d.ts → XRHandPose-CxPz8VBp.d.ts} +139 -20
  4. package/dist/{XRInteractor-I2q6TKtF.d.cts → XRHandPose-VuSbe4iz.d.cts} +139 -20
  5. package/dist/{chunk-FQTOC7HK.js → chunk-KIKIWCEB.js} +14 -5
  6. package/dist/chunk-KIKIWCEB.js.map +1 -0
  7. package/dist/{chunk-N4GKZ7IV.js → chunk-KVGGAL5V.js} +227 -3
  8. package/dist/chunk-KVGGAL5V.js.map +1 -0
  9. package/dist/{chunk-JRJ3CL5H.js → chunk-L5BP727H.js} +3 -3
  10. package/dist/{chunk-JRJ3CL5H.js.map → chunk-L5BP727H.js.map} +1 -1
  11. package/dist/{chunk-WRJ2K3LZ.js → chunk-LMTB4KQ4.js} +1011 -56
  12. package/dist/chunk-LMTB4KQ4.js.map +1 -0
  13. package/dist/components/index.cjs +98 -0
  14. package/dist/components/index.cjs.map +1 -1
  15. package/dist/components/index.d.cts +4 -3
  16. package/dist/components/index.d.ts +4 -3
  17. package/dist/components/index.js +1 -1
  18. package/dist/docs/guide/vr-and-hand-poses.md +111 -0
  19. package/dist/factories/index.cjs +58 -0
  20. package/dist/factories/index.cjs.map +1 -1
  21. package/dist/factories/index.d.cts +6 -3
  22. package/dist/factories/index.d.ts +6 -3
  23. package/dist/factories/index.js +3 -3
  24. package/dist/handPose-dFxqbFFY.d.cts +240 -0
  25. package/dist/handPose-dFxqbFFY.d.ts +240 -0
  26. package/dist/{index-W8pUajCW.d.ts → index-BIQoaKAA.d.cts} +9 -3
  27. package/dist/{index-BltjPEhg.d.cts → index-BxdXgqiI.d.ts} +9 -3
  28. package/dist/{index-BpbmZuOz.d.ts → index-C7XQyrgE.d.ts} +101 -7
  29. package/dist/{index-C5FrDt3z.d.cts → index-DyN50JZo.d.cts} +101 -7
  30. package/dist/index.cjs +1269 -51
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +104 -9
  33. package/dist/index.d.ts +104 -9
  34. package/dist/index.js +8 -8
  35. package/dist/systems/index.cjs +1226 -51
  36. package/dist/systems/index.cjs.map +1 -1
  37. package/dist/systems/index.d.cts +3 -2
  38. package/dist/systems/index.d.ts +3 -2
  39. package/dist/systems/index.js +3 -3
  40. package/dist/types.d.cts +1 -1
  41. package/dist/types.d.ts +1 -1
  42. package/dist/worldSnapshotUtils.d.cts +1 -1
  43. package/dist/worldSnapshotUtils.d.ts +1 -1
  44. package/package.json +1 -1
  45. package/dist/chunk-FQTOC7HK.js.map +0 -1
  46. package/dist/chunk-N4GKZ7IV.js.map +0 -1
  47. package/dist/chunk-WRJ2K3LZ.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -2611,6 +2611,218 @@ function createVRTeleportAreaComponent(world, entityId, ...args) {
2611
2611
  }
2612
2612
  __name(createVRTeleportAreaComponent, "createVRTeleportAreaComponent");
2613
2613
 
2614
+ // core/xr/handPosePresets.ts
2615
+ var WIDE = 0.35;
2616
+ var NARROW = 0.25;
2617
+ var EXTENDED = { curl: 0.05, tolerance: WIDE };
2618
+ var CURLED = { curl: 0.95, tolerance: WIDE };
2619
+ var THUMB_TUCKED = { curl: 0.7, tolerance: NARROW + 0.1 };
2620
+ var THUMB_RELAXED = { curl: 0.6, tolerance: 0.4 };
2621
+ function fingers(thumb, index, middle, ring, pinky) {
2622
+ return { thumb, index, middle, ring, pinky };
2623
+ }
2624
+ __name(fingers, "fingers");
2625
+ var THUMB_UP = {
2626
+ axis: "thumb",
2627
+ direction: "up",
2628
+ toleranceDegrees: 45
2629
+ };
2630
+ var THUMB_DOWN = {
2631
+ axis: "thumb",
2632
+ direction: "down",
2633
+ toleranceDegrees: 45
2634
+ };
2635
+ var HAND_POSE_PRESETS = {
2636
+ fist: {
2637
+ id: "fist",
2638
+ name: "Fist",
2639
+ fingers: fingers(THUMB_TUCKED, CURLED, CURLED, CURLED, CURLED)
2640
+ },
2641
+ "open-palm": {
2642
+ id: "open-palm",
2643
+ name: "Open Palm",
2644
+ fingers: fingers(EXTENDED, EXTENDED, EXTENDED, EXTENDED, EXTENDED)
2645
+ },
2646
+ point: {
2647
+ id: "point",
2648
+ name: "Point",
2649
+ fingers: fingers(THUMB_RELAXED, EXTENDED, CURLED, CURLED, CURLED)
2650
+ },
2651
+ "thumbs-up": {
2652
+ id: "thumbs-up",
2653
+ name: "Thumbs Up",
2654
+ fingers: fingers(EXTENDED, CURLED, CURLED, CURLED, CURLED),
2655
+ orientation: THUMB_UP
2656
+ },
2657
+ "thumbs-down": {
2658
+ id: "thumbs-down",
2659
+ name: "Thumbs Down",
2660
+ fingers: fingers(EXTENDED, CURLED, CURLED, CURLED, CURLED),
2661
+ orientation: THUMB_DOWN
2662
+ },
2663
+ peace: {
2664
+ id: "peace",
2665
+ name: "Peace",
2666
+ fingers: fingers(THUMB_RELAXED, EXTENDED, EXTENDED, CURLED, CURLED)
2667
+ },
2668
+ pinch: {
2669
+ id: "pinch",
2670
+ name: "Pinch",
2671
+ // Curl is left loose: what defines a pinch is the tips MEETING, and people
2672
+ // pinch with wildly different amounts of finger bend.
2673
+ fingers: fingers(
2674
+ { curl: 0.5, tolerance: 0.5 },
2675
+ { curl: 0.5, tolerance: 0.5 },
2676
+ { curl: 0.5, tolerance: 0.5 },
2677
+ { curl: 0.5, tolerance: 0.5 },
2678
+ { curl: 0.5, tolerance: 0.5 }
2679
+ ),
2680
+ pinch: { finger: "index", target: 1, tolerance: 0.25 }
2681
+ },
2682
+ ok: {
2683
+ id: "ok",
2684
+ name: "OK Sign",
2685
+ // Thumb and index meet in a ring while the rest stay up — that contrast with
2686
+ // a plain pinch is the whole gesture, so the other three are constrained.
2687
+ fingers: fingers(
2688
+ { curl: 0.5, tolerance: 0.4 },
2689
+ { curl: 0.55, tolerance: 0.35 },
2690
+ EXTENDED,
2691
+ EXTENDED,
2692
+ EXTENDED
2693
+ ),
2694
+ pinch: { finger: "index", target: 1, tolerance: 0.3 }
2695
+ },
2696
+ gun: {
2697
+ id: "gun",
2698
+ name: "Gun",
2699
+ fingers: fingers(EXTENDED, EXTENDED, CURLED, CURLED, CURLED)
2700
+ },
2701
+ rock: {
2702
+ id: "rock",
2703
+ name: "Rock Horns",
2704
+ fingers: fingers(THUMB_TUCKED, EXTENDED, CURLED, CURLED, EXTENDED)
2705
+ },
2706
+ shaka: {
2707
+ id: "shaka",
2708
+ name: "Shaka",
2709
+ fingers: fingers(EXTENDED, CURLED, CURLED, CURLED, EXTENDED)
2710
+ },
2711
+ "palm-up": {
2712
+ id: "palm-up",
2713
+ name: "Palm Up",
2714
+ fingers: fingers(EXTENDED, EXTENDED, EXTENDED, EXTENDED, EXTENDED),
2715
+ orientation: {
2716
+ axis: "palm-normal",
2717
+ direction: "up",
2718
+ toleranceDegrees: 40
2719
+ }
2720
+ }
2721
+ };
2722
+ var HAND_POSE_PRESET_IDS = Object.keys(HAND_POSE_PRESETS);
2723
+ function getHandPosePreset(id) {
2724
+ const preset = HAND_POSE_PRESETS[id];
2725
+ if (!preset) return null;
2726
+ return cloneHandPose(preset);
2727
+ }
2728
+ __name(getHandPosePreset, "getHandPosePreset");
2729
+ function cloneHandPose(definition) {
2730
+ return {
2731
+ ...definition,
2732
+ 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 }
2738
+ },
2739
+ pinch: definition.pinch ? { ...definition.pinch } : void 0,
2740
+ orientation: definition.orientation ? { ...definition.orientation } : void 0
2741
+ };
2742
+ }
2743
+ __name(cloneHandPose, "cloneHandPose");
2744
+ function handPoseFromCurls(id, name, curls, options) {
2745
+ const tolerance = options?.tolerance ?? NARROW;
2746
+ const target = /* @__PURE__ */ __name((curl) => ({
2747
+ curl: Math.min(1, Math.max(0, curl)),
2748
+ tolerance
2749
+ }), "target");
2750
+ return {
2751
+ id,
2752
+ name,
2753
+ 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)
2759
+ },
2760
+ pinch: options?.pinch,
2761
+ orientation: options?.orientation,
2762
+ hand: options?.hand
2763
+ };
2764
+ }
2765
+ __name(handPoseFromCurls, "handPoseFromCurls");
2766
+
2767
+ // core/components/XRHandPose.ts
2768
+ var DEFAULT_POSE_CURLS = {
2769
+ thumb: 0,
2770
+ index: 0,
2771
+ middle: 0,
2772
+ ring: 0,
2773
+ pinky: 0
2774
+ };
2775
+ function createHandPoseEntry(partial) {
2776
+ return {
2777
+ id: partial.id,
2778
+ name: partial.name ?? partial.id,
2779
+ preset: partial.preset ?? null,
2780
+ curls: { ...DEFAULT_POSE_CURLS, ...partial.curls },
2781
+ tolerance: partial.tolerance ?? 0.25,
2782
+ hand: partial.hand ?? null,
2783
+ orientation: partial.orientation ?? null,
2784
+ minHoldSeconds: partial.minHoldSeconds ?? 0.15,
2785
+ enabled: partial.enabled ?? true
2786
+ };
2787
+ }
2788
+ __name(createHandPoseEntry, "createHandPoseEntry");
2789
+ function handPoseDefinitionFor(entry, resolvePreset) {
2790
+ if (entry.preset) {
2791
+ const preset = resolvePreset(entry.preset);
2792
+ if (!preset) return null;
2793
+ return {
2794
+ ...preset,
2795
+ id: entry.id,
2796
+ name: entry.name,
2797
+ hand: entry.hand ?? preset.hand,
2798
+ orientation: entry.orientation ?? preset.orientation
2799
+ };
2800
+ }
2801
+ return handPoseFromCurls(entry.id, entry.name, entry.curls, {
2802
+ tolerance: entry.tolerance,
2803
+ orientation: entry.orientation ?? void 0,
2804
+ hand: entry.hand ?? void 0
2805
+ });
2806
+ }
2807
+ __name(handPoseDefinitionFor, "handPoseDefinitionFor");
2808
+ var _XRHandPoseComponent = class _XRHandPoseComponent extends Component {
2809
+ constructor(options) {
2810
+ super();
2811
+ this.enabled = options?.enabled ?? true;
2812
+ this.poses = (options?.poses ?? []).map((pose) => createHandPoseEntry(pose));
2813
+ }
2814
+ };
2815
+ __name(_XRHandPoseComponent, "XRHandPoseComponent");
2816
+ var XRHandPoseComponent = _XRHandPoseComponent;
2817
+
2818
+ // core/factories/components/XRHandPoseFactory.ts
2819
+ function createXRHandPoseComponent(world, entityId, ...args) {
2820
+ const component = new XRHandPoseComponent(...args);
2821
+ world.addComponent(entityId, component);
2822
+ return component;
2823
+ }
2824
+ __name(createXRHandPoseComponent, "createXRHandPoseComponent");
2825
+
2614
2826
  // core/components/XRInteractable.ts
2615
2827
  var _XRInteractableComponent = class _XRInteractableComponent extends Component {
2616
2828
  constructor(options) {
@@ -2619,6 +2831,22 @@ var _XRInteractableComponent = class _XRInteractableComponent extends Component
2619
2831
  this.grabbable = options?.grabbable ?? true;
2620
2832
  this.pointable = options?.pointable ?? true;
2621
2833
  this.grabMode = options?.grabMode ?? "kinematic";
2834
+ this.grabReach = options?.grabReach ?? "both";
2835
+ this.distanceBehavior = options?.distanceBehavior ?? "snap";
2836
+ this.grabGesture = options?.grabGesture ?? "any";
2837
+ this.grabRadius = options?.grabRadius ?? 0.15;
2838
+ this.grabOffsetPosition = options?.grabOffsetPosition ?? {
2839
+ x: 0,
2840
+ y: 0,
2841
+ z: 0
2842
+ };
2843
+ this.grabOffsetRotation = options?.grabOffsetRotation ?? {
2844
+ x: 0,
2845
+ y: 0,
2846
+ z: 0
2847
+ };
2848
+ this.throwOnRelease = options?.throwOnRelease ?? true;
2849
+ this.throwStrength = options?.throwStrength ?? 1;
2622
2850
  this.highlightOnHover = options?.highlightOnHover ?? true;
2623
2851
  }
2624
2852
  };
@@ -2683,6 +2911,7 @@ function registerDefaultComponentFactories(objectManagementSystem) {
2683
2911
  objectManagementSystem.registerComponentFactory("image", createImageComponent);
2684
2912
  objectManagementSystem.registerComponentFactory("xrRig", createXRRigComponent);
2685
2913
  objectManagementSystem.registerComponentFactory("vrTeleportArea", createVRTeleportAreaComponent);
2914
+ objectManagementSystem.registerComponentFactory("xrHandPose", createXRHandPoseComponent);
2686
2915
  objectManagementSystem.registerComponentFactory("xrInteractable", createXRInteractableComponent);
2687
2916
  objectManagementSystem.registerComponentFactory("xrInteractor", createXRInteractorComponent);
2688
2917
  objectManagementSystem.registerComponentFactory("xrInteractorSide", createXRInteractorSideComponent);
@@ -3705,6 +3934,7 @@ __export(components_exports, {
3705
3934
  DEFAULT_INSTANCE_TRANSFORM: () => DEFAULT_INSTANCE_TRANSFORM,
3706
3935
  DEFAULT_MESH_GEOMETRY_PARAMETERS: () => DEFAULT_MESH_GEOMETRY_PARAMETERS,
3707
3936
  DEFAULT_PHYSICS_STATE: () => DEFAULT_PHYSICS_STATE,
3937
+ DEFAULT_POSE_CURLS: () => DEFAULT_POSE_CURLS,
3708
3938
  DEFAULT_POSTPROCESSING_STATE: () => DEFAULT_POSTPROCESSING_STATE,
3709
3939
  DEFAULT_RIGIDBODY_STATE: () => DEFAULT_RIGIDBODY_STATE,
3710
3940
  EntityType: () => EntityType,
@@ -3734,11 +3964,14 @@ __export(components_exports, {
3734
3964
  TransformComponent: () => TransformComponent,
3735
3965
  VRTeleportAreaComponent: () => VRTeleportAreaComponent,
3736
3966
  VideoComponent: () => VideoComponent,
3967
+ XRHandPoseComponent: () => XRHandPoseComponent,
3737
3968
  XRInteractableComponent: () => XRInteractableComponent,
3738
3969
  XRInteractorComponent: () => XRInteractorComponent,
3739
3970
  XRInteractorSideComponent: () => XRInteractorSideComponent,
3740
3971
  XRRigComponent: () => XRRigComponent,
3972
+ createHandPoseEntry: () => createHandPoseEntry,
3741
3973
  defaultMaterialValue: () => defaultMaterialValue,
3974
+ handPoseDefinitionFor: () => handPoseDefinitionFor,
3742
3975
  normalizePhysicsState: () => normalizePhysicsState,
3743
3976
  setPhysicsLiveRef: () => setPhysicsLiveRef
3744
3977
  });
@@ -4427,6 +4660,10 @@ function buildComponentValueMaps(snapshot) {
4427
4660
  snapshot.components,
4428
4661
  "XRInteractorComponent"
4429
4662
  ),
4663
+ xrHandPoseValues: readByComponent(
4664
+ snapshot.components,
4665
+ "XRHandPoseComponent"
4666
+ ),
4430
4667
  xrInteractorSideValues: readByComponent(
4431
4668
  snapshot.components,
4432
4669
  "XRInteractorSideComponent"
@@ -4612,11 +4849,65 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
4612
4849
  if (typeof sourceInteractable.grabMode === "string") {
4613
4850
  interactable.grabMode = sourceInteractable.grabMode;
4614
4851
  }
4852
+ if (typeof sourceInteractable.grabReach === "string") {
4853
+ interactable.grabReach = sourceInteractable.grabReach;
4854
+ }
4855
+ if (typeof sourceInteractable.distanceBehavior === "string") {
4856
+ interactable.distanceBehavior = sourceInteractable.distanceBehavior;
4857
+ }
4858
+ if (typeof sourceInteractable.grabGesture === "string") {
4859
+ interactable.grabGesture = sourceInteractable.grabGesture;
4860
+ }
4861
+ if (typeof sourceInteractable.grabRadius === "number") {
4862
+ interactable.grabRadius = sourceInteractable.grabRadius;
4863
+ }
4864
+ if (typeof sourceInteractable.throwOnRelease === "boolean") {
4865
+ interactable.throwOnRelease = sourceInteractable.throwOnRelease;
4866
+ }
4867
+ if (typeof sourceInteractable.throwStrength === "number") {
4868
+ interactable.throwStrength = sourceInteractable.throwStrength;
4869
+ }
4870
+ const grabPos = asRecord3(sourceInteractable.grabOffsetPosition);
4871
+ if (grabPos) {
4872
+ interactable.grabOffsetPosition = {
4873
+ x: typeof grabPos.x === "number" ? grabPos.x : 0,
4874
+ y: typeof grabPos.y === "number" ? grabPos.y : 0,
4875
+ z: typeof grabPos.z === "number" ? grabPos.z : 0
4876
+ };
4877
+ }
4878
+ const grabRot = asRecord3(sourceInteractable.grabOffsetRotation);
4879
+ if (grabRot) {
4880
+ interactable.grabOffsetRotation = {
4881
+ x: typeof grabRot.x === "number" ? grabRot.x : 0,
4882
+ y: typeof grabRot.y === "number" ? grabRot.y : 0,
4883
+ z: typeof grabRot.z === "number" ? grabRot.z : 0
4884
+ };
4885
+ }
4615
4886
  if (typeof sourceInteractable.highlightOnHover === "boolean") {
4616
4887
  interactable.highlightOnHover = sourceInteractable.highlightOnHover;
4617
4888
  }
4618
4889
  }
4619
4890
  }
4891
+ {
4892
+ const sourceHandPose = asRecord3(
4893
+ values.xrHandPoseValues.get(sourceEntityId)
4894
+ );
4895
+ const handPose = world.getComponent(targetEntityId, XRHandPoseComponent);
4896
+ if (sourceHandPose && handPose) {
4897
+ if (typeof sourceHandPose.enabled === "boolean") {
4898
+ handPose.enabled = sourceHandPose.enabled;
4899
+ }
4900
+ if (Array.isArray(sourceHandPose.poses)) {
4901
+ handPose.poses = sourceHandPose.poses.map((entry) => asRecord3(entry)).filter(
4902
+ (entry) => entry !== null && typeof entry.id === "string"
4903
+ ).map(
4904
+ (entry) => createHandPoseEntry(
4905
+ entry
4906
+ )
4907
+ );
4908
+ }
4909
+ }
4910
+ }
4620
4911
  {
4621
4912
  const sourceInteractor = asRecord3(
4622
4913
  values.xrInteractorValues.get(sourceEntityId)
@@ -9178,6 +9469,17 @@ var _Behaviour = class _Behaviour {
9178
9469
  *
9179
9470
  * @param input - The source doing the grabbing.
9180
9471
  */
9472
+ /**
9473
+ * A hand pose declared on this entity started being held.
9474
+ *
9475
+ * Fires once the pose has been sustained for its hold time, not the instant
9476
+ * the fingers pass through it.
9477
+ */
9478
+ onHandPoseStart(_event) {
9479
+ }
9480
+ /** A held hand pose ended, or its hand stopped being tracked. */
9481
+ onHandPoseEnd(_event) {
9482
+ }
9181
9483
  onGrab(input) {
9182
9484
  }
9183
9485
  /** Called when this entity is released. Optional XR hook. */
@@ -10320,13 +10622,65 @@ function blankState(source) {
10320
10622
  squeezeEnd: false,
10321
10623
  axes: [],
10322
10624
  hasAxes: false,
10323
- buttons: []
10625
+ buttons: [],
10626
+ joints: null
10324
10627
  };
10325
10628
  }
10326
10629
  __name(blankState, "blankState");
10630
+ var JOINT_MATRIX_BUFFER = new Float32Array(25 * 16);
10631
+ function readHandJoints(frame, hand, referenceSpace, rigMatrix, existing) {
10632
+ const names = [];
10633
+ const spaces = [];
10634
+ for (const [name, space] of hand) {
10635
+ names.push(name);
10636
+ spaces.push(space);
10637
+ }
10638
+ if (spaces.length === 0) return null;
10639
+ const target = existing ?? {};
10640
+ const matrix = new THREE39__namespace.Matrix4();
10641
+ const buffer = spaces.length * 16 <= JOINT_MATRIX_BUFFER.length ? JOINT_MATRIX_BUFFER : new Float32Array(spaces.length * 16);
10642
+ const fillPoses = frame.fillPoses;
10643
+ if (typeof fillPoses === "function") {
10644
+ let filled = false;
10645
+ try {
10646
+ filled = fillPoses.call(frame, spaces, referenceSpace, buffer);
10647
+ } catch {
10648
+ filled = false;
10649
+ }
10650
+ if (filled) {
10651
+ for (let i = 0; i < names.length; i += 1) {
10652
+ matrix.fromArray(buffer, i * 16).premultiply(rigMatrix);
10653
+ const vector = target[names[i]] ?? new THREE39__namespace.Vector3();
10654
+ vector.setFromMatrixPosition(matrix);
10655
+ target[names[i]] = vector;
10656
+ }
10657
+ return target;
10658
+ }
10659
+ }
10660
+ let anyTracked = false;
10661
+ for (let i = 0; i < names.length; i += 1) {
10662
+ const pose = frame.getJointPose?.(
10663
+ spaces[i],
10664
+ referenceSpace
10665
+ );
10666
+ if (!pose) {
10667
+ target[names[i]] = void 0;
10668
+ continue;
10669
+ }
10670
+ matrix.fromArray(pose.transform.matrix).premultiply(rigMatrix);
10671
+ const vector = target[names[i]] ?? new THREE39__namespace.Vector3();
10672
+ vector.setFromMatrixPosition(matrix);
10673
+ target[names[i]] = vector;
10674
+ anyTracked = true;
10675
+ }
10676
+ return anyTracked ? target : null;
10677
+ }
10678
+ __name(readHandJoints, "readHandJoints");
10327
10679
  var _XRInputSystem = class _XRInputSystem {
10328
10680
  constructor() {
10329
10681
  this.records = /* @__PURE__ */ new Map();
10682
+ /** Reference-space → world matrix, rebuilt each frame from the XR rig. */
10683
+ this.referenceToWorld = new THREE39__namespace.Matrix4();
10330
10684
  this.session = null;
10331
10685
  this.onSelectStart = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSelectStart"), "onSelectStart");
10332
10686
  this.onSelectEnd = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSelectEnd"), "onSelectEnd");
@@ -10406,6 +10760,14 @@ var _XRInputSystem = class _XRInputSystem {
10406
10760
  const referenceSpace = renderer.xr.getReferenceSpace();
10407
10761
  const frame = renderer.xr.getFrame?.();
10408
10762
  const live = /* @__PURE__ */ new Set();
10763
+ const rigMatrix = this.referenceToWorld;
10764
+ const rig = world.getCamera().parent;
10765
+ if (rig) {
10766
+ rig.updateMatrixWorld();
10767
+ rigMatrix.copy(rig.matrixWorld);
10768
+ } else {
10769
+ rigMatrix.identity();
10770
+ }
10409
10771
  for (const source of session.inputSources) {
10410
10772
  live.add(source);
10411
10773
  const record = this.ensureRecord(source);
@@ -10416,7 +10778,7 @@ var _XRInputSystem = class _XRInputSystem {
10416
10778
  if (frame && referenceSpace && source.targetRaySpace) {
10417
10779
  const pose = frame.getPose(source.targetRaySpace, referenceSpace);
10418
10780
  if (pose) {
10419
- const m = new THREE39__namespace.Matrix4().fromArray(pose.transform.matrix);
10781
+ const m = new THREE39__namespace.Matrix4().fromArray(pose.transform.matrix).premultiply(rigMatrix);
10420
10782
  state.rayOrigin.setFromMatrixPosition(m);
10421
10783
  state.rayDirection.set(0, 0, -1).applyQuaternion(
10422
10784
  new THREE39__namespace.Quaternion().setFromRotationMatrix(m)
@@ -10426,7 +10788,7 @@ var _XRInputSystem = class _XRInputSystem {
10426
10788
  if (frame && referenceSpace && source.gripSpace) {
10427
10789
  const gripPose = frame.getPose(source.gripSpace, referenceSpace);
10428
10790
  if (gripPose) {
10429
- const m = new THREE39__namespace.Matrix4().fromArray(gripPose.transform.matrix);
10791
+ const m = new THREE39__namespace.Matrix4().fromArray(gripPose.transform.matrix).premultiply(rigMatrix);
10430
10792
  state.gripPosition ?? (state.gripPosition = new THREE39__namespace.Vector3());
10431
10793
  state.gripQuaternion ?? (state.gripQuaternion = new THREE39__namespace.Quaternion());
10432
10794
  state.gripPosition.setFromMatrixPosition(m);
@@ -10436,6 +10798,17 @@ var _XRInputSystem = class _XRInputSystem {
10436
10798
  state.gripPosition = null;
10437
10799
  state.gripQuaternion = null;
10438
10800
  }
10801
+ if (frame && referenceSpace && source.hand) {
10802
+ state.joints = readHandJoints(
10803
+ frame,
10804
+ source.hand,
10805
+ referenceSpace,
10806
+ rigMatrix,
10807
+ state.joints
10808
+ );
10809
+ } else {
10810
+ state.joints = null;
10811
+ }
10439
10812
  const gamepad = source.gamepad;
10440
10813
  if (gamepad) {
10441
10814
  state.axes = Array.from(gamepad.axes);
@@ -10500,20 +10873,84 @@ var _XRInputSystem = class _XRInputSystem {
10500
10873
  };
10501
10874
  __name(_XRInputSystem, "XRInputSystem");
10502
10875
  var XRInputSystem = _XRInputSystem;
10876
+ var VELOCITY_SAMPLES = 5;
10877
+ var MAX_SAMPLE_AGE_SECONDS = 0.2;
10878
+ var _ThrowVelocityTracker = class _ThrowVelocityTracker {
10879
+ constructor() {
10880
+ this.samples = [];
10881
+ }
10882
+ /** Record where the hand is now. Call once per frame while held. */
10883
+ record(position, time) {
10884
+ this.samples.push({ position: position.clone(), time });
10885
+ if (this.samples.length > VELOCITY_SAMPLES) {
10886
+ this.samples.shift();
10887
+ }
10888
+ }
10889
+ /**
10890
+ * Velocity to release with, in metres/second.
10891
+ *
10892
+ * Uses the FASTEST segment inside the window rather than the most recent or
10893
+ * the window average. Users decelerate as they open their hand, so the last
10894
+ * segment understates the throw and the average washes it out; peak speed is
10895
+ * what matches the motion the user actually felt they made.
10896
+ */
10897
+ computeVelocity(now) {
10898
+ const fresh = this.samples.filter(
10899
+ (sample) => now - sample.time <= MAX_SAMPLE_AGE_SECONDS
10900
+ );
10901
+ if (fresh.length < 2) return new THREE39__namespace.Vector3();
10902
+ let best = new THREE39__namespace.Vector3();
10903
+ let bestSpeed = 0;
10904
+ for (let i = 1; i < fresh.length; i += 1) {
10905
+ const previous = fresh[i - 1];
10906
+ const current = fresh[i];
10907
+ const dt = current.time - previous.time;
10908
+ if (dt <= 1e-5) continue;
10909
+ const velocity = new THREE39__namespace.Vector3().subVectors(current.position, previous.position).divideScalar(dt);
10910
+ const speed = velocity.length();
10911
+ if (speed > bestSpeed) {
10912
+ bestSpeed = speed;
10913
+ best = velocity;
10914
+ }
10915
+ }
10916
+ return best;
10917
+ }
10918
+ clear() {
10919
+ this.samples.length = 0;
10920
+ }
10921
+ };
10922
+ __name(_ThrowVelocityTracker, "ThrowVelocityTracker");
10923
+ var ThrowVelocityTracker = _ThrowVelocityTracker;
10924
+ function angularVelocityFrom(previous, current, deltaTime) {
10925
+ if (deltaTime <= 1e-5) return new THREE39__namespace.Vector3();
10926
+ const delta = current.clone().multiply(previous.clone().invert());
10927
+ if (delta.w < 0) {
10928
+ delta.set(-delta.x, -delta.y, -delta.z, -delta.w);
10929
+ }
10930
+ const angle = 2 * Math.acos(THREE39__namespace.MathUtils.clamp(delta.w, -1, 1));
10931
+ const sinHalf = Math.sqrt(Math.max(0, 1 - delta.w * delta.w));
10932
+ if (sinHalf < 1e-6 || angle < 1e-6) return new THREE39__namespace.Vector3();
10933
+ return new THREE39__namespace.Vector3(delta.x, delta.y, delta.z).divideScalar(sinHalf).multiplyScalar(angle / deltaTime);
10934
+ }
10935
+ __name(angularVelocityFrom, "angularVelocityFrom");
10936
+
10937
+ // core/systems/XRInteractionSystem.ts
10503
10938
  var RAYLESS_HOLD_DISTANCE = 0.5;
10504
10939
  var _XRInteractionSystem = class _XRInteractionSystem {
10505
10940
  constructor(inputSystem) {
10506
10941
  this.raycaster = new THREE39__namespace.Raycaster();
10507
- /** Which entity each source is holding. Keyed by source state identity. */
10508
10942
  this.grabs = /* @__PURE__ */ new Map();
10509
10943
  this.hoveredEntityId = null;
10944
+ /** Monotonic seconds, accumulated from deltaTime for velocity sampling. */
10945
+ this.elapsed = 0;
10510
10946
  this.inputSystem = inputSystem;
10511
10947
  }
10512
- update(world) {
10948
+ update(world, deltaTime) {
10513
10949
  if (!world.isXrPresenting()) {
10514
10950
  this.releaseAll(world);
10515
10951
  return;
10516
10952
  }
10953
+ this.elapsed += Math.max(0, deltaTime);
10517
10954
  const inputs = this.inputSystem.getInputs();
10518
10955
  const interactables = this.collectInteractables(world);
10519
10956
  this.hoveredEntityId = null;
@@ -10524,29 +10961,100 @@ var _XRInteractionSystem = class _XRInteractionSystem {
10524
10961
  }
10525
10962
  const held = this.grabs.get(input);
10526
10963
  if (held) {
10527
- this.moveHeld(world, input, held);
10528
- if (input.selectEnd) this.release(world, input);
10964
+ this.moveHeld(world, input, held, deltaTime);
10965
+ if (this.wantsRelease(world, input, held)) {
10966
+ this.release(world, input);
10967
+ }
10529
10968
  continue;
10530
10969
  }
10531
- const hitEntityId = this.raycast(input, interactables);
10532
- if (hitEntityId === null) continue;
10970
+ const candidate = this.findGrabCandidate(world, input, interactables);
10971
+ if (candidate === null) continue;
10533
10972
  if (this.hoveredEntityId === null) {
10534
- this.hoveredEntityId = hitEntityId;
10973
+ this.hoveredEntityId = candidate.entityId;
10535
10974
  }
10536
10975
  const interactable = world.getComponent(
10537
- hitEntityId,
10976
+ candidate.entityId,
10538
10977
  XRInteractableComponent
10539
10978
  );
10540
- if (input.selectStart && interactable?.grabbable) {
10541
- this.grab(world, input, hitEntityId);
10979
+ if (interactable?.grabbable && this.gestureMatches(
10980
+ input,
10981
+ interactable,
10982
+ /* starting */
10983
+ true
10984
+ )) {
10985
+ this.grab(world, input, candidate.entityId, candidate.viaDistance);
10986
+ }
10987
+ }
10988
+ }
10989
+ /** Whether the grab gesture for this object is currently starting. */
10990
+ gestureMatches(input, interactable, starting) {
10991
+ const pinch = starting ? input.selectStart : input.select;
10992
+ const full = starting ? input.squeezeStart : input.squeeze;
10993
+ switch (interactable.grabGesture) {
10994
+ case "pinch":
10995
+ return pinch;
10996
+ case "full":
10997
+ return full;
10998
+ default:
10999
+ return pinch || full;
11000
+ }
11001
+ }
11002
+ /** True once the gesture that started the grab has ended. */
11003
+ wantsRelease(world, input, held) {
11004
+ const interactable = world.getComponent(
11005
+ held.entityId,
11006
+ XRInteractableComponent
11007
+ );
11008
+ if (!interactable) return true;
11009
+ switch (interactable.grabGesture) {
11010
+ case "pinch":
11011
+ return input.selectEnd || !input.select;
11012
+ case "full":
11013
+ return input.squeezeEnd || !input.squeeze;
11014
+ default:
11015
+ return !input.select && !input.squeeze;
11016
+ }
11017
+ }
11018
+ /**
11019
+ * Find what this hand would grab: a physically-reachable object first, then
11020
+ * one along the pointer ray.
11021
+ *
11022
+ * Proximity wins over the ray so that reaching for something you are touching
11023
+ * never picks up a distant object behind it, which feels broken.
11024
+ */
11025
+ findGrabCandidate(world, input, interactables) {
11026
+ const hand = this.handPosition(input);
11027
+ let nearestId = null;
11028
+ let nearestDistance = Number.POSITIVE_INFINITY;
11029
+ for (const entry of interactables) {
11030
+ const interactable2 = world.getComponent(
11031
+ entry.entityId,
11032
+ XRInteractableComponent
11033
+ );
11034
+ if (!interactable2) continue;
11035
+ if (interactable2.grabReach === "distance") continue;
11036
+ const position = new THREE39__namespace.Vector3();
11037
+ entry.object.getWorldPosition(position);
11038
+ const distance = position.distanceTo(hand);
11039
+ if (distance <= interactable2.grabRadius && distance < nearestDistance) {
11040
+ nearestDistance = distance;
11041
+ nearestId = entry.entityId;
10542
11042
  }
10543
11043
  }
11044
+ if (nearestId !== null) {
11045
+ return { entityId: nearestId, viaDistance: false };
11046
+ }
11047
+ const hitId = this.raycast(input, interactables);
11048
+ if (hitId === null) return null;
11049
+ const interactable = world.getComponent(hitId, XRInteractableComponent);
11050
+ const viaDistanceAllowed = interactable?.grabReach === "distance" || interactable?.grabReach === "both";
11051
+ return { entityId: hitId, viaDistance: viaDistanceAllowed };
10544
11052
  }
10545
11053
  collectInteractables(world) {
10546
11054
  const out = [];
10547
11055
  for (const entityId of world.query(XRInteractableComponent, Object3DRef)) {
10548
11056
  const interactable = world.getComponent(entityId, XRInteractableComponent);
10549
- if (!interactable?.enabled || !interactable.pointable) continue;
11057
+ if (!interactable?.enabled) continue;
10550
11058
  if (!world.isEntityVisible(entityId)) continue;
10551
11059
  const object = world.getComponent(entityId, Object3DRef)?.object;
10552
11060
  if (object) out.push({ entityId, object });
@@ -10557,6 +11065,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
10557
11065
  raycast(input, interactables) {
10558
11066
  if (interactables.length === 0) return null;
10559
11067
  this.raycaster.set(input.rayOrigin, input.rayDirection);
11068
+ this.raycaster.far = Number.POSITIVE_INFINITY;
10560
11069
  let nearestId = null;
10561
11070
  let nearestDistance = Number.POSITIVE_INFINITY;
10562
11071
  for (const entry of interactables) {
@@ -10569,43 +11078,120 @@ var _XRInteractionSystem = class _XRInteractionSystem {
10569
11078
  }
10570
11079
  return nearestId;
10571
11080
  }
10572
- grab(world, input, entityId) {
11081
+ grab(world, input, entityId, viaDistance) {
10573
11082
  const transform = world.getComponent(entityId, TransformComponent);
10574
- if (!transform) return;
11083
+ const interactable = world.getComponent(entityId, XRInteractableComponent);
11084
+ if (!transform || !interactable) return;
10575
11085
  const hand = this.handPosition(input);
10576
- const offset = new THREE39__namespace.Vector3(
10577
- transform.position.x - hand.x,
10578
- transform.position.y - hand.y,
10579
- transform.position.z - hand.z
10580
- );
11086
+ const handQuaternion = this.handQuaternion(input);
11087
+ const remote = viaDistance && interactable.distanceBehavior === "remote";
11088
+ let offset;
11089
+ let remoteDistance = 0;
11090
+ if (remote) {
11091
+ const position = new THREE39__namespace.Vector3(
11092
+ transform.position.x,
11093
+ transform.position.y,
11094
+ transform.position.z
11095
+ );
11096
+ remoteDistance = position.distanceTo(input.rayOrigin);
11097
+ offset = new THREE39__namespace.Vector3();
11098
+ } else if (viaDistance) {
11099
+ offset = new THREE39__namespace.Vector3(
11100
+ interactable.grabOffsetPosition.x,
11101
+ interactable.grabOffsetPosition.y,
11102
+ interactable.grabOffsetPosition.z
11103
+ );
11104
+ } else {
11105
+ offset = new THREE39__namespace.Vector3(
11106
+ transform.position.x - hand.x,
11107
+ transform.position.y - hand.y,
11108
+ transform.position.z - hand.z
11109
+ );
11110
+ }
10581
11111
  const physics = world.getComponent(entityId, PhysicsComponent);
10582
11112
  let restoreBodyType = null;
10583
- const interactable = world.getComponent(entityId, XRInteractableComponent);
10584
- if (physics && interactable?.grabMode === "kinematic") {
11113
+ if (physics) {
10585
11114
  restoreBodyType = physics.values.rigidbody.type;
10586
11115
  physics.values.rigidbody.type = "kinematicPosition";
10587
11116
  }
10588
- this.grabs.set(input, { entityId, offset, restoreBodyType });
11117
+ const tracker = new ThrowVelocityTracker();
11118
+ tracker.record(hand, this.elapsed);
11119
+ this.grabs.set(input, {
11120
+ entityId,
11121
+ offset,
11122
+ rotationOffset: new THREE39__namespace.Quaternion().setFromEuler(
11123
+ new THREE39__namespace.Euler(
11124
+ interactable.grabOffsetRotation.x,
11125
+ interactable.grabOffsetRotation.y,
11126
+ interactable.grabOffsetRotation.z
11127
+ )
11128
+ ),
11129
+ remoteDistance,
11130
+ remote,
11131
+ restoreBodyType,
11132
+ tracker,
11133
+ lastHandQuaternion: handQuaternion.clone(),
11134
+ angularVelocity: new THREE39__namespace.Vector3()
11135
+ });
10589
11136
  this.dispatch(world, entityId, (b) => b.onGrab(input));
10590
11137
  }
10591
- moveHeld(world, input, held) {
11138
+ moveHeld(world, input, held, deltaTime) {
10592
11139
  const transform = world.getComponent(held.entityId, TransformComponent);
10593
11140
  if (!transform) return;
10594
11141
  const hand = this.handPosition(input);
10595
- transform.position.x = hand.x + held.offset.x;
10596
- transform.position.y = hand.y + held.offset.y;
10597
- transform.position.z = hand.z + held.offset.z;
11142
+ if (held.remote) {
11143
+ const target = new THREE39__namespace.Vector3().copy(input.rayDirection).multiplyScalar(held.remoteDistance).add(input.rayOrigin);
11144
+ transform.position.x = target.x;
11145
+ transform.position.y = target.y;
11146
+ transform.position.z = target.z;
11147
+ } else {
11148
+ transform.position.x = hand.x + held.offset.x;
11149
+ transform.position.y = hand.y + held.offset.y;
11150
+ transform.position.z = hand.z + held.offset.z;
11151
+ }
11152
+ held.tracker.record(
11153
+ held.remote ? new THREE39__namespace.Vector3(
11154
+ transform.position.x,
11155
+ transform.position.y,
11156
+ transform.position.z
11157
+ ) : hand,
11158
+ this.elapsed
11159
+ );
11160
+ const handQuaternion = this.handQuaternion(input);
11161
+ held.angularVelocity = angularVelocityFrom(
11162
+ held.lastHandQuaternion,
11163
+ handQuaternion,
11164
+ deltaTime
11165
+ );
11166
+ held.lastHandQuaternion = handQuaternion;
10598
11167
  }
10599
11168
  release(world, input) {
10600
11169
  const held = this.grabs.get(input);
10601
11170
  if (!held) return;
10602
11171
  this.grabs.delete(input);
10603
- if (held.restoreBodyType !== null) {
10604
- const physics = world.getComponent(held.entityId, PhysicsComponent);
10605
- if (physics) {
10606
- physics.values.rigidbody.type = held.restoreBodyType;
11172
+ const interactable = world.getComponent(
11173
+ held.entityId,
11174
+ XRInteractableComponent
11175
+ );
11176
+ const physics = world.getComponent(held.entityId, PhysicsComponent);
11177
+ if (held.restoreBodyType !== null && physics) {
11178
+ physics.values.rigidbody.type = held.restoreBodyType;
11179
+ const body = physics.rigidBody;
11180
+ const dynamic = held.restoreBodyType === "dynamic";
11181
+ if (body && dynamic && interactable?.throwOnRelease) {
11182
+ const velocity = held.tracker.computeVelocity(this.elapsed).multiplyScalar(interactable.throwStrength);
11183
+ try {
11184
+ body.setLinvel(
11185
+ { x: velocity.x, y: velocity.y, z: velocity.z },
11186
+ true
11187
+ );
11188
+ const spin = held.angularVelocity;
11189
+ body.setAngvel({ x: spin.x, y: spin.y, z: spin.z }, true);
11190
+ } catch {
11191
+ }
10607
11192
  }
10608
11193
  }
11194
+ held.tracker.clear();
10609
11195
  this.dispatch(world, held.entityId, (b) => b.onRelease(input));
10610
11196
  }
10611
11197
  releaseAll(world) {
@@ -10614,14 +11200,25 @@ var _XRInteractionSystem = class _XRInteractionSystem {
10614
11200
  }
10615
11201
  }
10616
11202
  /**
10617
- * Where the "hand" is for this source. Controllers and tracked hands report a
10618
- * grip pose; gaze and transient-pointer sources have none, so we hold the
10619
- * object a fixed distance along the ray instead.
11203
+ * Where the "hand" is. Controllers and tracked hands report a grip pose;
11204
+ * gaze and transient-pointer sources have none, so we hold the object a fixed
11205
+ * distance along the ray instead — which is what makes Vision Pro work.
10620
11206
  */
10621
11207
  handPosition(input) {
10622
11208
  if (input.gripPosition) return input.gripPosition;
10623
11209
  return new THREE39__namespace.Vector3().copy(input.rayDirection).multiplyScalar(RAYLESS_HOLD_DISTANCE).add(input.rayOrigin);
10624
11210
  }
11211
+ /**
11212
+ * Hand orientation. Grip-less sources (gaze / transient-pointer) have none,
11213
+ * so we derive one from the aim direction — enough for spin on release.
11214
+ */
11215
+ handQuaternion(input) {
11216
+ if (input.gripQuaternion) return input.gripQuaternion.clone();
11217
+ return new THREE39__namespace.Quaternion().setFromUnitVectors(
11218
+ new THREE39__namespace.Vector3(0, 0, -1),
11219
+ input.rayDirection.clone().normalize()
11220
+ );
11221
+ }
10625
11222
  dispatch(world, entityId, visit) {
10626
11223
  const script = world.getComponent(entityId, ScriptComponent);
10627
11224
  const instances = script?.scriptInstances;
@@ -10697,6 +11294,10 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
10697
11294
  /** Curve up to the landing point, for the visual system. Empty when not aiming. */
10698
11295
  this.aimCurve = [];
10699
11296
  this.landingPoint = null;
11297
+ /** True when the current aim lands somewhere the player may actually stand. */
11298
+ this.aimValid = false;
11299
+ /** Sources currently aiming via thumbstick (hysteresis, so it doesn't chatter). */
11300
+ this.stickAiming = /* @__PURE__ */ new WeakSet();
10700
11301
  /** True once we've tried to self-heal camera parenting for this session. */
10701
11302
  this.parentingChecked = false;
10702
11303
  this.inputSystem = inputSystem;
@@ -10704,6 +11305,7 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
10704
11305
  update(world, deltaTime) {
10705
11306
  this.aimCurve = [];
10706
11307
  this.landingPoint = null;
11308
+ this.aimValid = false;
10707
11309
  if (!world.isXrPresenting()) {
10708
11310
  this.parentingChecked = false;
10709
11311
  return;
@@ -10711,6 +11313,7 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
10711
11313
  if (!this.parentingChecked) {
10712
11314
  this.parentingChecked = true;
10713
11315
  this.ensureCameraUnderRig(world);
11316
+ this.warnOnIncompleteSetup(world);
10714
11317
  }
10715
11318
  const rigEntityId = world.query(XRRigComponent, TransformComponent)[0];
10716
11319
  if (typeof rigEntityId !== "number") return;
@@ -10728,6 +11331,26 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
10728
11331
  this.applySmoothMove(world, rig, rigTransform, inputs, deltaTime);
10729
11332
  }
10730
11333
  }
11334
+ /**
11335
+ * Name what is missing, once per session.
11336
+ *
11337
+ * Inside a headset there is no console and no inspector, so a scene that
11338
+ * simply cannot move gives the author nothing to go on. These messages are
11339
+ * visible over remote debugging and say exactly which entity to add.
11340
+ */
11341
+ warnOnIncompleteSetup(world) {
11342
+ if (typeof world.query(XRRigComponent)[0] !== "number") {
11343
+ console.warn(
11344
+ "[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)."
11345
+ );
11346
+ return;
11347
+ }
11348
+ if (world.query(VRTeleportAreaComponent).length === 0) {
11349
+ console.warn(
11350
+ "[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."
11351
+ );
11352
+ }
11353
+ }
10731
11354
  /**
10732
11355
  * Guarantee the render camera is a CHILD of the rig.
10733
11356
  *
@@ -10795,29 +11418,60 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
10795
11418
  }
10796
11419
  applyTeleport(world, transform, inputs) {
10797
11420
  const targets = this.collectTeleportTargets(world);
10798
- if (targets.length === 0) return;
10799
11421
  for (const input of inputs) {
10800
- if (!input.select && !input.selectEnd) continue;
11422
+ const source = input;
11423
+ const stick = this.readStickAim(input, source);
11424
+ const aiming = input.select || stick.aiming;
11425
+ const commit = input.selectEnd || stick.released;
11426
+ if (!aiming && !commit) continue;
10801
11427
  const points = sampleTeleportArc(
10802
11428
  input.rayOrigin,
10803
11429
  input.rayDirection,
10804
11430
  this.arcPoints
10805
11431
  );
10806
11432
  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) {
11433
+ this.aimCurve = hit ? points.slice(0, hit.segmentIndex + 1).concat(hit.point) : points.slice();
11434
+ this.landingPoint = hit ? hit.point : null;
11435
+ this.aimValid = hit !== null;
11436
+ if (commit && hit) {
10811
11437
  transform.position.x = hit.point.x;
10812
11438
  transform.position.y = hit.point.y;
10813
11439
  transform.position.z = hit.point.z;
10814
11440
  this.aimCurve = [];
10815
11441
  this.landingPoint = null;
11442
+ this.aimValid = false;
11443
+ } else if (commit) {
11444
+ this.aimCurve = [];
11445
+ this.aimValid = false;
10816
11446
  }
10817
11447
  }
10818
11448
  }
11449
+ /**
11450
+ * Thumbstick-forward aiming with hysteresis.
11451
+ *
11452
+ * Engaging at 0.6 and releasing at 0.3 stops a stick resting near the
11453
+ * threshold from flickering the arc on and off — and the release edge is what
11454
+ * commits the teleport, so chatter there would teleport the player randomly.
11455
+ */
11456
+ readStickAim(input, source) {
11457
+ if (!input.hasAxes || input.axes.length < 4) {
11458
+ return { aiming: false, released: false };
11459
+ }
11460
+ const forward = -(input.axes[3] ?? 0);
11461
+ const wasAiming = this.stickAiming.has(source);
11462
+ if (!wasAiming && forward > 0.6) {
11463
+ this.stickAiming.add(source);
11464
+ return { aiming: true, released: false };
11465
+ }
11466
+ if (wasAiming && forward < 0.3) {
11467
+ this.stickAiming.delete(source);
11468
+ return { aiming: false, released: true };
11469
+ }
11470
+ return { aiming: wasAiming, released: false };
11471
+ }
10819
11472
  /** Walk the arc segment-by-segment, returning the first surface hit. */
10820
11473
  raycastArc(points, targets) {
11474
+ if (targets.length === 0) return null;
10821
11475
  for (let i = 1; i < points.length; i += 1) {
10822
11476
  const from = points[i - 1];
10823
11477
  const to = points[i];
@@ -10848,6 +11502,10 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
10848
11502
  getAimCurve() {
10849
11503
  return this.aimCurve;
10850
11504
  }
11505
+ /** True when the aim lands on a valid teleport area. */
11506
+ isAimValid() {
11507
+ return this.aimValid;
11508
+ }
10851
11509
  /** Where the arc currently lands, or null when not aiming at a valid area. */
10852
11510
  getLandingPoint() {
10853
11511
  return this.landingPoint;
@@ -11130,6 +11788,7 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
11130
11788
  this.handStyleSignature = "";
11131
11789
  this.handMaterial = null;
11132
11790
  this.interactionSystem = options?.interactionSystem ?? null;
11791
+ this.inputSystem = options?.inputSystem ?? null;
11133
11792
  this.group.name = "XRControllerVisuals";
11134
11793
  this.group.userData.ecsPreviewHidden = true;
11135
11794
  }
@@ -11146,8 +11805,11 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
11146
11805
  for (const { index, side } of SLOTS) {
11147
11806
  const visuals = this.slots.get(index);
11148
11807
  if (!visuals) continue;
11149
- visuals.controllerModel.visible = showControllers;
11150
- visuals.handModel.visible = showHands;
11808
+ const liveKind = this.liveKindFor(side);
11809
+ const hasController = liveKind === null || liveKind === "controller";
11810
+ const hasHand = liveKind === null || liveKind === "hand";
11811
+ visuals.controllerModel.visible = showControllers && hasController;
11812
+ visuals.handModel.visible = showHands && hasHand;
11151
11813
  visuals.ray.visible = this.shouldShowRay(interactor);
11152
11814
  if (showHands && this.handMaterial) {
11153
11815
  applyHandMaterial(visuals.handModel, this.handMaterial);
@@ -11155,6 +11817,20 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
11155
11817
  this.syncCustomContent(world, visuals, side);
11156
11818
  }
11157
11819
  }
11820
+ /**
11821
+ * What kind of source is currently driving this hand, or null when we cannot
11822
+ * tell (no input system wired) — in which case callers fall back to showing
11823
+ * everything rather than hiding the user's hands entirely.
11824
+ */
11825
+ liveKindFor(side) {
11826
+ if (!this.inputSystem) return null;
11827
+ const inputs = this.inputSystem.getInputs();
11828
+ if (inputs.length === 0) return null;
11829
+ for (const input of inputs) {
11830
+ if (input.handedness === side && input.connected) return input.kind;
11831
+ }
11832
+ return "none";
11833
+ }
11158
11834
  /** The scene's interactor config, if one was authored. */
11159
11835
  resolveInteractor(world) {
11160
11836
  const entityId = world.query(XRInteractorComponent)[0];
@@ -11294,6 +11970,7 @@ __name(_XRControllerVisualSystem, "XRControllerVisualSystem");
11294
11970
  var XRControllerVisualSystem = _XRControllerVisualSystem;
11295
11971
  var MARKER_RADIUS = 0.28;
11296
11972
  var ACCENT = new THREE39__namespace.Color("#4da3ff");
11973
+ var BLOCKED = new THREE39__namespace.Color("#ff5a5a");
11297
11974
  var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
11298
11975
  constructor(locomotionSystem) {
11299
11976
  this.group = new THREE39__namespace.Group();
@@ -11324,16 +12001,22 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
11324
12001
  }
11325
12002
  const curve = this.locomotionSystem.getAimCurve();
11326
12003
  const landing = this.locomotionSystem.getLandingPoint();
11327
- if (!landing || curve.length < 2) {
12004
+ const valid = this.locomotionSystem.isAimValid();
12005
+ if (curve.length < 2) {
11328
12006
  this.setVisible(false);
11329
12007
  return;
11330
12008
  }
11331
- this.updateArcGeometry(curve);
11332
- this.marker.position.copy(landing);
11333
- this.marker.position.y += 0.01;
12009
+ this.updateArcGeometry(curve, valid ? ACCENT : BLOCKED);
12010
+ if (valid && landing) {
12011
+ this.marker.position.copy(landing);
12012
+ this.marker.position.y += 0.01;
12013
+ this.marker.visible = true;
12014
+ } else {
12015
+ this.marker.visible = false;
12016
+ }
11334
12017
  this.setVisible(true);
11335
12018
  }
11336
- updateArcGeometry(curve) {
12019
+ updateArcGeometry(curve, tint) {
11337
12020
  const count = curve.length;
11338
12021
  const positions = new Float32Array(count * 3);
11339
12022
  const colors = new Float32Array(count * 3);
@@ -11344,9 +12027,9 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
11344
12027
  positions[i * 3 + 2] = point.z;
11345
12028
  const t = count > 1 ? i / (count - 1) : 0;
11346
12029
  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;
12030
+ colors[i * 3] = tint.r * fade;
12031
+ colors[i * 3 + 1] = tint.g * fade;
12032
+ colors[i * 3 + 2] = tint.b * fade;
11350
12033
  }
11351
12034
  this.arcGeometry.setAttribute(
11352
12035
  "position",
@@ -11407,6 +12090,511 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
11407
12090
  };
11408
12091
  __name(_XRTeleportVisualSystem, "XRTeleportVisualSystem");
11409
12092
  var XRTeleportVisualSystem = _XRTeleportVisualSystem;
12093
+ var XR_FINGERS = [
12094
+ "thumb",
12095
+ "index",
12096
+ "middle",
12097
+ "ring",
12098
+ "pinky"
12099
+ ];
12100
+ var PINCH_FINGERS = [
12101
+ "index",
12102
+ "middle",
12103
+ "ring",
12104
+ "pinky"
12105
+ ];
12106
+ var FINGER_CHAINS = {
12107
+ thumb: [
12108
+ "thumb-metacarpal",
12109
+ "thumb-phalanx-proximal",
12110
+ "thumb-phalanx-distal",
12111
+ "thumb-tip"
12112
+ ],
12113
+ index: [
12114
+ "index-finger-metacarpal",
12115
+ "index-finger-phalanx-proximal",
12116
+ "index-finger-phalanx-intermediate",
12117
+ "index-finger-phalanx-distal",
12118
+ "index-finger-tip"
12119
+ ],
12120
+ middle: [
12121
+ "middle-finger-metacarpal",
12122
+ "middle-finger-phalanx-proximal",
12123
+ "middle-finger-phalanx-intermediate",
12124
+ "middle-finger-phalanx-distal",
12125
+ "middle-finger-tip"
12126
+ ],
12127
+ ring: [
12128
+ "ring-finger-metacarpal",
12129
+ "ring-finger-phalanx-proximal",
12130
+ "ring-finger-phalanx-intermediate",
12131
+ "ring-finger-phalanx-distal",
12132
+ "ring-finger-tip"
12133
+ ],
12134
+ pinky: [
12135
+ "pinky-finger-metacarpal",
12136
+ "pinky-finger-phalanx-proximal",
12137
+ "pinky-finger-phalanx-intermediate",
12138
+ "pinky-finger-phalanx-distal",
12139
+ "pinky-finger-tip"
12140
+ ]
12141
+ };
12142
+ var TIP_JOINTS = {
12143
+ thumb: "thumb-tip",
12144
+ index: "index-finger-tip",
12145
+ middle: "middle-finger-tip",
12146
+ ring: "ring-finger-tip",
12147
+ pinky: "pinky-finger-tip"
12148
+ };
12149
+ var METACARPAL_JOINTS = {
12150
+ thumb: "thumb-metacarpal",
12151
+ index: "index-finger-metacarpal",
12152
+ middle: "middle-finger-metacarpal",
12153
+ ring: "ring-finger-metacarpal",
12154
+ pinky: "pinky-finger-metacarpal"
12155
+ };
12156
+ var FALLBACK_HAND_SCALE = 0.08;
12157
+ var STRAIGHT_COS = 0.95;
12158
+ var CURLED_COS = 0.55;
12159
+ function clamp01(value) {
12160
+ return value < 0 ? 0 : value > 1 ? 1 : value;
12161
+ }
12162
+ __name(clamp01, "clamp01");
12163
+ function boneDirections(joints, finger) {
12164
+ const chain = FINGER_CHAINS[finger];
12165
+ const directions = [];
12166
+ for (let i = 1; i < chain.length; i += 1) {
12167
+ const from = joints[chain[i - 1]];
12168
+ const to = joints[chain[i]];
12169
+ if (!from || !to) return null;
12170
+ const direction = new THREE39__namespace.Vector3().subVectors(to, from);
12171
+ if (direction.lengthSq() < 1e-12) return null;
12172
+ directions.push(direction.normalize());
12173
+ }
12174
+ return directions.length >= 2 ? directions : null;
12175
+ }
12176
+ __name(boneDirections, "boneDirections");
12177
+ function bendAmounts(directions) {
12178
+ const bends = [];
12179
+ for (let i = 1; i < directions.length; i += 1) {
12180
+ const cos = directions[i - 1].dot(directions[i]);
12181
+ const straightness = clamp01((cos - CURLED_COS) / (STRAIGHT_COS - CURLED_COS));
12182
+ bends.push(1 - straightness);
12183
+ }
12184
+ return bends;
12185
+ }
12186
+ __name(bendAmounts, "bendAmounts");
12187
+ function average(values) {
12188
+ if (values.length === 0) return 0;
12189
+ let sum = 0;
12190
+ for (const value of values) sum += value;
12191
+ return sum / values.length;
12192
+ }
12193
+ __name(average, "average");
12194
+ function computeFingerCurl(joints, finger) {
12195
+ const directions = boneDirections(joints, finger);
12196
+ if (!directions) return null;
12197
+ const bends = bendAmounts(directions);
12198
+ if (bends.length === 0) return null;
12199
+ return {
12200
+ baseCurl: bends[0],
12201
+ tipCurl: average(bends.slice(1)) || bends[bends.length - 1],
12202
+ curl: average(bends)
12203
+ };
12204
+ }
12205
+ __name(computeFingerCurl, "computeFingerCurl");
12206
+ function computePalmWidth(joints) {
12207
+ const index = joints["index-finger-metacarpal"];
12208
+ const pinky = joints["pinky-finger-metacarpal"];
12209
+ if (!index || !pinky) return null;
12210
+ const width = index.distanceTo(pinky);
12211
+ return width > 1e-6 ? width : null;
12212
+ }
12213
+ __name(computePalmWidth, "computePalmWidth");
12214
+ function estimateHandScale(joints) {
12215
+ const measurements = [];
12216
+ const width = computePalmWidth(joints);
12217
+ if (width !== null) measurements.push(width);
12218
+ const wrist = joints.wrist;
12219
+ const middleTip = joints["middle-finger-tip"];
12220
+ if (wrist && middleTip) {
12221
+ const span = wrist.distanceTo(middleTip);
12222
+ if (span > 1e-6) measurements.push(span * 0.5);
12223
+ }
12224
+ if (measurements.length === 0) return FALLBACK_HAND_SCALE;
12225
+ return average(measurements);
12226
+ }
12227
+ __name(estimateHandScale, "estimateHandScale");
12228
+ function computePinch(joints, finger) {
12229
+ const thumbTip = joints["thumb-tip"];
12230
+ const fingerTip = joints[TIP_JOINTS[finger]];
12231
+ if (!thumbTip || !fingerTip) return null;
12232
+ const scale = estimateHandScale(joints);
12233
+ const distance = thumbTip.distanceTo(fingerTip);
12234
+ return clamp01(1 - distance / (scale * 0.7));
12235
+ }
12236
+ __name(computePinch, "computePinch");
12237
+ function computeSpread(joints, a, b) {
12238
+ const aBase = joints[METACARPAL_JOINTS[a]];
12239
+ const aTip = joints[TIP_JOINTS[a]];
12240
+ const bBase = joints[METACARPAL_JOINTS[b]];
12241
+ const bTip = joints[TIP_JOINTS[b]];
12242
+ if (!aBase || !aTip || !bBase || !bTip) return null;
12243
+ const dirA = new THREE39__namespace.Vector3().subVectors(aTip, aBase);
12244
+ const dirB = new THREE39__namespace.Vector3().subVectors(bTip, bBase);
12245
+ if (dirA.lengthSq() < 1e-12 || dirB.lengthSq() < 1e-12) return null;
12246
+ return clamp01((1 - dirA.normalize().dot(dirB.normalize())) / 0.45);
12247
+ }
12248
+ __name(computeSpread, "computeSpread");
12249
+ function computePalmPose(joints, handedness) {
12250
+ const wrist = joints.wrist;
12251
+ const indexBase = joints["index-finger-metacarpal"];
12252
+ const pinkyBase = joints["pinky-finger-metacarpal"];
12253
+ if (!wrist || !indexBase || !pinkyBase) return null;
12254
+ const u = new THREE39__namespace.Vector3().subVectors(indexBase, wrist);
12255
+ const v = new THREE39__namespace.Vector3().subVectors(pinkyBase, wrist);
12256
+ const normal = new THREE39__namespace.Vector3().crossVectors(u, v);
12257
+ if (normal.lengthSq() < 1e-12) return null;
12258
+ normal.normalize();
12259
+ if (handedness === "left") normal.multiplyScalar(-1);
12260
+ const right = new THREE39__namespace.Vector3().subVectors(indexBase, pinkyBase);
12261
+ if (right.lengthSq() < 1e-12) return null;
12262
+ right.normalize();
12263
+ if (handedness === "left") right.multiplyScalar(-1);
12264
+ const up = new THREE39__namespace.Vector3().crossVectors(normal, right).normalize();
12265
+ const center = new THREE39__namespace.Vector3().add(wrist).add(indexBase).add(pinkyBase).divideScalar(3);
12266
+ return {
12267
+ center,
12268
+ normal,
12269
+ right,
12270
+ up,
12271
+ width: computePalmWidth(joints) ?? FALLBACK_HAND_SCALE
12272
+ };
12273
+ }
12274
+ __name(computePalmPose, "computePalmPose");
12275
+ function computeHandShape(joints, handedness) {
12276
+ const fingers2 = {};
12277
+ let tracked = 0;
12278
+ for (const finger of XR_FINGERS) {
12279
+ const curl = computeFingerCurl(joints, finger);
12280
+ if (curl) {
12281
+ fingers2[finger] = curl;
12282
+ tracked += 1;
12283
+ } else {
12284
+ fingers2[finger] = { baseCurl: 0, tipCurl: 0, curl: 0 };
12285
+ }
12286
+ }
12287
+ if (tracked === 0) return null;
12288
+ const pinch = {};
12289
+ for (const finger of PINCH_FINGERS) {
12290
+ pinch[finger] = computePinch(joints, finger) ?? 0;
12291
+ }
12292
+ const thumbBase = joints["thumb-metacarpal"];
12293
+ const thumbTip = joints["thumb-tip"];
12294
+ let thumbDirection = null;
12295
+ if (thumbBase && thumbTip) {
12296
+ const direction = new THREE39__namespace.Vector3().subVectors(thumbTip, thumbBase);
12297
+ if (direction.lengthSq() > 1e-12) thumbDirection = direction.normalize();
12298
+ }
12299
+ return {
12300
+ handedness,
12301
+ fingers: fingers2,
12302
+ pinch,
12303
+ spread: {
12304
+ thumbIndex: computeSpread(joints, "thumb", "index") ?? 0,
12305
+ indexMiddle: computeSpread(joints, "index", "middle") ?? 0,
12306
+ middleRing: computeSpread(joints, "middle", "ring") ?? 0,
12307
+ ringPinky: computeSpread(joints, "ring", "pinky") ?? 0
12308
+ },
12309
+ palm: computePalmPose(joints, handedness),
12310
+ scale: estimateHandScale(joints),
12311
+ thumbDirection
12312
+ };
12313
+ }
12314
+ __name(computeHandShape, "computeHandShape");
12315
+ var MATCH_FALLOFF = 0.25;
12316
+ var NO_MATCH = {
12317
+ matched: false,
12318
+ confidence: 0,
12319
+ worstConstraint: null
12320
+ };
12321
+ function scoreAgainst(actual, target, tolerance) {
12322
+ const error = Math.abs(actual - target);
12323
+ if (error <= tolerance) return 1;
12324
+ return clamp01(1 - (error - tolerance) / MATCH_FALLOFF);
12325
+ }
12326
+ __name(scoreAgainst, "scoreAgainst");
12327
+ var WORLD_DIRECTIONS = {
12328
+ up: new THREE39__namespace.Vector3(0, 1, 0),
12329
+ down: new THREE39__namespace.Vector3(0, -1, 0),
12330
+ forward: new THREE39__namespace.Vector3(0, 0, -1),
12331
+ back: new THREE39__namespace.Vector3(0, 0, 1),
12332
+ left: new THREE39__namespace.Vector3(-1, 0, 0),
12333
+ right: new THREE39__namespace.Vector3(1, 0, 0)
12334
+ };
12335
+ function axisVector(shape, axis) {
12336
+ if (axis === "thumb") return shape.thumbDirection;
12337
+ if (!shape.palm) return null;
12338
+ return axis === "palm-normal" ? shape.palm.normal : shape.palm.up;
12339
+ }
12340
+ __name(axisVector, "axisVector");
12341
+ function resolveDirection(direction, shape, context) {
12342
+ if (direction !== "toward-head" && direction !== "away-from-head") {
12343
+ return WORLD_DIRECTIONS[direction];
12344
+ }
12345
+ const head = context?.headPosition;
12346
+ const origin = shape.palm?.center;
12347
+ if (!head || !origin) return null;
12348
+ const toHead = new THREE39__namespace.Vector3().subVectors(head, origin);
12349
+ if (toHead.lengthSq() < 1e-12) return null;
12350
+ toHead.normalize();
12351
+ return direction === "toward-head" ? toHead : toHead.multiplyScalar(-1);
12352
+ }
12353
+ __name(resolveDirection, "resolveDirection");
12354
+ function matchHandPose(shape, definition, context) {
12355
+ if (definition.hand && definition.hand !== shape.handedness) return NO_MATCH;
12356
+ let confidence = 1;
12357
+ let worstScore = 1;
12358
+ let worstConstraint = null;
12359
+ const consider = /* @__PURE__ */ __name((score, label) => {
12360
+ if (score < confidence) confidence = score;
12361
+ if (score < worstScore) {
12362
+ worstScore = score;
12363
+ worstConstraint = label;
12364
+ }
12365
+ }, "consider");
12366
+ for (const finger of XR_FINGERS) {
12367
+ const target = definition.fingers[finger];
12368
+ if (!target) continue;
12369
+ const actual = shape.fingers[finger];
12370
+ consider(scoreAgainst(actual.curl, target.curl, target.tolerance), finger);
12371
+ if (target.baseCurl !== void 0) {
12372
+ consider(
12373
+ scoreAgainst(actual.baseCurl, target.baseCurl, target.tolerance),
12374
+ `${finger} knuckle`
12375
+ );
12376
+ }
12377
+ if (target.tipCurl !== void 0) {
12378
+ consider(
12379
+ scoreAgainst(actual.tipCurl, target.tipCurl, target.tolerance),
12380
+ `${finger} tip`
12381
+ );
12382
+ }
12383
+ }
12384
+ if (definition.pinch) {
12385
+ const { finger, target, tolerance } = definition.pinch;
12386
+ consider(
12387
+ scoreAgainst(shape.pinch[finger], target, tolerance),
12388
+ `${finger} pinch`
12389
+ );
12390
+ }
12391
+ if (definition.orientation) {
12392
+ const { axis, direction, toleranceDegrees } = definition.orientation;
12393
+ const handAxis = axisVector(shape, axis);
12394
+ const wanted = resolveDirection(direction, shape, context);
12395
+ if (!handAxis || !wanted) {
12396
+ consider(0, "orientation");
12397
+ } else {
12398
+ const angle = THREE39__namespace.MathUtils.radToDeg(
12399
+ Math.acos(THREE39__namespace.MathUtils.clamp(handAxis.dot(wanted), -1, 1))
12400
+ );
12401
+ const normalizedError = Math.max(0, angle - toleranceDegrees) / 90;
12402
+ consider(clamp01(1 - normalizedError / MATCH_FALLOFF), "orientation");
12403
+ }
12404
+ }
12405
+ return {
12406
+ matched: confidence >= 1,
12407
+ confidence,
12408
+ worstConstraint: confidence >= 1 ? null : worstConstraint
12409
+ };
12410
+ }
12411
+ __name(matchHandPose, "matchHandPose");
12412
+ var DEFAULT_ENTER_CONFIDENCE = 0.8;
12413
+ var DEFAULT_EXIT_CONFIDENCE = 0.55;
12414
+ var DEFAULT_MIN_HOLD_SECONDS = 0.15;
12415
+ var DEFAULT_LOST_GRACE_SECONDS = 0.25;
12416
+ var _HandPoseTracker = class _HandPoseTracker {
12417
+ constructor(options) {
12418
+ this.active = false;
12419
+ this.holdingSince = null;
12420
+ this.lostSince = null;
12421
+ this.enterConfidence = options?.enterConfidence ?? DEFAULT_ENTER_CONFIDENCE;
12422
+ const exit = options?.exitConfidence ?? DEFAULT_EXIT_CONFIDENCE;
12423
+ this.exitConfidence = Math.min(exit, this.enterConfidence - 1e-3);
12424
+ this.minHoldSeconds = options?.minHoldSeconds ?? DEFAULT_MIN_HOLD_SECONDS;
12425
+ this.lostGraceSeconds = options?.lostGraceSeconds ?? DEFAULT_LOST_GRACE_SECONDS;
12426
+ }
12427
+ isActive() {
12428
+ return this.active;
12429
+ }
12430
+ /** Feed this frame's confidence. Returns an event on a transition. */
12431
+ update(confidence, now) {
12432
+ this.lostSince = null;
12433
+ if (this.active) {
12434
+ if (confidence < this.exitConfidence) {
12435
+ this.active = false;
12436
+ this.holdingSince = null;
12437
+ return "end";
12438
+ }
12439
+ return null;
12440
+ }
12441
+ if (confidence >= this.enterConfidence) {
12442
+ if (this.holdingSince === null) this.holdingSince = now;
12443
+ if (now - this.holdingSince >= this.minHoldSeconds) {
12444
+ this.active = true;
12445
+ return "start";
12446
+ }
12447
+ return null;
12448
+ }
12449
+ this.holdingSince = null;
12450
+ return null;
12451
+ }
12452
+ /** Feed a frame where the hand isn't tracked at all. */
12453
+ miss(now) {
12454
+ this.holdingSince = null;
12455
+ if (!this.active) return null;
12456
+ if (this.lostSince === null) this.lostSince = now;
12457
+ if (now - this.lostSince < this.lostGraceSeconds) return null;
12458
+ this.active = false;
12459
+ this.lostSince = null;
12460
+ return "end";
12461
+ }
12462
+ reset() {
12463
+ this.active = false;
12464
+ this.holdingSince = null;
12465
+ this.lostSince = null;
12466
+ }
12467
+ };
12468
+ __name(_HandPoseTracker, "HandPoseTracker");
12469
+ var HandPoseTracker = _HandPoseTracker;
12470
+
12471
+ // core/systems/XRHandPoseSystem.ts
12472
+ var _XRHandPoseSystem = class _XRHandPoseSystem {
12473
+ constructor(inputSystem) {
12474
+ this.trackers = /* @__PURE__ */ new Map();
12475
+ this.shapes = /* @__PURE__ */ new Map();
12476
+ /** Reused so head-relative orientation checks allocate nothing per frame. */
12477
+ this.headPosition = new THREE39__namespace.Vector3();
12478
+ this.elapsed = 0;
12479
+ this.inputSystem = inputSystem;
12480
+ }
12481
+ update(world, deltaTime) {
12482
+ if (!world.isXrPresenting()) {
12483
+ this.resetAll();
12484
+ return;
12485
+ }
12486
+ this.elapsed += deltaTime;
12487
+ this.refreshShapes();
12488
+ const entries = this.collectEntries(world);
12489
+ if (entries.length === 0) return;
12490
+ world.getCamera().getWorldPosition(this.headPosition);
12491
+ const context = { headPosition: this.headPosition };
12492
+ for (const { entityId, entry } of entries) {
12493
+ const definition = handPoseDefinitionFor(entry, getHandPosePreset);
12494
+ if (!definition) continue;
12495
+ for (const handedness of ["left", "right"]) {
12496
+ if (entry.hand && entry.hand !== handedness) continue;
12497
+ const key = `${entityId}:${entry.id}`;
12498
+ const trackers = this.trackersFor(handedness);
12499
+ let tracker = trackers.get(key);
12500
+ if (!tracker) {
12501
+ tracker = new HandPoseTracker({
12502
+ minHoldSeconds: entry.minHoldSeconds
12503
+ });
12504
+ trackers.set(key, tracker);
12505
+ }
12506
+ const shape = this.shapes.get(handedness) ?? null;
12507
+ if (!shape) {
12508
+ if (tracker.miss(this.elapsed) === "end") {
12509
+ this.dispatch(world, entityId, "end", {
12510
+ poseId: entry.id,
12511
+ poseName: entry.name,
12512
+ handedness,
12513
+ confidence: 0
12514
+ });
12515
+ }
12516
+ continue;
12517
+ }
12518
+ const match = matchHandPose(shape, definition, context);
12519
+ const event = tracker.update(match.confidence, this.elapsed);
12520
+ if (event) {
12521
+ this.dispatch(world, entityId, event, {
12522
+ poseId: entry.id,
12523
+ poseName: entry.name,
12524
+ handedness,
12525
+ confidence: match.confidence
12526
+ });
12527
+ }
12528
+ }
12529
+ }
12530
+ }
12531
+ /** Derive each hand's shape once for the whole frame. */
12532
+ refreshShapes() {
12533
+ for (const handedness of ["left", "right"]) {
12534
+ const input = this.inputSystem.getInput(handedness);
12535
+ const joints = input?.joints ?? null;
12536
+ this.shapes.set(
12537
+ handedness,
12538
+ joints ? computeHandShape(joints, handedness) : null
12539
+ );
12540
+ }
12541
+ }
12542
+ collectEntries(world) {
12543
+ const collected = [];
12544
+ for (const entityId of world.query(XRHandPoseComponent)) {
12545
+ const component = world.getComponent(entityId, XRHandPoseComponent);
12546
+ if (!component?.enabled) continue;
12547
+ for (const entry of component.poses) {
12548
+ if (entry.enabled) collected.push({ entityId, entry });
12549
+ }
12550
+ }
12551
+ return collected;
12552
+ }
12553
+ trackersFor(handedness) {
12554
+ let trackers = this.trackers.get(handedness);
12555
+ if (!trackers) {
12556
+ trackers = /* @__PURE__ */ new Map();
12557
+ this.trackers.set(handedness, trackers);
12558
+ }
12559
+ return trackers;
12560
+ }
12561
+ /**
12562
+ * Deliver to the entity's own Behaviours.
12563
+ *
12564
+ * Scoped to the entity that declared the pose rather than broadcast, so two
12565
+ * objects listening for the same gesture stay independent.
12566
+ */
12567
+ dispatch(world, entityId, event, detail) {
12568
+ const script = world.getComponent(entityId, ScriptComponent);
12569
+ const instances = script?.scriptInstances;
12570
+ if (!instances) return;
12571
+ for (const instance of instances.values()) {
12572
+ if (!(instance instanceof Behaviour)) continue;
12573
+ try {
12574
+ if (event === "start") instance.onHandPoseStart(detail);
12575
+ else instance.onHandPoseEnd(detail);
12576
+ } catch (error) {
12577
+ console.error("[XRHandPoseSystem] Behaviour hook failed:", error);
12578
+ }
12579
+ }
12580
+ }
12581
+ resetAll() {
12582
+ for (const trackers of this.trackers.values()) {
12583
+ for (const tracker of trackers.values()) tracker.reset();
12584
+ }
12585
+ this.shapes.clear();
12586
+ }
12587
+ /** Live shape for a hand — powers the editor's pose readout. */
12588
+ getHandShape(handedness) {
12589
+ return this.shapes.get(handedness) ?? null;
12590
+ }
12591
+ /** Whether a given entity's pose is currently held. */
12592
+ isPoseActive(entityId, poseId, handedness) {
12593
+ return this.trackers.get(handedness)?.get(`${entityId}:${poseId}`)?.isActive() ?? false;
12594
+ }
12595
+ };
12596
+ __name(_XRHandPoseSystem, "XRHandPoseSystem");
12597
+ var XRHandPoseSystem = _XRHandPoseSystem;
11410
12598
 
11411
12599
  // core/World.ts
11412
12600
  var _World = class _World {
@@ -12790,15 +13978,26 @@ exports.AnimationStateManager = AnimationStateManager;
12790
13978
  exports.AnimationSystem = AnimationSystem;
12791
13979
  exports.AudioSystem = AudioSystem;
12792
13980
  exports.Behaviour = Behaviour;
13981
+ exports.CURLED_COS = CURLED_COS;
12793
13982
  exports.CameraSyncSystem = CameraSyncSystem;
12794
13983
  exports.Component = Component;
12795
13984
  exports.CoreComponents = components_exports;
13985
+ exports.DEFAULT_ENTER_CONFIDENCE = DEFAULT_ENTER_CONFIDENCE;
13986
+ exports.DEFAULT_EXIT_CONFIDENCE = DEFAULT_EXIT_CONFIDENCE;
13987
+ exports.DEFAULT_LOST_GRACE_SECONDS = DEFAULT_LOST_GRACE_SECONDS;
13988
+ exports.DEFAULT_MIN_HOLD_SECONDS = DEFAULT_MIN_HOLD_SECONDS;
12796
13989
  exports.EnvironmentSyncSystem = EnvironmentSyncSystem;
13990
+ exports.FALLBACK_HAND_SCALE = FALLBACK_HAND_SCALE;
13991
+ exports.HAND_POSE_PRESETS = HAND_POSE_PRESETS;
13992
+ exports.HAND_POSE_PRESET_IDS = HAND_POSE_PRESET_IDS;
12797
13993
  exports.HYDRATABLE_ENTITY_KINDS = HYDRATABLE_ENTITY_KINDS;
13994
+ exports.HandPoseTracker = HandPoseTracker;
12798
13995
  exports.InputSystem = InputSystem;
12799
13996
  exports.InstancedMeshSystem = InstancedMeshSystem;
12800
13997
  exports.KeyboardKeys = KeyboardKeys;
12801
13998
  exports.LightSyncSystem = LightSyncSystem;
13999
+ exports.MATCH_FALLOFF = MATCH_FALLOFF;
14000
+ exports.MAX_SAMPLE_AGE_SECONDS = MAX_SAMPLE_AGE_SECONDS;
12802
14001
  exports.MaterialSyncSystem = MaterialSyncSystem;
12803
14002
  exports.MeshGeometrySystem = MeshGeometrySystem;
12804
14003
  exports.NODE_KEY_SEPARATOR = NODE_KEY_SEPARATOR;
@@ -12807,6 +14006,7 @@ exports.OVERRIDE_DIFF_EXCLUDED_COMPONENTS = OVERRIDE_DIFF_EXCLUDED_COMPONENTS;
12807
14006
  exports.OVERRIDE_ROOT_EXCLUDED_COMPONENTS = OVERRIDE_ROOT_EXCLUDED_COMPONENTS;
12808
14007
  exports.ObjectManagementSystem = ObjectManagementSystem;
12809
14008
  exports.ObjectVisibilitySystem = ObjectVisibilitySystem;
14009
+ exports.PINCH_FINGERS = PINCH_FINGERS;
12810
14010
  exports.PREFAB_FILE_VERSION = PREFAB_FILE_VERSION;
12811
14011
  exports.PREFAB_FILE_VERSION_NESTED = PREFAB_FILE_VERSION_NESTED;
12812
14012
  exports.PhysicsSyncSystem = PhysicsSyncSystem;
@@ -12814,19 +14014,25 @@ exports.PostprocessingSyncSystem = PostprocessingSyncSystem;
12814
14014
  exports.RenderSystem = RenderSystem;
12815
14015
  exports.SCRIPT_OBJECT_ROOT_FLAG = SCRIPT_OBJECT_ROOT_FLAG;
12816
14016
  exports.SHARED_MODEL_RESOURCE_FLAG = SHARED_MODEL_RESOURCE_FLAG;
14017
+ exports.STRAIGHT_COS = STRAIGHT_COS;
12817
14018
  exports.ScriptBehaviourSystem = ScriptBehaviourSystem;
12818
14019
  exports.ScriptObject = ScriptObject;
12819
14020
  exports.ScriptObjectSystem = ScriptObjectSystem;
14021
+ exports.ThrowVelocityTracker = ThrowVelocityTracker;
12820
14022
  exports.TransformSyncSystem = TransformSyncSystem;
14023
+ exports.VELOCITY_SAMPLES = VELOCITY_SAMPLES;
12821
14024
  exports.VRTeleportAreaSystem = VRTeleportAreaSystem;
12822
14025
  exports.VideoSystem = VideoSystem;
12823
14026
  exports.World = World;
12824
14027
  exports.XRControllerVisualSystem = XRControllerVisualSystem;
14028
+ exports.XRHandPoseSystem = XRHandPoseSystem;
12825
14029
  exports.XRInputSystem = XRInputSystem;
12826
14030
  exports.XRInteractionSystem = XRInteractionSystem;
12827
14031
  exports.XRLocomotionSystem = XRLocomotionSystem;
12828
14032
  exports.XRSessionSystem = XRSessionSystem;
12829
14033
  exports.XRTeleportVisualSystem = XRTeleportVisualSystem;
14034
+ exports.XR_FINGERS = XR_FINGERS;
14035
+ exports.angularVelocityFrom = angularVelocityFrom;
12830
14036
  exports.applyHandMaterial = applyHandMaterial;
12831
14037
  exports.applyPrefabToSnapshotInstances = applyPrefabToSnapshotInstances;
12832
14038
  exports.arcGroundCrossing = arcGroundCrossing;
@@ -12836,11 +14042,18 @@ exports.buildNodeMapsFromSnapshot = buildNodeMapsFromSnapshot;
12836
14042
  exports.canonicalizeComponentValue = canonicalizeComponentValue;
12837
14043
  exports.clearModelCache = clearModelCache;
12838
14044
  exports.clearPrefabCache = clearPrefabCache;
14045
+ exports.cloneHandPose = cloneHandPose;
12839
14046
  exports.collectDiffPaths = collectDiffPaths;
12840
14047
  exports.collectDirectPrefabDependencies = collectDirectPrefabDependencies;
12841
14048
  exports.collectEntitySubtree = collectEntitySubtree;
12842
14049
  exports.collectSnapshotSubtreeIds = collectSnapshotSubtreeIds;
14050
+ exports.computeFingerCurl = computeFingerCurl;
14051
+ exports.computeHandShape = computeHandShape;
12843
14052
  exports.computeInstanceOverrides = computeInstanceOverrides;
14053
+ exports.computePalmPose = computePalmPose;
14054
+ exports.computePalmWidth = computePalmWidth;
14055
+ exports.computePinch = computePinch;
14056
+ exports.computeSpread = computeSpread;
12844
14057
  exports.createAnimationComponent = createAnimationComponent;
12845
14058
  exports.createAudioComponent = createAudioComponent;
12846
14059
  exports.createCameraComponent = createCameraComponent;
@@ -12870,6 +14083,7 @@ exports.createSpriteComponent = createSpriteComponent;
12870
14083
  exports.createTransformComponent = createTransformComponent;
12871
14084
  exports.createVRTeleportAreaComponent = createVRTeleportAreaComponent;
12872
14085
  exports.createVideoComponent = createVideoComponent;
14086
+ exports.createXRHandPoseComponent = createXRHandPoseComponent;
12873
14087
  exports.createXRInteractableComponent = createXRInteractableComponent;
12874
14088
  exports.createXRInteractorComponent = createXRInteractorComponent;
12875
14089
  exports.createXRInteractorSideComponent = createXRInteractorSideComponent;
@@ -12877,15 +14091,19 @@ exports.createXRRigComponent = createXRRigComponent;
12877
14091
  exports.decanonicalizeComponentValue = decanonicalizeComponentValue;
12878
14092
  exports.despawnPrefab = despawnPrefab;
12879
14093
  exports.encodeOverride = encodeOverride;
14094
+ exports.estimateHandScale = estimateHandScale;
12880
14095
  exports.findSnapshotRootEntityIds = findSnapshotRootEntityIds;
12881
14096
  exports.findTopLevelNestedRegions = findTopLevelNestedRegions;
14097
+ exports.getHandPosePreset = getHandPosePreset;
12882
14098
  exports.groupOverridePaths = groupOverridePaths;
14099
+ exports.handPoseFromCurls = handPoseFromCurls;
12883
14100
  exports.hydrateWorldFromSnapshot = hydrateWorldFromSnapshot;
12884
14101
  exports.hydrateWorldFromSnapshotDetailed = hydrateWorldFromSnapshotDetailed;
12885
14102
  exports.indexSnapshotByEntity = indexSnapshotByEntity;
12886
14103
  exports.instantiateSubtree = instantiateSubtree;
12887
14104
  exports.loadPrefab = loadPrefab;
12888
14105
  exports.loadSharedGltf = loadSharedGltf;
14106
+ exports.matchHandPose = matchHandPose;
12889
14107
  exports.mergeComponentValue = mergeComponentValue;
12890
14108
  exports.parseOverride = parseOverride;
12891
14109
  exports.parsePrefabAsset = parsePrefabAsset;