@relu-interactives/spatial-ecs 0.3.7 → 0.3.9

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-O6FJA4XW.js → chunk-W5TSYV6Q.js} +946 -50
  12. package/dist/chunk-W5TSYV6Q.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-BltjPEhg.d.cts → index-BIQoaKAA.d.cts} +9 -3
  27. package/dist/{index-W8pUajCW.d.ts → index-BxdXgqiI.d.ts} +9 -3
  28. package/dist/{index-_q9TVDI5.d.ts → index-C7XQyrgE.d.ts} +79 -7
  29. package/dist/{index-B9nwRDf9.d.cts → index-DyN50JZo.d.cts} +79 -7
  30. package/dist/index.cjs +1205 -45
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +118 -9
  33. package/dist/index.d.ts +118 -9
  34. package/dist/index.js +8 -8
  35. package/dist/systems/index.cjs +1161 -45
  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-O6FJA4XW.js.map +0 -1
@@ -2099,6 +2099,22 @@ var _XRInteractableComponent = class _XRInteractableComponent extends Component
2099
2099
  this.grabbable = options?.grabbable ?? true;
2100
2100
  this.pointable = options?.pointable ?? true;
2101
2101
  this.grabMode = options?.grabMode ?? "kinematic";
2102
+ this.grabReach = options?.grabReach ?? "both";
2103
+ this.distanceBehavior = options?.distanceBehavior ?? "snap";
2104
+ this.grabGesture = options?.grabGesture ?? "any";
2105
+ this.grabRadius = options?.grabRadius ?? 0.15;
2106
+ this.grabOffsetPosition = options?.grabOffsetPosition ?? {
2107
+ x: 0,
2108
+ y: 0,
2109
+ z: 0
2110
+ };
2111
+ this.grabOffsetRotation = options?.grabOffsetRotation ?? {
2112
+ x: 0,
2113
+ y: 0,
2114
+ z: 0
2115
+ };
2116
+ this.throwOnRelease = options?.throwOnRelease ?? true;
2117
+ this.throwStrength = options?.throwStrength ?? 1;
2102
2118
  this.highlightOnHover = options?.highlightOnHover ?? true;
2103
2119
  }
2104
2120
  };
@@ -2143,6 +2159,209 @@ var _XRInteractorSideComponent = class _XRInteractorSideComponent extends Compon
2143
2159
  __name(_XRInteractorSideComponent, "XRInteractorSideComponent");
2144
2160
  var XRInteractorSideComponent = _XRInteractorSideComponent;
2145
2161
 
2162
+ // core/xr/handPosePresets.ts
2163
+ var WIDE = 0.35;
2164
+ var NARROW = 0.25;
2165
+ var EXTENDED = { curl: 0.05, tolerance: WIDE };
2166
+ var CURLED = { curl: 0.95, tolerance: WIDE };
2167
+ var THUMB_TUCKED = { curl: 0.7, tolerance: NARROW + 0.1 };
2168
+ var THUMB_RELAXED = { curl: 0.6, tolerance: 0.4 };
2169
+ function fingers(thumb, index, middle, ring, pinky) {
2170
+ return { thumb, index, middle, ring, pinky };
2171
+ }
2172
+ __name(fingers, "fingers");
2173
+ var THUMB_UP = {
2174
+ axis: "thumb",
2175
+ direction: "up",
2176
+ toleranceDegrees: 45
2177
+ };
2178
+ var THUMB_DOWN = {
2179
+ axis: "thumb",
2180
+ direction: "down",
2181
+ toleranceDegrees: 45
2182
+ };
2183
+ var HAND_POSE_PRESETS = {
2184
+ fist: {
2185
+ id: "fist",
2186
+ name: "Fist",
2187
+ fingers: fingers(THUMB_TUCKED, CURLED, CURLED, CURLED, CURLED)
2188
+ },
2189
+ "open-palm": {
2190
+ id: "open-palm",
2191
+ name: "Open Palm",
2192
+ fingers: fingers(EXTENDED, EXTENDED, EXTENDED, EXTENDED, EXTENDED)
2193
+ },
2194
+ point: {
2195
+ id: "point",
2196
+ name: "Point",
2197
+ fingers: fingers(THUMB_RELAXED, EXTENDED, CURLED, CURLED, CURLED)
2198
+ },
2199
+ "thumbs-up": {
2200
+ id: "thumbs-up",
2201
+ name: "Thumbs Up",
2202
+ fingers: fingers(EXTENDED, CURLED, CURLED, CURLED, CURLED),
2203
+ orientation: THUMB_UP
2204
+ },
2205
+ "thumbs-down": {
2206
+ id: "thumbs-down",
2207
+ name: "Thumbs Down",
2208
+ fingers: fingers(EXTENDED, CURLED, CURLED, CURLED, CURLED),
2209
+ orientation: THUMB_DOWN
2210
+ },
2211
+ peace: {
2212
+ id: "peace",
2213
+ name: "Peace",
2214
+ fingers: fingers(THUMB_RELAXED, EXTENDED, EXTENDED, CURLED, CURLED)
2215
+ },
2216
+ pinch: {
2217
+ id: "pinch",
2218
+ name: "Pinch",
2219
+ // Curl is left loose: what defines a pinch is the tips MEETING, and people
2220
+ // pinch with wildly different amounts of finger bend.
2221
+ fingers: fingers(
2222
+ { curl: 0.5, tolerance: 0.5 },
2223
+ { curl: 0.5, tolerance: 0.5 },
2224
+ { curl: 0.5, tolerance: 0.5 },
2225
+ { curl: 0.5, tolerance: 0.5 },
2226
+ { curl: 0.5, tolerance: 0.5 }
2227
+ ),
2228
+ pinch: { finger: "index", target: 1, tolerance: 0.25 }
2229
+ },
2230
+ ok: {
2231
+ id: "ok",
2232
+ name: "OK Sign",
2233
+ // Thumb and index meet in a ring while the rest stay up — that contrast with
2234
+ // a plain pinch is the whole gesture, so the other three are constrained.
2235
+ fingers: fingers(
2236
+ { curl: 0.5, tolerance: 0.4 },
2237
+ { curl: 0.55, tolerance: 0.35 },
2238
+ EXTENDED,
2239
+ EXTENDED,
2240
+ EXTENDED
2241
+ ),
2242
+ pinch: { finger: "index", target: 1, tolerance: 0.3 }
2243
+ },
2244
+ gun: {
2245
+ id: "gun",
2246
+ name: "Gun",
2247
+ fingers: fingers(EXTENDED, EXTENDED, CURLED, CURLED, CURLED)
2248
+ },
2249
+ rock: {
2250
+ id: "rock",
2251
+ name: "Rock Horns",
2252
+ fingers: fingers(THUMB_TUCKED, EXTENDED, CURLED, CURLED, EXTENDED)
2253
+ },
2254
+ shaka: {
2255
+ id: "shaka",
2256
+ name: "Shaka",
2257
+ fingers: fingers(EXTENDED, CURLED, CURLED, CURLED, EXTENDED)
2258
+ },
2259
+ "palm-up": {
2260
+ id: "palm-up",
2261
+ name: "Palm Up",
2262
+ fingers: fingers(EXTENDED, EXTENDED, EXTENDED, EXTENDED, EXTENDED),
2263
+ orientation: {
2264
+ axis: "palm-normal",
2265
+ direction: "up",
2266
+ toleranceDegrees: 40
2267
+ }
2268
+ }
2269
+ };
2270
+ function getHandPosePreset(id) {
2271
+ const preset = HAND_POSE_PRESETS[id];
2272
+ if (!preset) return null;
2273
+ return cloneHandPose(preset);
2274
+ }
2275
+ __name(getHandPosePreset, "getHandPosePreset");
2276
+ function cloneHandPose(definition) {
2277
+ return {
2278
+ ...definition,
2279
+ fingers: {
2280
+ thumb: { ...definition.fingers.thumb },
2281
+ index: { ...definition.fingers.index },
2282
+ middle: { ...definition.fingers.middle },
2283
+ ring: { ...definition.fingers.ring },
2284
+ pinky: { ...definition.fingers.pinky }
2285
+ },
2286
+ pinch: definition.pinch ? { ...definition.pinch } : void 0,
2287
+ orientation: definition.orientation ? { ...definition.orientation } : void 0
2288
+ };
2289
+ }
2290
+ __name(cloneHandPose, "cloneHandPose");
2291
+ function handPoseFromCurls(id, name, curls, options) {
2292
+ const tolerance = options?.tolerance ?? NARROW;
2293
+ const target = /* @__PURE__ */ __name((curl) => ({
2294
+ curl: Math.min(1, Math.max(0, curl)),
2295
+ tolerance
2296
+ }), "target");
2297
+ return {
2298
+ id,
2299
+ name,
2300
+ fingers: {
2301
+ thumb: target(curls.thumb),
2302
+ index: target(curls.index),
2303
+ middle: target(curls.middle),
2304
+ ring: target(curls.ring),
2305
+ pinky: target(curls.pinky)
2306
+ },
2307
+ pinch: options?.pinch,
2308
+ orientation: options?.orientation,
2309
+ hand: options?.hand
2310
+ };
2311
+ }
2312
+ __name(handPoseFromCurls, "handPoseFromCurls");
2313
+
2314
+ // core/components/XRHandPose.ts
2315
+ var DEFAULT_POSE_CURLS = {
2316
+ thumb: 0,
2317
+ index: 0,
2318
+ middle: 0,
2319
+ ring: 0,
2320
+ pinky: 0
2321
+ };
2322
+ function createHandPoseEntry(partial) {
2323
+ return {
2324
+ id: partial.id,
2325
+ name: partial.name ?? partial.id,
2326
+ preset: partial.preset ?? null,
2327
+ curls: { ...DEFAULT_POSE_CURLS, ...partial.curls },
2328
+ tolerance: partial.tolerance ?? 0.25,
2329
+ hand: partial.hand ?? null,
2330
+ orientation: partial.orientation ?? null,
2331
+ minHoldSeconds: partial.minHoldSeconds ?? 0.15,
2332
+ enabled: partial.enabled ?? true
2333
+ };
2334
+ }
2335
+ __name(createHandPoseEntry, "createHandPoseEntry");
2336
+ function handPoseDefinitionFor(entry, resolvePreset) {
2337
+ if (entry.preset) {
2338
+ const preset = resolvePreset(entry.preset);
2339
+ if (!preset) return null;
2340
+ return {
2341
+ ...preset,
2342
+ id: entry.id,
2343
+ name: entry.name,
2344
+ hand: entry.hand ?? preset.hand,
2345
+ orientation: entry.orientation ?? preset.orientation
2346
+ };
2347
+ }
2348
+ return handPoseFromCurls(entry.id, entry.name, entry.curls, {
2349
+ tolerance: entry.tolerance,
2350
+ orientation: entry.orientation ?? void 0,
2351
+ hand: entry.hand ?? void 0
2352
+ });
2353
+ }
2354
+ __name(handPoseDefinitionFor, "handPoseDefinitionFor");
2355
+ var _XRHandPoseComponent = class _XRHandPoseComponent extends Component {
2356
+ constructor(options) {
2357
+ super();
2358
+ this.enabled = options?.enabled ?? true;
2359
+ this.poses = (options?.poses ?? []).map((pose) => createHandPoseEntry(pose));
2360
+ }
2361
+ };
2362
+ __name(_XRHandPoseComponent, "XRHandPoseComponent");
2363
+ var XRHandPoseComponent = _XRHandPoseComponent;
2364
+
2146
2365
  // core/utils/dracoDecoderPath.ts
2147
2366
  var LOCAL_DRACO_DECODER_RELATIVE_PATH = "./assets/draco/";
2148
2367
  var DRACO_DECODER_CDN_PATH = "https://www.gstatic.com/draco/versioned/decoders/1.5.7/";
@@ -4941,6 +5160,10 @@ function buildComponentValueMaps(snapshot) {
4941
5160
  snapshot.components,
4942
5161
  "XRInteractorComponent"
4943
5162
  ),
5163
+ xrHandPoseValues: readByComponent(
5164
+ snapshot.components,
5165
+ "XRHandPoseComponent"
5166
+ ),
4944
5167
  xrInteractorSideValues: readByComponent(
4945
5168
  snapshot.components,
4946
5169
  "XRInteractorSideComponent"
@@ -5054,13 +5277,9 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
5054
5277
  runtimeMessage: typeof entry.runtimeMessage === "string" ? entry.runtimeMessage : void 0
5055
5278
  };
5056
5279
  }).filter((entry) => entry.className || entry.scriptUrl) : [];
5057
- if (!entries.length) {
5058
- return;
5280
+ if (entries.length) {
5281
+ world.addComponent(targetEntityId, new ScriptComponent(entries));
5059
5282
  }
5060
- world.addComponent(
5061
- targetEntityId,
5062
- new ScriptComponent(entries)
5063
- );
5064
5283
  }
5065
5284
  const sourceScriptObject = asRecord3(values.scriptObjectValues.get(sourceEntityId));
5066
5285
  if (sourceScriptObject) {
@@ -5109,10 +5328,14 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
5109
5328
  const sourceInteractable = asRecord3(
5110
5329
  values.xrInteractableValues.get(sourceEntityId)
5111
5330
  );
5112
- const interactable = world.getComponent(
5331
+ let interactable = world.getComponent(
5113
5332
  targetEntityId,
5114
5333
  XRInteractableComponent
5115
5334
  );
5335
+ if (sourceInteractable && !interactable) {
5336
+ interactable = new XRInteractableComponent();
5337
+ world.addComponent(targetEntityId, interactable);
5338
+ }
5116
5339
  if (sourceInteractable && interactable) {
5117
5340
  if (typeof sourceInteractable.enabled === "boolean") {
5118
5341
  interactable.enabled = sourceInteractable.enabled;
@@ -5126,11 +5349,69 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
5126
5349
  if (typeof sourceInteractable.grabMode === "string") {
5127
5350
  interactable.grabMode = sourceInteractable.grabMode;
5128
5351
  }
5352
+ if (typeof sourceInteractable.grabReach === "string") {
5353
+ interactable.grabReach = sourceInteractable.grabReach;
5354
+ }
5355
+ if (typeof sourceInteractable.distanceBehavior === "string") {
5356
+ interactable.distanceBehavior = sourceInteractable.distanceBehavior;
5357
+ }
5358
+ if (typeof sourceInteractable.grabGesture === "string") {
5359
+ interactable.grabGesture = sourceInteractable.grabGesture;
5360
+ }
5361
+ if (typeof sourceInteractable.grabRadius === "number") {
5362
+ interactable.grabRadius = sourceInteractable.grabRadius;
5363
+ }
5364
+ if (typeof sourceInteractable.throwOnRelease === "boolean") {
5365
+ interactable.throwOnRelease = sourceInteractable.throwOnRelease;
5366
+ }
5367
+ if (typeof sourceInteractable.throwStrength === "number") {
5368
+ interactable.throwStrength = sourceInteractable.throwStrength;
5369
+ }
5370
+ const grabPos = asRecord3(sourceInteractable.grabOffsetPosition);
5371
+ if (grabPos) {
5372
+ interactable.grabOffsetPosition = {
5373
+ x: typeof grabPos.x === "number" ? grabPos.x : 0,
5374
+ y: typeof grabPos.y === "number" ? grabPos.y : 0,
5375
+ z: typeof grabPos.z === "number" ? grabPos.z : 0
5376
+ };
5377
+ }
5378
+ const grabRot = asRecord3(sourceInteractable.grabOffsetRotation);
5379
+ if (grabRot) {
5380
+ interactable.grabOffsetRotation = {
5381
+ x: typeof grabRot.x === "number" ? grabRot.x : 0,
5382
+ y: typeof grabRot.y === "number" ? grabRot.y : 0,
5383
+ z: typeof grabRot.z === "number" ? grabRot.z : 0
5384
+ };
5385
+ }
5129
5386
  if (typeof sourceInteractable.highlightOnHover === "boolean") {
5130
5387
  interactable.highlightOnHover = sourceInteractable.highlightOnHover;
5131
5388
  }
5132
5389
  }
5133
5390
  }
5391
+ {
5392
+ const sourceHandPose = asRecord3(
5393
+ values.xrHandPoseValues.get(sourceEntityId)
5394
+ );
5395
+ let handPose = world.getComponent(targetEntityId, XRHandPoseComponent);
5396
+ if (sourceHandPose && !handPose) {
5397
+ handPose = new XRHandPoseComponent();
5398
+ world.addComponent(targetEntityId, handPose);
5399
+ }
5400
+ if (sourceHandPose && handPose) {
5401
+ if (typeof sourceHandPose.enabled === "boolean") {
5402
+ handPose.enabled = sourceHandPose.enabled;
5403
+ }
5404
+ if (Array.isArray(sourceHandPose.poses)) {
5405
+ handPose.poses = sourceHandPose.poses.map((entry) => asRecord3(entry)).filter(
5406
+ (entry) => entry !== null && typeof entry.id === "string"
5407
+ ).map(
5408
+ (entry) => createHandPoseEntry(
5409
+ entry
5410
+ )
5411
+ );
5412
+ }
5413
+ }
5414
+ }
5134
5415
  {
5135
5416
  const sourceInteractor = asRecord3(
5136
5417
  values.xrInteractorValues.get(sourceEntityId)
@@ -6517,6 +6798,17 @@ var _Behaviour = class _Behaviour {
6517
6798
  *
6518
6799
  * @param input - The source doing the grabbing.
6519
6800
  */
6801
+ /**
6802
+ * A hand pose declared on this entity started being held.
6803
+ *
6804
+ * Fires once the pose has been sustained for its hold time, not the instant
6805
+ * the fingers pass through it.
6806
+ */
6807
+ onHandPoseStart(_event) {
6808
+ }
6809
+ /** A held hand pose ended, or its hand stopped being tracked. */
6810
+ onHandPoseEnd(_event) {
6811
+ }
6520
6812
  onGrab(input) {
6521
6813
  }
6522
6814
  /** Called when this entity is released. Optional XR hook. */
@@ -7659,13 +7951,65 @@ function blankState(source) {
7659
7951
  squeezeEnd: false,
7660
7952
  axes: [],
7661
7953
  hasAxes: false,
7662
- buttons: []
7954
+ buttons: [],
7955
+ joints: null
7663
7956
  };
7664
7957
  }
7665
7958
  __name(blankState, "blankState");
7959
+ var JOINT_MATRIX_BUFFER = new Float32Array(25 * 16);
7960
+ function readHandJoints(frame, hand, referenceSpace, rigMatrix, existing) {
7961
+ const names = [];
7962
+ const spaces = [];
7963
+ for (const [name, space] of hand) {
7964
+ names.push(name);
7965
+ spaces.push(space);
7966
+ }
7967
+ if (spaces.length === 0) return null;
7968
+ const target = existing ?? {};
7969
+ const matrix = new THREE14__namespace.Matrix4();
7970
+ const buffer = spaces.length * 16 <= JOINT_MATRIX_BUFFER.length ? JOINT_MATRIX_BUFFER : new Float32Array(spaces.length * 16);
7971
+ const fillPoses = frame.fillPoses;
7972
+ if (typeof fillPoses === "function") {
7973
+ let filled = false;
7974
+ try {
7975
+ filled = fillPoses.call(frame, spaces, referenceSpace, buffer);
7976
+ } catch {
7977
+ filled = false;
7978
+ }
7979
+ if (filled) {
7980
+ for (let i = 0; i < names.length; i += 1) {
7981
+ matrix.fromArray(buffer, i * 16).premultiply(rigMatrix);
7982
+ const vector = target[names[i]] ?? new THREE14__namespace.Vector3();
7983
+ vector.setFromMatrixPosition(matrix);
7984
+ target[names[i]] = vector;
7985
+ }
7986
+ return target;
7987
+ }
7988
+ }
7989
+ let anyTracked = false;
7990
+ for (let i = 0; i < names.length; i += 1) {
7991
+ const pose = frame.getJointPose?.(
7992
+ spaces[i],
7993
+ referenceSpace
7994
+ );
7995
+ if (!pose) {
7996
+ target[names[i]] = void 0;
7997
+ continue;
7998
+ }
7999
+ matrix.fromArray(pose.transform.matrix).premultiply(rigMatrix);
8000
+ const vector = target[names[i]] ?? new THREE14__namespace.Vector3();
8001
+ vector.setFromMatrixPosition(matrix);
8002
+ target[names[i]] = vector;
8003
+ anyTracked = true;
8004
+ }
8005
+ return anyTracked ? target : null;
8006
+ }
8007
+ __name(readHandJoints, "readHandJoints");
7666
8008
  var _XRInputSystem = class _XRInputSystem {
7667
8009
  constructor() {
7668
8010
  this.records = /* @__PURE__ */ new Map();
8011
+ /** Reference-space → world matrix, rebuilt each frame from the XR rig. */
8012
+ this.referenceToWorld = new THREE14__namespace.Matrix4();
7669
8013
  this.session = null;
7670
8014
  this.onSelectStart = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSelectStart"), "onSelectStart");
7671
8015
  this.onSelectEnd = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSelectEnd"), "onSelectEnd");
@@ -7745,6 +8089,14 @@ var _XRInputSystem = class _XRInputSystem {
7745
8089
  const referenceSpace = renderer.xr.getReferenceSpace();
7746
8090
  const frame = renderer.xr.getFrame?.();
7747
8091
  const live = /* @__PURE__ */ new Set();
8092
+ const rigMatrix = this.referenceToWorld;
8093
+ const rig = world.getCamera().parent;
8094
+ if (rig) {
8095
+ rig.updateMatrixWorld();
8096
+ rigMatrix.copy(rig.matrixWorld);
8097
+ } else {
8098
+ rigMatrix.identity();
8099
+ }
7748
8100
  for (const source of session.inputSources) {
7749
8101
  live.add(source);
7750
8102
  const record = this.ensureRecord(source);
@@ -7755,7 +8107,7 @@ var _XRInputSystem = class _XRInputSystem {
7755
8107
  if (frame && referenceSpace && source.targetRaySpace) {
7756
8108
  const pose = frame.getPose(source.targetRaySpace, referenceSpace);
7757
8109
  if (pose) {
7758
- const m = new THREE14__namespace.Matrix4().fromArray(pose.transform.matrix);
8110
+ const m = new THREE14__namespace.Matrix4().fromArray(pose.transform.matrix).premultiply(rigMatrix);
7759
8111
  state.rayOrigin.setFromMatrixPosition(m);
7760
8112
  state.rayDirection.set(0, 0, -1).applyQuaternion(
7761
8113
  new THREE14__namespace.Quaternion().setFromRotationMatrix(m)
@@ -7765,7 +8117,7 @@ var _XRInputSystem = class _XRInputSystem {
7765
8117
  if (frame && referenceSpace && source.gripSpace) {
7766
8118
  const gripPose = frame.getPose(source.gripSpace, referenceSpace);
7767
8119
  if (gripPose) {
7768
- const m = new THREE14__namespace.Matrix4().fromArray(gripPose.transform.matrix);
8120
+ const m = new THREE14__namespace.Matrix4().fromArray(gripPose.transform.matrix).premultiply(rigMatrix);
7769
8121
  state.gripPosition ?? (state.gripPosition = new THREE14__namespace.Vector3());
7770
8122
  state.gripQuaternion ?? (state.gripQuaternion = new THREE14__namespace.Quaternion());
7771
8123
  state.gripPosition.setFromMatrixPosition(m);
@@ -7775,6 +8127,17 @@ var _XRInputSystem = class _XRInputSystem {
7775
8127
  state.gripPosition = null;
7776
8128
  state.gripQuaternion = null;
7777
8129
  }
8130
+ if (frame && referenceSpace && source.hand) {
8131
+ state.joints = readHandJoints(
8132
+ frame,
8133
+ source.hand,
8134
+ referenceSpace,
8135
+ rigMatrix,
8136
+ state.joints
8137
+ );
8138
+ } else {
8139
+ state.joints = null;
8140
+ }
7778
8141
  const gamepad = source.gamepad;
7779
8142
  if (gamepad) {
7780
8143
  state.axes = Array.from(gamepad.axes);
@@ -7839,20 +8202,86 @@ var _XRInputSystem = class _XRInputSystem {
7839
8202
  };
7840
8203
  __name(_XRInputSystem, "XRInputSystem");
7841
8204
  var XRInputSystem = _XRInputSystem;
8205
+ var VELOCITY_SAMPLES = 5;
8206
+ var MAX_SAMPLE_AGE_SECONDS = 0.2;
8207
+ var MAX_PLAUSIBLE_SPEED = 20;
8208
+ var _ThrowVelocityTracker = class _ThrowVelocityTracker {
8209
+ constructor() {
8210
+ this.samples = [];
8211
+ }
8212
+ /** Record where the hand is now. Call once per frame while held. */
8213
+ record(position, time) {
8214
+ this.samples.push({ position: position.clone(), time });
8215
+ if (this.samples.length > VELOCITY_SAMPLES) {
8216
+ this.samples.shift();
8217
+ }
8218
+ }
8219
+ /**
8220
+ * Velocity to release with, in metres/second.
8221
+ *
8222
+ * Uses the FASTEST segment inside the window rather than the most recent or
8223
+ * the window average. Users decelerate as they open their hand, so the last
8224
+ * segment understates the throw and the average washes it out; peak speed is
8225
+ * what matches the motion the user actually felt they made.
8226
+ */
8227
+ computeVelocity(now) {
8228
+ const fresh = this.samples.filter(
8229
+ (sample) => now - sample.time <= MAX_SAMPLE_AGE_SECONDS
8230
+ );
8231
+ if (fresh.length < 2) return new THREE14__namespace.Vector3();
8232
+ let best = new THREE14__namespace.Vector3();
8233
+ let bestSpeed = 0;
8234
+ for (let i = 1; i < fresh.length; i += 1) {
8235
+ const previous = fresh[i - 1];
8236
+ const current = fresh[i];
8237
+ const dt = current.time - previous.time;
8238
+ if (dt <= 1e-5) continue;
8239
+ const velocity = new THREE14__namespace.Vector3().subVectors(current.position, previous.position).divideScalar(dt);
8240
+ const speed = velocity.length();
8241
+ if (speed > MAX_PLAUSIBLE_SPEED) continue;
8242
+ if (speed > bestSpeed) {
8243
+ bestSpeed = speed;
8244
+ best = velocity;
8245
+ }
8246
+ }
8247
+ return best;
8248
+ }
8249
+ clear() {
8250
+ this.samples.length = 0;
8251
+ }
8252
+ };
8253
+ __name(_ThrowVelocityTracker, "ThrowVelocityTracker");
8254
+ var ThrowVelocityTracker = _ThrowVelocityTracker;
8255
+ function angularVelocityFrom(previous, current, deltaTime) {
8256
+ if (deltaTime <= 1e-5) return new THREE14__namespace.Vector3();
8257
+ const delta = current.clone().multiply(previous.clone().invert());
8258
+ if (delta.w < 0) {
8259
+ delta.set(-delta.x, -delta.y, -delta.z, -delta.w);
8260
+ }
8261
+ const angle = 2 * Math.acos(THREE14__namespace.MathUtils.clamp(delta.w, -1, 1));
8262
+ const sinHalf = Math.sqrt(Math.max(0, 1 - delta.w * delta.w));
8263
+ if (sinHalf < 1e-6 || angle < 1e-6) return new THREE14__namespace.Vector3();
8264
+ return new THREE14__namespace.Vector3(delta.x, delta.y, delta.z).divideScalar(sinHalf).multiplyScalar(angle / deltaTime);
8265
+ }
8266
+ __name(angularVelocityFrom, "angularVelocityFrom");
8267
+
8268
+ // core/systems/XRInteractionSystem.ts
7842
8269
  var RAYLESS_HOLD_DISTANCE = 0.5;
7843
8270
  var _XRInteractionSystem = class _XRInteractionSystem {
7844
8271
  constructor(inputSystem) {
7845
8272
  this.raycaster = new THREE14__namespace.Raycaster();
7846
- /** Which entity each source is holding. Keyed by source state identity. */
7847
8273
  this.grabs = /* @__PURE__ */ new Map();
7848
8274
  this.hoveredEntityId = null;
8275
+ /** Monotonic seconds, accumulated from deltaTime for velocity sampling. */
8276
+ this.elapsed = 0;
7849
8277
  this.inputSystem = inputSystem;
7850
8278
  }
7851
- update(world) {
8279
+ update(world, deltaTime) {
7852
8280
  if (!world.isXrPresenting()) {
7853
8281
  this.releaseAll(world);
7854
8282
  return;
7855
8283
  }
8284
+ this.elapsed += Math.max(0, deltaTime);
7856
8285
  const inputs = this.inputSystem.getInputs();
7857
8286
  const interactables = this.collectInteractables(world);
7858
8287
  this.hoveredEntityId = null;
@@ -7863,32 +8292,105 @@ var _XRInteractionSystem = class _XRInteractionSystem {
7863
8292
  }
7864
8293
  const held = this.grabs.get(input);
7865
8294
  if (held) {
7866
- this.moveHeld(world, input, held);
7867
- if (input.selectEnd) this.release(world, input);
8295
+ this.moveHeld(world, input, held, deltaTime);
8296
+ if (this.wantsRelease(world, input, held)) {
8297
+ this.release(world, input);
8298
+ }
7868
8299
  continue;
7869
8300
  }
7870
- const hitEntityId = this.raycast(input, interactables);
7871
- if (hitEntityId === null) continue;
8301
+ const candidate = this.findGrabCandidate(world, input, interactables);
8302
+ if (candidate === null) continue;
7872
8303
  if (this.hoveredEntityId === null) {
7873
- this.hoveredEntityId = hitEntityId;
8304
+ this.hoveredEntityId = candidate.entityId;
7874
8305
  }
7875
8306
  const interactable = world.getComponent(
7876
- hitEntityId,
8307
+ candidate.entityId,
7877
8308
  XRInteractableComponent
7878
8309
  );
7879
- if (input.selectStart && interactable?.grabbable) {
7880
- this.grab(world, input, hitEntityId);
8310
+ if (interactable?.grabbable && this.gestureMatches(
8311
+ input,
8312
+ interactable,
8313
+ /* starting */
8314
+ true
8315
+ )) {
8316
+ this.grab(world, input, candidate.entityId, candidate.viaDistance);
8317
+ }
8318
+ }
8319
+ }
8320
+ /** Whether the grab gesture for this object is currently starting. */
8321
+ gestureMatches(input, interactable, starting) {
8322
+ const pinch = starting ? input.selectStart : input.select;
8323
+ const full = starting ? input.squeezeStart : input.squeeze;
8324
+ switch (interactable.grabGesture) {
8325
+ case "pinch":
8326
+ return pinch;
8327
+ case "full":
8328
+ return full;
8329
+ default:
8330
+ return pinch || full;
8331
+ }
8332
+ }
8333
+ /** True once the gesture that started the grab has ended. */
8334
+ wantsRelease(world, input, held) {
8335
+ const interactable = world.getComponent(
8336
+ held.entityId,
8337
+ XRInteractableComponent
8338
+ );
8339
+ if (!interactable) return true;
8340
+ switch (interactable.grabGesture) {
8341
+ case "pinch":
8342
+ return input.selectEnd || !input.select;
8343
+ case "full":
8344
+ return input.squeezeEnd || !input.squeeze;
8345
+ default:
8346
+ return !input.select && !input.squeeze;
8347
+ }
8348
+ }
8349
+ /**
8350
+ * Find what this hand would grab: a physically-reachable object first, then
8351
+ * one along the pointer ray.
8352
+ *
8353
+ * Proximity wins over the ray so that reaching for something you are touching
8354
+ * never picks up a distant object behind it, which feels broken.
8355
+ */
8356
+ findGrabCandidate(world, input, interactables) {
8357
+ const hand = this.handPosition(input);
8358
+ let nearestId = null;
8359
+ let nearestDistance = Number.POSITIVE_INFINITY;
8360
+ for (const entry of interactables) {
8361
+ const interactable2 = world.getComponent(
8362
+ entry.entityId,
8363
+ XRInteractableComponent
8364
+ );
8365
+ if (!interactable2) continue;
8366
+ if (interactable2.grabReach === "distance") continue;
8367
+ const position = new THREE14__namespace.Vector3();
8368
+ entry.object.getWorldPosition(position);
8369
+ const distance = position.distanceTo(hand);
8370
+ if (distance <= interactable2.grabRadius && distance < nearestDistance) {
8371
+ nearestDistance = distance;
8372
+ nearestId = entry.entityId;
7881
8373
  }
7882
8374
  }
8375
+ if (nearestId !== null) {
8376
+ return { entityId: nearestId, viaDistance: false };
8377
+ }
8378
+ const hitId = this.raycast(input, interactables);
8379
+ if (hitId === null) return null;
8380
+ const interactable = world.getComponent(hitId, XRInteractableComponent);
8381
+ const viaDistanceAllowed = interactable?.grabReach === "distance" || interactable?.grabReach === "both";
8382
+ return { entityId: hitId, viaDistance: viaDistanceAllowed };
7883
8383
  }
7884
8384
  collectInteractables(world) {
7885
8385
  const out = [];
7886
8386
  for (const entityId of world.query(XRInteractableComponent, Object3DRef)) {
7887
8387
  const interactable = world.getComponent(entityId, XRInteractableComponent);
7888
- if (!interactable?.enabled || !interactable.pointable) continue;
8388
+ if (!interactable?.enabled) continue;
7889
8389
  if (!world.isEntityVisible(entityId)) continue;
7890
8390
  const object = world.getComponent(entityId, Object3DRef)?.object;
7891
- if (object) out.push({ entityId, object });
8391
+ if (object) {
8392
+ out.push({ entityId, object, pointable: interactable.pointable });
8393
+ }
7892
8394
  }
7893
8395
  return out;
7894
8396
  }
@@ -7896,9 +8398,11 @@ var _XRInteractionSystem = class _XRInteractionSystem {
7896
8398
  raycast(input, interactables) {
7897
8399
  if (interactables.length === 0) return null;
7898
8400
  this.raycaster.set(input.rayOrigin, input.rayDirection);
8401
+ this.raycaster.far = Number.POSITIVE_INFINITY;
7899
8402
  let nearestId = null;
7900
8403
  let nearestDistance = Number.POSITIVE_INFINITY;
7901
8404
  for (const entry of interactables) {
8405
+ if (!entry.pointable) continue;
7902
8406
  const hit = this.raycaster.intersectObject(entry.object, true)[0];
7903
8407
  if (!hit) continue;
7904
8408
  if (hit.distance < nearestDistance) {
@@ -7908,43 +8412,120 @@ var _XRInteractionSystem = class _XRInteractionSystem {
7908
8412
  }
7909
8413
  return nearestId;
7910
8414
  }
7911
- grab(world, input, entityId) {
8415
+ grab(world, input, entityId, viaDistance) {
7912
8416
  const transform = world.getComponent(entityId, TransformComponent);
7913
- if (!transform) return;
8417
+ const interactable = world.getComponent(entityId, XRInteractableComponent);
8418
+ if (!transform || !interactable) return;
7914
8419
  const hand = this.handPosition(input);
7915
- const offset = new THREE14__namespace.Vector3(
7916
- transform.position.x - hand.x,
7917
- transform.position.y - hand.y,
7918
- transform.position.z - hand.z
7919
- );
8420
+ const handQuaternion = this.handQuaternion(input);
8421
+ const remote = viaDistance && interactable.distanceBehavior === "remote";
8422
+ let offset;
8423
+ let remoteDistance = 0;
8424
+ if (remote) {
8425
+ const position = new THREE14__namespace.Vector3(
8426
+ transform.position.x,
8427
+ transform.position.y,
8428
+ transform.position.z
8429
+ );
8430
+ remoteDistance = position.distanceTo(input.rayOrigin);
8431
+ offset = new THREE14__namespace.Vector3();
8432
+ } else if (viaDistance) {
8433
+ offset = new THREE14__namespace.Vector3(
8434
+ interactable.grabOffsetPosition.x,
8435
+ interactable.grabOffsetPosition.y,
8436
+ interactable.grabOffsetPosition.z
8437
+ );
8438
+ } else {
8439
+ offset = new THREE14__namespace.Vector3(
8440
+ transform.position.x - hand.x,
8441
+ transform.position.y - hand.y,
8442
+ transform.position.z - hand.z
8443
+ );
8444
+ }
7920
8445
  const physics = world.getComponent(entityId, PhysicsComponent);
7921
8446
  let restoreBodyType = null;
7922
- const interactable = world.getComponent(entityId, XRInteractableComponent);
7923
- if (physics && interactable?.grabMode === "kinematic") {
8447
+ if (physics) {
7924
8448
  restoreBodyType = physics.values.rigidbody.type;
7925
8449
  physics.values.rigidbody.type = "kinematicPosition";
7926
8450
  }
7927
- this.grabs.set(input, { entityId, offset, restoreBodyType });
8451
+ const tracker = new ThrowVelocityTracker();
8452
+ tracker.record(hand, this.elapsed);
8453
+ this.grabs.set(input, {
8454
+ entityId,
8455
+ offset,
8456
+ rotationOffset: new THREE14__namespace.Quaternion().setFromEuler(
8457
+ new THREE14__namespace.Euler(
8458
+ interactable.grabOffsetRotation.x,
8459
+ interactable.grabOffsetRotation.y,
8460
+ interactable.grabOffsetRotation.z
8461
+ )
8462
+ ),
8463
+ remoteDistance,
8464
+ remote,
8465
+ restoreBodyType,
8466
+ tracker,
8467
+ lastHandQuaternion: handQuaternion.clone(),
8468
+ angularVelocity: new THREE14__namespace.Vector3()
8469
+ });
7928
8470
  this.dispatch(world, entityId, (b) => b.onGrab(input));
7929
8471
  }
7930
- moveHeld(world, input, held) {
8472
+ moveHeld(world, input, held, deltaTime) {
7931
8473
  const transform = world.getComponent(held.entityId, TransformComponent);
7932
8474
  if (!transform) return;
7933
8475
  const hand = this.handPosition(input);
7934
- transform.position.x = hand.x + held.offset.x;
7935
- transform.position.y = hand.y + held.offset.y;
7936
- transform.position.z = hand.z + held.offset.z;
8476
+ if (held.remote) {
8477
+ const target = new THREE14__namespace.Vector3().copy(input.rayDirection).multiplyScalar(held.remoteDistance).add(input.rayOrigin);
8478
+ transform.position.x = target.x;
8479
+ transform.position.y = target.y;
8480
+ transform.position.z = target.z;
8481
+ } else {
8482
+ transform.position.x = hand.x + held.offset.x;
8483
+ transform.position.y = hand.y + held.offset.y;
8484
+ transform.position.z = hand.z + held.offset.z;
8485
+ }
8486
+ held.tracker.record(
8487
+ held.remote ? new THREE14__namespace.Vector3(
8488
+ transform.position.x,
8489
+ transform.position.y,
8490
+ transform.position.z
8491
+ ) : hand,
8492
+ this.elapsed
8493
+ );
8494
+ const handQuaternion = this.handQuaternion(input);
8495
+ held.angularVelocity = angularVelocityFrom(
8496
+ held.lastHandQuaternion,
8497
+ handQuaternion,
8498
+ deltaTime
8499
+ );
8500
+ held.lastHandQuaternion = handQuaternion;
7937
8501
  }
7938
8502
  release(world, input) {
7939
8503
  const held = this.grabs.get(input);
7940
8504
  if (!held) return;
7941
8505
  this.grabs.delete(input);
7942
- if (held.restoreBodyType !== null) {
7943
- const physics = world.getComponent(held.entityId, PhysicsComponent);
7944
- if (physics) {
7945
- physics.values.rigidbody.type = held.restoreBodyType;
8506
+ const interactable = world.getComponent(
8507
+ held.entityId,
8508
+ XRInteractableComponent
8509
+ );
8510
+ const physics = world.getComponent(held.entityId, PhysicsComponent);
8511
+ if (held.restoreBodyType !== null && physics) {
8512
+ physics.values.rigidbody.type = held.restoreBodyType;
8513
+ const body = physics.rigidBody;
8514
+ const dynamic = held.restoreBodyType === "dynamic";
8515
+ if (body && dynamic && interactable?.throwOnRelease) {
8516
+ const velocity = held.tracker.computeVelocity(this.elapsed).multiplyScalar(interactable.throwStrength);
8517
+ try {
8518
+ body.setLinvel(
8519
+ { x: velocity.x, y: velocity.y, z: velocity.z },
8520
+ true
8521
+ );
8522
+ const spin = held.angularVelocity;
8523
+ body.setAngvel({ x: spin.x, y: spin.y, z: spin.z }, true);
8524
+ } catch {
8525
+ }
7946
8526
  }
7947
8527
  }
8528
+ held.tracker.clear();
7948
8529
  this.dispatch(world, held.entityId, (b) => b.onRelease(input));
7949
8530
  }
7950
8531
  releaseAll(world) {
@@ -7953,14 +8534,25 @@ var _XRInteractionSystem = class _XRInteractionSystem {
7953
8534
  }
7954
8535
  }
7955
8536
  /**
7956
- * Where the "hand" is for this source. Controllers and tracked hands report a
7957
- * grip pose; gaze and transient-pointer sources have none, so we hold the
7958
- * object a fixed distance along the ray instead.
8537
+ * Where the "hand" is. Controllers and tracked hands report a grip pose;
8538
+ * gaze and transient-pointer sources have none, so we hold the object a fixed
8539
+ * distance along the ray instead — which is what makes Vision Pro work.
7959
8540
  */
7960
8541
  handPosition(input) {
7961
8542
  if (input.gripPosition) return input.gripPosition;
7962
8543
  return new THREE14__namespace.Vector3().copy(input.rayDirection).multiplyScalar(RAYLESS_HOLD_DISTANCE).add(input.rayOrigin);
7963
8544
  }
8545
+ /**
8546
+ * Hand orientation. Grip-less sources (gaze / transient-pointer) have none,
8547
+ * so we derive one from the aim direction — enough for spin on release.
8548
+ */
8549
+ handQuaternion(input) {
8550
+ if (input.gripQuaternion) return input.gripQuaternion.clone();
8551
+ return new THREE14__namespace.Quaternion().setFromUnitVectors(
8552
+ new THREE14__namespace.Vector3(0, 0, -1),
8553
+ input.rayDirection.clone().normalize()
8554
+ );
8555
+ }
7964
8556
  dispatch(world, entityId, visit) {
7965
8557
  const script = world.getComponent(entityId, ScriptComponent);
7966
8558
  const instances = script?.scriptInstances;
@@ -8517,6 +9109,7 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
8517
9109
  this.handStyleSignature = "";
8518
9110
  this.handMaterial = null;
8519
9111
  this.interactionSystem = options?.interactionSystem ?? null;
9112
+ this.inputSystem = options?.inputSystem ?? null;
8520
9113
  this.group.name = "XRControllerVisuals";
8521
9114
  this.group.userData.ecsPreviewHidden = true;
8522
9115
  }
@@ -8533,8 +9126,11 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
8533
9126
  for (const { index, side } of SLOTS) {
8534
9127
  const visuals = this.slots.get(index);
8535
9128
  if (!visuals) continue;
8536
- visuals.controllerModel.visible = showControllers;
8537
- visuals.handModel.visible = showHands;
9129
+ const liveKind = this.liveKindFor(side);
9130
+ const hasController = liveKind === null || liveKind === "controller";
9131
+ const hasHand = liveKind === null || liveKind === "hand";
9132
+ visuals.controllerModel.visible = showControllers && hasController;
9133
+ visuals.handModel.visible = showHands && hasHand;
8538
9134
  visuals.ray.visible = this.shouldShowRay(interactor);
8539
9135
  if (showHands && this.handMaterial) {
8540
9136
  applyHandMaterial(visuals.handModel, this.handMaterial);
@@ -8542,6 +9138,20 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
8542
9138
  this.syncCustomContent(world, visuals, side);
8543
9139
  }
8544
9140
  }
9141
+ /**
9142
+ * What kind of source is currently driving this hand, or null when we cannot
9143
+ * tell (no input system wired) — in which case callers fall back to showing
9144
+ * everything rather than hiding the user's hands entirely.
9145
+ */
9146
+ liveKindFor(side) {
9147
+ if (!this.inputSystem) return null;
9148
+ const inputs = this.inputSystem.getInputs();
9149
+ if (inputs.length === 0) return null;
9150
+ for (const input of inputs) {
9151
+ if (input.handedness === side && input.connected) return input.kind;
9152
+ }
9153
+ return "none";
9154
+ }
8545
9155
  /** The scene's interactor config, if one was authored. */
8546
9156
  resolveInteractor(world) {
8547
9157
  const entityId = world.query(XRInteractorComponent)[0];
@@ -8801,6 +9411,511 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
8801
9411
  };
8802
9412
  __name(_XRTeleportVisualSystem, "XRTeleportVisualSystem");
8803
9413
  var XRTeleportVisualSystem = _XRTeleportVisualSystem;
9414
+ var XR_FINGERS = [
9415
+ "thumb",
9416
+ "index",
9417
+ "middle",
9418
+ "ring",
9419
+ "pinky"
9420
+ ];
9421
+ var PINCH_FINGERS = [
9422
+ "index",
9423
+ "middle",
9424
+ "ring",
9425
+ "pinky"
9426
+ ];
9427
+ var FINGER_CHAINS = {
9428
+ thumb: [
9429
+ "thumb-metacarpal",
9430
+ "thumb-phalanx-proximal",
9431
+ "thumb-phalanx-distal",
9432
+ "thumb-tip"
9433
+ ],
9434
+ index: [
9435
+ "index-finger-metacarpal",
9436
+ "index-finger-phalanx-proximal",
9437
+ "index-finger-phalanx-intermediate",
9438
+ "index-finger-phalanx-distal",
9439
+ "index-finger-tip"
9440
+ ],
9441
+ middle: [
9442
+ "middle-finger-metacarpal",
9443
+ "middle-finger-phalanx-proximal",
9444
+ "middle-finger-phalanx-intermediate",
9445
+ "middle-finger-phalanx-distal",
9446
+ "middle-finger-tip"
9447
+ ],
9448
+ ring: [
9449
+ "ring-finger-metacarpal",
9450
+ "ring-finger-phalanx-proximal",
9451
+ "ring-finger-phalanx-intermediate",
9452
+ "ring-finger-phalanx-distal",
9453
+ "ring-finger-tip"
9454
+ ],
9455
+ pinky: [
9456
+ "pinky-finger-metacarpal",
9457
+ "pinky-finger-phalanx-proximal",
9458
+ "pinky-finger-phalanx-intermediate",
9459
+ "pinky-finger-phalanx-distal",
9460
+ "pinky-finger-tip"
9461
+ ]
9462
+ };
9463
+ var TIP_JOINTS = {
9464
+ thumb: "thumb-tip",
9465
+ index: "index-finger-tip",
9466
+ middle: "middle-finger-tip",
9467
+ ring: "ring-finger-tip",
9468
+ pinky: "pinky-finger-tip"
9469
+ };
9470
+ var METACARPAL_JOINTS = {
9471
+ thumb: "thumb-metacarpal",
9472
+ index: "index-finger-metacarpal",
9473
+ middle: "middle-finger-metacarpal",
9474
+ ring: "ring-finger-metacarpal",
9475
+ pinky: "pinky-finger-metacarpal"
9476
+ };
9477
+ var FALLBACK_HAND_SCALE = 0.08;
9478
+ var STRAIGHT_COS = 0.95;
9479
+ var CURLED_COS = 0.55;
9480
+ function clamp01(value) {
9481
+ return value < 0 ? 0 : value > 1 ? 1 : value;
9482
+ }
9483
+ __name(clamp01, "clamp01");
9484
+ function boneDirections(joints, finger) {
9485
+ const chain = FINGER_CHAINS[finger];
9486
+ const directions = [];
9487
+ for (let i = 1; i < chain.length; i += 1) {
9488
+ const from = joints[chain[i - 1]];
9489
+ const to = joints[chain[i]];
9490
+ if (!from || !to) return null;
9491
+ const direction = new THREE14__namespace.Vector3().subVectors(to, from);
9492
+ if (direction.lengthSq() < 1e-12) return null;
9493
+ directions.push(direction.normalize());
9494
+ }
9495
+ return directions.length >= 2 ? directions : null;
9496
+ }
9497
+ __name(boneDirections, "boneDirections");
9498
+ function bendAmounts(directions) {
9499
+ const bends = [];
9500
+ for (let i = 1; i < directions.length; i += 1) {
9501
+ const cos = directions[i - 1].dot(directions[i]);
9502
+ const straightness = clamp01((cos - CURLED_COS) / (STRAIGHT_COS - CURLED_COS));
9503
+ bends.push(1 - straightness);
9504
+ }
9505
+ return bends;
9506
+ }
9507
+ __name(bendAmounts, "bendAmounts");
9508
+ function average(values) {
9509
+ if (values.length === 0) return 0;
9510
+ let sum = 0;
9511
+ for (const value of values) sum += value;
9512
+ return sum / values.length;
9513
+ }
9514
+ __name(average, "average");
9515
+ function computeFingerCurl(joints, finger) {
9516
+ const directions = boneDirections(joints, finger);
9517
+ if (!directions) return null;
9518
+ const bends = bendAmounts(directions);
9519
+ if (bends.length === 0) return null;
9520
+ return {
9521
+ baseCurl: bends[0],
9522
+ tipCurl: average(bends.slice(1)) || bends[bends.length - 1],
9523
+ curl: average(bends)
9524
+ };
9525
+ }
9526
+ __name(computeFingerCurl, "computeFingerCurl");
9527
+ function computePalmWidth(joints) {
9528
+ const index = joints["index-finger-metacarpal"];
9529
+ const pinky = joints["pinky-finger-metacarpal"];
9530
+ if (!index || !pinky) return null;
9531
+ const width = index.distanceTo(pinky);
9532
+ return width > 1e-6 ? width : null;
9533
+ }
9534
+ __name(computePalmWidth, "computePalmWidth");
9535
+ function estimateHandScale(joints) {
9536
+ const measurements = [];
9537
+ const width = computePalmWidth(joints);
9538
+ if (width !== null) measurements.push(width);
9539
+ const wrist = joints.wrist;
9540
+ const middleTip = joints["middle-finger-tip"];
9541
+ if (wrist && middleTip) {
9542
+ const span = wrist.distanceTo(middleTip);
9543
+ if (span > 1e-6) measurements.push(span * 0.5);
9544
+ }
9545
+ if (measurements.length === 0) return FALLBACK_HAND_SCALE;
9546
+ return average(measurements);
9547
+ }
9548
+ __name(estimateHandScale, "estimateHandScale");
9549
+ function computePinch(joints, finger) {
9550
+ const thumbTip = joints["thumb-tip"];
9551
+ const fingerTip = joints[TIP_JOINTS[finger]];
9552
+ if (!thumbTip || !fingerTip) return null;
9553
+ const scale = estimateHandScale(joints);
9554
+ const distance = thumbTip.distanceTo(fingerTip);
9555
+ return clamp01(1 - distance / (scale * 0.7));
9556
+ }
9557
+ __name(computePinch, "computePinch");
9558
+ function computeSpread(joints, a, b) {
9559
+ const aBase = joints[METACARPAL_JOINTS[a]];
9560
+ const aTip = joints[TIP_JOINTS[a]];
9561
+ const bBase = joints[METACARPAL_JOINTS[b]];
9562
+ const bTip = joints[TIP_JOINTS[b]];
9563
+ if (!aBase || !aTip || !bBase || !bTip) return null;
9564
+ const dirA = new THREE14__namespace.Vector3().subVectors(aTip, aBase);
9565
+ const dirB = new THREE14__namespace.Vector3().subVectors(bTip, bBase);
9566
+ if (dirA.lengthSq() < 1e-12 || dirB.lengthSq() < 1e-12) return null;
9567
+ return clamp01((1 - dirA.normalize().dot(dirB.normalize())) / 0.45);
9568
+ }
9569
+ __name(computeSpread, "computeSpread");
9570
+ function computePalmPose(joints, handedness) {
9571
+ const wrist = joints.wrist;
9572
+ const indexBase = joints["index-finger-metacarpal"];
9573
+ const pinkyBase = joints["pinky-finger-metacarpal"];
9574
+ if (!wrist || !indexBase || !pinkyBase) return null;
9575
+ const u = new THREE14__namespace.Vector3().subVectors(indexBase, wrist);
9576
+ const v = new THREE14__namespace.Vector3().subVectors(pinkyBase, wrist);
9577
+ const normal = new THREE14__namespace.Vector3().crossVectors(u, v);
9578
+ if (normal.lengthSq() < 1e-12) return null;
9579
+ normal.normalize();
9580
+ if (handedness === "left") normal.multiplyScalar(-1);
9581
+ const right = new THREE14__namespace.Vector3().subVectors(indexBase, pinkyBase);
9582
+ if (right.lengthSq() < 1e-12) return null;
9583
+ right.normalize();
9584
+ if (handedness === "left") right.multiplyScalar(-1);
9585
+ const up = new THREE14__namespace.Vector3().crossVectors(normal, right).normalize();
9586
+ const center = new THREE14__namespace.Vector3().add(wrist).add(indexBase).add(pinkyBase).divideScalar(3);
9587
+ return {
9588
+ center,
9589
+ normal,
9590
+ right,
9591
+ up,
9592
+ width: computePalmWidth(joints) ?? FALLBACK_HAND_SCALE
9593
+ };
9594
+ }
9595
+ __name(computePalmPose, "computePalmPose");
9596
+ function computeHandShape(joints, handedness) {
9597
+ const fingers2 = {};
9598
+ let tracked = 0;
9599
+ for (const finger of XR_FINGERS) {
9600
+ const curl = computeFingerCurl(joints, finger);
9601
+ if (curl) {
9602
+ fingers2[finger] = curl;
9603
+ tracked += 1;
9604
+ } else {
9605
+ fingers2[finger] = { baseCurl: 0, tipCurl: 0, curl: 0 };
9606
+ }
9607
+ }
9608
+ if (tracked === 0) return null;
9609
+ const pinch = {};
9610
+ for (const finger of PINCH_FINGERS) {
9611
+ pinch[finger] = computePinch(joints, finger) ?? 0;
9612
+ }
9613
+ const thumbBase = joints["thumb-metacarpal"];
9614
+ const thumbTip = joints["thumb-tip"];
9615
+ let thumbDirection = null;
9616
+ if (thumbBase && thumbTip) {
9617
+ const direction = new THREE14__namespace.Vector3().subVectors(thumbTip, thumbBase);
9618
+ if (direction.lengthSq() > 1e-12) thumbDirection = direction.normalize();
9619
+ }
9620
+ return {
9621
+ handedness,
9622
+ fingers: fingers2,
9623
+ pinch,
9624
+ spread: {
9625
+ thumbIndex: computeSpread(joints, "thumb", "index") ?? 0,
9626
+ indexMiddle: computeSpread(joints, "index", "middle") ?? 0,
9627
+ middleRing: computeSpread(joints, "middle", "ring") ?? 0,
9628
+ ringPinky: computeSpread(joints, "ring", "pinky") ?? 0
9629
+ },
9630
+ palm: computePalmPose(joints, handedness),
9631
+ scale: estimateHandScale(joints),
9632
+ thumbDirection
9633
+ };
9634
+ }
9635
+ __name(computeHandShape, "computeHandShape");
9636
+ var MATCH_FALLOFF = 0.25;
9637
+ var NO_MATCH = {
9638
+ matched: false,
9639
+ confidence: 0,
9640
+ worstConstraint: null
9641
+ };
9642
+ function scoreAgainst(actual, target, tolerance) {
9643
+ const error = Math.abs(actual - target);
9644
+ if (error <= tolerance) return 1;
9645
+ return clamp01(1 - (error - tolerance) / MATCH_FALLOFF);
9646
+ }
9647
+ __name(scoreAgainst, "scoreAgainst");
9648
+ var WORLD_DIRECTIONS = {
9649
+ up: new THREE14__namespace.Vector3(0, 1, 0),
9650
+ down: new THREE14__namespace.Vector3(0, -1, 0),
9651
+ forward: new THREE14__namespace.Vector3(0, 0, -1),
9652
+ back: new THREE14__namespace.Vector3(0, 0, 1),
9653
+ left: new THREE14__namespace.Vector3(-1, 0, 0),
9654
+ right: new THREE14__namespace.Vector3(1, 0, 0)
9655
+ };
9656
+ function axisVector(shape, axis) {
9657
+ if (axis === "thumb") return shape.thumbDirection;
9658
+ if (!shape.palm) return null;
9659
+ return axis === "palm-normal" ? shape.palm.normal : shape.palm.up;
9660
+ }
9661
+ __name(axisVector, "axisVector");
9662
+ function resolveDirection(direction, shape, context) {
9663
+ if (direction !== "toward-head" && direction !== "away-from-head") {
9664
+ return WORLD_DIRECTIONS[direction];
9665
+ }
9666
+ const head = context?.headPosition;
9667
+ const origin = shape.palm?.center;
9668
+ if (!head || !origin) return null;
9669
+ const toHead = new THREE14__namespace.Vector3().subVectors(head, origin);
9670
+ if (toHead.lengthSq() < 1e-12) return null;
9671
+ toHead.normalize();
9672
+ return direction === "toward-head" ? toHead : toHead.multiplyScalar(-1);
9673
+ }
9674
+ __name(resolveDirection, "resolveDirection");
9675
+ function matchHandPose(shape, definition, context) {
9676
+ if (definition.hand && definition.hand !== shape.handedness) return NO_MATCH;
9677
+ let confidence = 1;
9678
+ let worstScore = 1;
9679
+ let worstConstraint = null;
9680
+ const consider = /* @__PURE__ */ __name((score, label) => {
9681
+ if (score < confidence) confidence = score;
9682
+ if (score < worstScore) {
9683
+ worstScore = score;
9684
+ worstConstraint = label;
9685
+ }
9686
+ }, "consider");
9687
+ for (const finger of XR_FINGERS) {
9688
+ const target = definition.fingers[finger];
9689
+ if (!target) continue;
9690
+ const actual = shape.fingers[finger];
9691
+ consider(scoreAgainst(actual.curl, target.curl, target.tolerance), finger);
9692
+ if (target.baseCurl !== void 0) {
9693
+ consider(
9694
+ scoreAgainst(actual.baseCurl, target.baseCurl, target.tolerance),
9695
+ `${finger} knuckle`
9696
+ );
9697
+ }
9698
+ if (target.tipCurl !== void 0) {
9699
+ consider(
9700
+ scoreAgainst(actual.tipCurl, target.tipCurl, target.tolerance),
9701
+ `${finger} tip`
9702
+ );
9703
+ }
9704
+ }
9705
+ if (definition.pinch) {
9706
+ const { finger, target, tolerance } = definition.pinch;
9707
+ consider(
9708
+ scoreAgainst(shape.pinch[finger], target, tolerance),
9709
+ `${finger} pinch`
9710
+ );
9711
+ }
9712
+ if (definition.orientation) {
9713
+ const { axis, direction, toleranceDegrees } = definition.orientation;
9714
+ const handAxis = axisVector(shape, axis);
9715
+ const wanted = resolveDirection(direction, shape, context);
9716
+ if (!handAxis || !wanted) {
9717
+ consider(0, "orientation");
9718
+ } else {
9719
+ const angle = THREE14__namespace.MathUtils.radToDeg(
9720
+ Math.acos(THREE14__namespace.MathUtils.clamp(handAxis.dot(wanted), -1, 1))
9721
+ );
9722
+ const normalizedError = Math.max(0, angle - toleranceDegrees) / 90;
9723
+ consider(clamp01(1 - normalizedError / MATCH_FALLOFF), "orientation");
9724
+ }
9725
+ }
9726
+ return {
9727
+ matched: confidence >= 1,
9728
+ confidence,
9729
+ worstConstraint: confidence >= 1 ? null : worstConstraint
9730
+ };
9731
+ }
9732
+ __name(matchHandPose, "matchHandPose");
9733
+ var DEFAULT_ENTER_CONFIDENCE = 0.8;
9734
+ var DEFAULT_EXIT_CONFIDENCE = 0.55;
9735
+ var DEFAULT_MIN_HOLD_SECONDS = 0.15;
9736
+ var DEFAULT_LOST_GRACE_SECONDS = 0.25;
9737
+ var _HandPoseTracker = class _HandPoseTracker {
9738
+ constructor(options) {
9739
+ this.active = false;
9740
+ this.holdingSince = null;
9741
+ this.lostSince = null;
9742
+ this.enterConfidence = options?.enterConfidence ?? DEFAULT_ENTER_CONFIDENCE;
9743
+ const exit = options?.exitConfidence ?? DEFAULT_EXIT_CONFIDENCE;
9744
+ this.exitConfidence = Math.min(exit, this.enterConfidence - 1e-3);
9745
+ this.minHoldSeconds = options?.minHoldSeconds ?? DEFAULT_MIN_HOLD_SECONDS;
9746
+ this.lostGraceSeconds = options?.lostGraceSeconds ?? DEFAULT_LOST_GRACE_SECONDS;
9747
+ }
9748
+ isActive() {
9749
+ return this.active;
9750
+ }
9751
+ /** Feed this frame's confidence. Returns an event on a transition. */
9752
+ update(confidence, now) {
9753
+ this.lostSince = null;
9754
+ if (this.active) {
9755
+ if (confidence < this.exitConfidence) {
9756
+ this.active = false;
9757
+ this.holdingSince = null;
9758
+ return "end";
9759
+ }
9760
+ return null;
9761
+ }
9762
+ if (confidence >= this.enterConfidence) {
9763
+ if (this.holdingSince === null) this.holdingSince = now;
9764
+ if (now - this.holdingSince >= this.minHoldSeconds) {
9765
+ this.active = true;
9766
+ return "start";
9767
+ }
9768
+ return null;
9769
+ }
9770
+ this.holdingSince = null;
9771
+ return null;
9772
+ }
9773
+ /** Feed a frame where the hand isn't tracked at all. */
9774
+ miss(now) {
9775
+ this.holdingSince = null;
9776
+ if (!this.active) return null;
9777
+ if (this.lostSince === null) this.lostSince = now;
9778
+ if (now - this.lostSince < this.lostGraceSeconds) return null;
9779
+ this.active = false;
9780
+ this.lostSince = null;
9781
+ return "end";
9782
+ }
9783
+ reset() {
9784
+ this.active = false;
9785
+ this.holdingSince = null;
9786
+ this.lostSince = null;
9787
+ }
9788
+ };
9789
+ __name(_HandPoseTracker, "HandPoseTracker");
9790
+ var HandPoseTracker = _HandPoseTracker;
9791
+
9792
+ // core/systems/XRHandPoseSystem.ts
9793
+ var _XRHandPoseSystem = class _XRHandPoseSystem {
9794
+ constructor(inputSystem) {
9795
+ this.trackers = /* @__PURE__ */ new Map();
9796
+ this.shapes = /* @__PURE__ */ new Map();
9797
+ /** Reused so head-relative orientation checks allocate nothing per frame. */
9798
+ this.headPosition = new THREE14__namespace.Vector3();
9799
+ this.elapsed = 0;
9800
+ this.inputSystem = inputSystem;
9801
+ }
9802
+ update(world, deltaTime) {
9803
+ if (!world.isXrPresenting()) {
9804
+ this.resetAll();
9805
+ return;
9806
+ }
9807
+ this.elapsed += deltaTime;
9808
+ this.refreshShapes();
9809
+ const entries = this.collectEntries(world);
9810
+ if (entries.length === 0) return;
9811
+ world.getCamera().getWorldPosition(this.headPosition);
9812
+ const context = { headPosition: this.headPosition };
9813
+ for (const { entityId, entry } of entries) {
9814
+ const definition = handPoseDefinitionFor(entry, getHandPosePreset);
9815
+ if (!definition) continue;
9816
+ for (const handedness of ["left", "right"]) {
9817
+ if (entry.hand && entry.hand !== handedness) continue;
9818
+ const key = `${entityId}:${entry.id}`;
9819
+ const trackers = this.trackersFor(handedness);
9820
+ let tracker = trackers.get(key);
9821
+ if (!tracker) {
9822
+ tracker = new HandPoseTracker({
9823
+ minHoldSeconds: entry.minHoldSeconds
9824
+ });
9825
+ trackers.set(key, tracker);
9826
+ }
9827
+ const shape = this.shapes.get(handedness) ?? null;
9828
+ if (!shape) {
9829
+ if (tracker.miss(this.elapsed) === "end") {
9830
+ this.dispatch(world, entityId, "end", {
9831
+ poseId: entry.id,
9832
+ poseName: entry.name,
9833
+ handedness,
9834
+ confidence: 0
9835
+ });
9836
+ }
9837
+ continue;
9838
+ }
9839
+ const match = matchHandPose(shape, definition, context);
9840
+ const event = tracker.update(match.confidence, this.elapsed);
9841
+ if (event) {
9842
+ this.dispatch(world, entityId, event, {
9843
+ poseId: entry.id,
9844
+ poseName: entry.name,
9845
+ handedness,
9846
+ confidence: match.confidence
9847
+ });
9848
+ }
9849
+ }
9850
+ }
9851
+ }
9852
+ /** Derive each hand's shape once for the whole frame. */
9853
+ refreshShapes() {
9854
+ for (const handedness of ["left", "right"]) {
9855
+ const input = this.inputSystem.getInput(handedness);
9856
+ const joints = input?.joints ?? null;
9857
+ this.shapes.set(
9858
+ handedness,
9859
+ joints ? computeHandShape(joints, handedness) : null
9860
+ );
9861
+ }
9862
+ }
9863
+ collectEntries(world) {
9864
+ const collected = [];
9865
+ for (const entityId of world.query(XRHandPoseComponent)) {
9866
+ const component = world.getComponent(entityId, XRHandPoseComponent);
9867
+ if (!component?.enabled) continue;
9868
+ for (const entry of component.poses) {
9869
+ if (entry.enabled) collected.push({ entityId, entry });
9870
+ }
9871
+ }
9872
+ return collected;
9873
+ }
9874
+ trackersFor(handedness) {
9875
+ let trackers = this.trackers.get(handedness);
9876
+ if (!trackers) {
9877
+ trackers = /* @__PURE__ */ new Map();
9878
+ this.trackers.set(handedness, trackers);
9879
+ }
9880
+ return trackers;
9881
+ }
9882
+ /**
9883
+ * Deliver to the entity's own Behaviours.
9884
+ *
9885
+ * Scoped to the entity that declared the pose rather than broadcast, so two
9886
+ * objects listening for the same gesture stay independent.
9887
+ */
9888
+ dispatch(world, entityId, event, detail) {
9889
+ const script = world.getComponent(entityId, ScriptComponent);
9890
+ const instances = script?.scriptInstances;
9891
+ if (!instances) return;
9892
+ for (const instance of instances.values()) {
9893
+ if (!(instance instanceof Behaviour)) continue;
9894
+ try {
9895
+ if (event === "start") instance.onHandPoseStart(detail);
9896
+ else instance.onHandPoseEnd(detail);
9897
+ } catch (error) {
9898
+ console.error("[XRHandPoseSystem] Behaviour hook failed:", error);
9899
+ }
9900
+ }
9901
+ }
9902
+ resetAll() {
9903
+ for (const trackers of this.trackers.values()) {
9904
+ for (const tracker of trackers.values()) tracker.reset();
9905
+ }
9906
+ this.shapes.clear();
9907
+ }
9908
+ /** Live shape for a hand — powers the editor's pose readout. */
9909
+ getHandShape(handedness) {
9910
+ return this.shapes.get(handedness) ?? null;
9911
+ }
9912
+ /** Whether a given entity's pose is currently held. */
9913
+ isPoseActive(entityId, poseId, handedness) {
9914
+ return this.trackers.get(handedness)?.get(`${entityId}:${poseId}`)?.isActive() ?? false;
9915
+ }
9916
+ };
9917
+ __name(_XRHandPoseSystem, "XRHandPoseSystem");
9918
+ var XRHandPoseSystem = _XRHandPoseSystem;
8804
9919
 
8805
9920
  exports.AnimationSystem = AnimationSystem;
8806
9921
  exports.AudioSystem = AudioSystem;
@@ -8824,6 +9939,7 @@ exports.TransformSyncSystem = TransformSyncSystem;
8824
9939
  exports.VRTeleportAreaSystem = VRTeleportAreaSystem;
8825
9940
  exports.VideoSystem = VideoSystem;
8826
9941
  exports.XRControllerVisualSystem = XRControllerVisualSystem;
9942
+ exports.XRHandPoseSystem = XRHandPoseSystem;
8827
9943
  exports.XRInputSystem = XRInputSystem;
8828
9944
  exports.XRInteractionSystem = XRInteractionSystem;
8829
9945
  exports.XRLocomotionSystem = XRLocomotionSystem;