@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.
- package/dist/{System-TmC2EijF.d.cts → System-31wk-A0g.d.cts} +1 -1
- package/dist/{System-TmC2EijF.d.ts → System-31wk-A0g.d.ts} +1 -1
- package/dist/{XRInteractor-CCUzSqg9.d.ts → XRHandPose-CxPz8VBp.d.ts} +139 -20
- package/dist/{XRInteractor-I2q6TKtF.d.cts → XRHandPose-VuSbe4iz.d.cts} +139 -20
- package/dist/{chunk-FQTOC7HK.js → chunk-KIKIWCEB.js} +14 -5
- package/dist/chunk-KIKIWCEB.js.map +1 -0
- package/dist/{chunk-N4GKZ7IV.js → chunk-KVGGAL5V.js} +227 -3
- package/dist/chunk-KVGGAL5V.js.map +1 -0
- package/dist/{chunk-JRJ3CL5H.js → chunk-L5BP727H.js} +3 -3
- package/dist/{chunk-JRJ3CL5H.js.map → chunk-L5BP727H.js.map} +1 -1
- package/dist/{chunk-WRJ2K3LZ.js → chunk-LMTB4KQ4.js} +1011 -56
- package/dist/chunk-LMTB4KQ4.js.map +1 -0
- package/dist/components/index.cjs +98 -0
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.cts +4 -3
- package/dist/components/index.d.ts +4 -3
- package/dist/components/index.js +1 -1
- package/dist/docs/guide/vr-and-hand-poses.md +111 -0
- package/dist/factories/index.cjs +58 -0
- package/dist/factories/index.cjs.map +1 -1
- package/dist/factories/index.d.cts +6 -3
- package/dist/factories/index.d.ts +6 -3
- package/dist/factories/index.js +3 -3
- package/dist/handPose-dFxqbFFY.d.cts +240 -0
- package/dist/handPose-dFxqbFFY.d.ts +240 -0
- package/dist/{index-W8pUajCW.d.ts → index-BIQoaKAA.d.cts} +9 -3
- package/dist/{index-BltjPEhg.d.cts → index-BxdXgqiI.d.ts} +9 -3
- package/dist/{index-BpbmZuOz.d.ts → index-C7XQyrgE.d.ts} +101 -7
- package/dist/{index-C5FrDt3z.d.cts → index-DyN50JZo.d.cts} +101 -7
- package/dist/index.cjs +1269 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -9
- package/dist/index.d.ts +104 -9
- package/dist/index.js +8 -8
- package/dist/systems/index.cjs +1226 -51
- package/dist/systems/index.cjs.map +1 -1
- package/dist/systems/index.d.cts +3 -2
- package/dist/systems/index.d.ts +3 -2
- package/dist/systems/index.js +3 -3
- package/dist/types.d.cts +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/worldSnapshotUtils.d.cts +1 -1
- package/dist/worldSnapshotUtils.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-FQTOC7HK.js.map +0 -1
- package/dist/chunk-N4GKZ7IV.js.map +0 -1
- package/dist/chunk-WRJ2K3LZ.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { resolveDracoDecoderPath, applyTransform, SHARED_MODEL_RESOURCE_FLAG } from './chunk-
|
|
2
|
-
import { AnimationComponent, ModelComponent, CameraComponent, Object3DRef, Environment, Postprocessing, TransformComponent, PhysicsComponent, setPhysicsLiveRef, LightComponent, MaterialComponent, DEFAULT_MESH_GEOMETRY_PARAMETERS, MeshGeometryComponent, InstancedMeshComponent, ParentId, ScriptComponent, ScriptObjectComponent, XRRigComponent, VRTeleportAreaComponent, XRInteractableComponent, XRInteractorComponent, XRInteractorSideComponent, InstanceSlotComponent, Name, ImageComponent, SpriteComponent, VideoComponent, AudioComponent, ImageTarget, Selectable, PrefabNode, PrefabInstance } from './chunk-
|
|
1
|
+
import { resolveDracoDecoderPath, applyTransform, SHARED_MODEL_RESOURCE_FLAG } from './chunk-L5BP727H.js';
|
|
2
|
+
import { AnimationComponent, ModelComponent, CameraComponent, Object3DRef, Environment, Postprocessing, TransformComponent, PhysicsComponent, setPhysicsLiveRef, LightComponent, MaterialComponent, DEFAULT_MESH_GEOMETRY_PARAMETERS, MeshGeometryComponent, InstancedMeshComponent, ParentId, ScriptComponent, ScriptObjectComponent, XRRigComponent, VRTeleportAreaComponent, XRInteractableComponent, XRHandPoseComponent, createHandPoseEntry, XRInteractorComponent, XRInteractorSideComponent, InstanceSlotComponent, Name, ImageComponent, SpriteComponent, VideoComponent, AudioComponent, ImageTarget, Selectable, PrefabNode, PrefabInstance, handPoseDefinitionFor, getHandPosePreset } from './chunk-KVGGAL5V.js';
|
|
3
3
|
import { __name } from './chunk-OY7ZODZP.js';
|
|
4
4
|
import * as THREE7 from 'three';
|
|
5
5
|
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
|
@@ -2795,6 +2795,10 @@ function buildComponentValueMaps(snapshot) {
|
|
|
2795
2795
|
snapshot.components,
|
|
2796
2796
|
"XRInteractorComponent"
|
|
2797
2797
|
),
|
|
2798
|
+
xrHandPoseValues: readByComponent(
|
|
2799
|
+
snapshot.components,
|
|
2800
|
+
"XRHandPoseComponent"
|
|
2801
|
+
),
|
|
2798
2802
|
xrInteractorSideValues: readByComponent(
|
|
2799
2803
|
snapshot.components,
|
|
2800
2804
|
"XRInteractorSideComponent"
|
|
@@ -2980,11 +2984,65 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
|
|
|
2980
2984
|
if (typeof sourceInteractable.grabMode === "string") {
|
|
2981
2985
|
interactable.grabMode = sourceInteractable.grabMode;
|
|
2982
2986
|
}
|
|
2987
|
+
if (typeof sourceInteractable.grabReach === "string") {
|
|
2988
|
+
interactable.grabReach = sourceInteractable.grabReach;
|
|
2989
|
+
}
|
|
2990
|
+
if (typeof sourceInteractable.distanceBehavior === "string") {
|
|
2991
|
+
interactable.distanceBehavior = sourceInteractable.distanceBehavior;
|
|
2992
|
+
}
|
|
2993
|
+
if (typeof sourceInteractable.grabGesture === "string") {
|
|
2994
|
+
interactable.grabGesture = sourceInteractable.grabGesture;
|
|
2995
|
+
}
|
|
2996
|
+
if (typeof sourceInteractable.grabRadius === "number") {
|
|
2997
|
+
interactable.grabRadius = sourceInteractable.grabRadius;
|
|
2998
|
+
}
|
|
2999
|
+
if (typeof sourceInteractable.throwOnRelease === "boolean") {
|
|
3000
|
+
interactable.throwOnRelease = sourceInteractable.throwOnRelease;
|
|
3001
|
+
}
|
|
3002
|
+
if (typeof sourceInteractable.throwStrength === "number") {
|
|
3003
|
+
interactable.throwStrength = sourceInteractable.throwStrength;
|
|
3004
|
+
}
|
|
3005
|
+
const grabPos = asRecord(sourceInteractable.grabOffsetPosition);
|
|
3006
|
+
if (grabPos) {
|
|
3007
|
+
interactable.grabOffsetPosition = {
|
|
3008
|
+
x: typeof grabPos.x === "number" ? grabPos.x : 0,
|
|
3009
|
+
y: typeof grabPos.y === "number" ? grabPos.y : 0,
|
|
3010
|
+
z: typeof grabPos.z === "number" ? grabPos.z : 0
|
|
3011
|
+
};
|
|
3012
|
+
}
|
|
3013
|
+
const grabRot = asRecord(sourceInteractable.grabOffsetRotation);
|
|
3014
|
+
if (grabRot) {
|
|
3015
|
+
interactable.grabOffsetRotation = {
|
|
3016
|
+
x: typeof grabRot.x === "number" ? grabRot.x : 0,
|
|
3017
|
+
y: typeof grabRot.y === "number" ? grabRot.y : 0,
|
|
3018
|
+
z: typeof grabRot.z === "number" ? grabRot.z : 0
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
2983
3021
|
if (typeof sourceInteractable.highlightOnHover === "boolean") {
|
|
2984
3022
|
interactable.highlightOnHover = sourceInteractable.highlightOnHover;
|
|
2985
3023
|
}
|
|
2986
3024
|
}
|
|
2987
3025
|
}
|
|
3026
|
+
{
|
|
3027
|
+
const sourceHandPose = asRecord(
|
|
3028
|
+
values.xrHandPoseValues.get(sourceEntityId)
|
|
3029
|
+
);
|
|
3030
|
+
const handPose = world.getComponent(targetEntityId, XRHandPoseComponent);
|
|
3031
|
+
if (sourceHandPose && handPose) {
|
|
3032
|
+
if (typeof sourceHandPose.enabled === "boolean") {
|
|
3033
|
+
handPose.enabled = sourceHandPose.enabled;
|
|
3034
|
+
}
|
|
3035
|
+
if (Array.isArray(sourceHandPose.poses)) {
|
|
3036
|
+
handPose.poses = sourceHandPose.poses.map((entry) => asRecord(entry)).filter(
|
|
3037
|
+
(entry) => entry !== null && typeof entry.id === "string"
|
|
3038
|
+
).map(
|
|
3039
|
+
(entry) => createHandPoseEntry(
|
|
3040
|
+
entry
|
|
3041
|
+
)
|
|
3042
|
+
);
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
2988
3046
|
{
|
|
2989
3047
|
const sourceInteractor = asRecord(
|
|
2990
3048
|
values.xrInteractorValues.get(sourceEntityId)
|
|
@@ -4914,6 +4972,17 @@ var _Behaviour = class _Behaviour {
|
|
|
4914
4972
|
*
|
|
4915
4973
|
* @param input - The source doing the grabbing.
|
|
4916
4974
|
*/
|
|
4975
|
+
/**
|
|
4976
|
+
* A hand pose declared on this entity started being held.
|
|
4977
|
+
*
|
|
4978
|
+
* Fires once the pose has been sustained for its hold time, not the instant
|
|
4979
|
+
* the fingers pass through it.
|
|
4980
|
+
*/
|
|
4981
|
+
onHandPoseStart(_event) {
|
|
4982
|
+
}
|
|
4983
|
+
/** A held hand pose ended, or its hand stopped being tracked. */
|
|
4984
|
+
onHandPoseEnd(_event) {
|
|
4985
|
+
}
|
|
4917
4986
|
onGrab(input) {
|
|
4918
4987
|
}
|
|
4919
4988
|
/** Called when this entity is released. Optional XR hook. */
|
|
@@ -6056,13 +6125,65 @@ function blankState(source) {
|
|
|
6056
6125
|
squeezeEnd: false,
|
|
6057
6126
|
axes: [],
|
|
6058
6127
|
hasAxes: false,
|
|
6059
|
-
buttons: []
|
|
6128
|
+
buttons: [],
|
|
6129
|
+
joints: null
|
|
6060
6130
|
};
|
|
6061
6131
|
}
|
|
6062
6132
|
__name(blankState, "blankState");
|
|
6133
|
+
var JOINT_MATRIX_BUFFER = new Float32Array(25 * 16);
|
|
6134
|
+
function readHandJoints(frame, hand, referenceSpace, rigMatrix, existing) {
|
|
6135
|
+
const names = [];
|
|
6136
|
+
const spaces = [];
|
|
6137
|
+
for (const [name, space] of hand) {
|
|
6138
|
+
names.push(name);
|
|
6139
|
+
spaces.push(space);
|
|
6140
|
+
}
|
|
6141
|
+
if (spaces.length === 0) return null;
|
|
6142
|
+
const target = existing ?? {};
|
|
6143
|
+
const matrix = new THREE7.Matrix4();
|
|
6144
|
+
const buffer = spaces.length * 16 <= JOINT_MATRIX_BUFFER.length ? JOINT_MATRIX_BUFFER : new Float32Array(spaces.length * 16);
|
|
6145
|
+
const fillPoses = frame.fillPoses;
|
|
6146
|
+
if (typeof fillPoses === "function") {
|
|
6147
|
+
let filled = false;
|
|
6148
|
+
try {
|
|
6149
|
+
filled = fillPoses.call(frame, spaces, referenceSpace, buffer);
|
|
6150
|
+
} catch {
|
|
6151
|
+
filled = false;
|
|
6152
|
+
}
|
|
6153
|
+
if (filled) {
|
|
6154
|
+
for (let i = 0; i < names.length; i += 1) {
|
|
6155
|
+
matrix.fromArray(buffer, i * 16).premultiply(rigMatrix);
|
|
6156
|
+
const vector = target[names[i]] ?? new THREE7.Vector3();
|
|
6157
|
+
vector.setFromMatrixPosition(matrix);
|
|
6158
|
+
target[names[i]] = vector;
|
|
6159
|
+
}
|
|
6160
|
+
return target;
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
let anyTracked = false;
|
|
6164
|
+
for (let i = 0; i < names.length; i += 1) {
|
|
6165
|
+
const pose = frame.getJointPose?.(
|
|
6166
|
+
spaces[i],
|
|
6167
|
+
referenceSpace
|
|
6168
|
+
);
|
|
6169
|
+
if (!pose) {
|
|
6170
|
+
target[names[i]] = void 0;
|
|
6171
|
+
continue;
|
|
6172
|
+
}
|
|
6173
|
+
matrix.fromArray(pose.transform.matrix).premultiply(rigMatrix);
|
|
6174
|
+
const vector = target[names[i]] ?? new THREE7.Vector3();
|
|
6175
|
+
vector.setFromMatrixPosition(matrix);
|
|
6176
|
+
target[names[i]] = vector;
|
|
6177
|
+
anyTracked = true;
|
|
6178
|
+
}
|
|
6179
|
+
return anyTracked ? target : null;
|
|
6180
|
+
}
|
|
6181
|
+
__name(readHandJoints, "readHandJoints");
|
|
6063
6182
|
var _XRInputSystem = class _XRInputSystem {
|
|
6064
6183
|
constructor() {
|
|
6065
6184
|
this.records = /* @__PURE__ */ new Map();
|
|
6185
|
+
/** Reference-space → world matrix, rebuilt each frame from the XR rig. */
|
|
6186
|
+
this.referenceToWorld = new THREE7.Matrix4();
|
|
6066
6187
|
this.session = null;
|
|
6067
6188
|
this.onSelectStart = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSelectStart"), "onSelectStart");
|
|
6068
6189
|
this.onSelectEnd = /* @__PURE__ */ __name((event) => this.mark(event, "pendingSelectEnd"), "onSelectEnd");
|
|
@@ -6142,6 +6263,14 @@ var _XRInputSystem = class _XRInputSystem {
|
|
|
6142
6263
|
const referenceSpace = renderer.xr.getReferenceSpace();
|
|
6143
6264
|
const frame = renderer.xr.getFrame?.();
|
|
6144
6265
|
const live = /* @__PURE__ */ new Set();
|
|
6266
|
+
const rigMatrix = this.referenceToWorld;
|
|
6267
|
+
const rig = world.getCamera().parent;
|
|
6268
|
+
if (rig) {
|
|
6269
|
+
rig.updateMatrixWorld();
|
|
6270
|
+
rigMatrix.copy(rig.matrixWorld);
|
|
6271
|
+
} else {
|
|
6272
|
+
rigMatrix.identity();
|
|
6273
|
+
}
|
|
6145
6274
|
for (const source of session.inputSources) {
|
|
6146
6275
|
live.add(source);
|
|
6147
6276
|
const record = this.ensureRecord(source);
|
|
@@ -6152,7 +6281,7 @@ var _XRInputSystem = class _XRInputSystem {
|
|
|
6152
6281
|
if (frame && referenceSpace && source.targetRaySpace) {
|
|
6153
6282
|
const pose = frame.getPose(source.targetRaySpace, referenceSpace);
|
|
6154
6283
|
if (pose) {
|
|
6155
|
-
const m = new THREE7.Matrix4().fromArray(pose.transform.matrix);
|
|
6284
|
+
const m = new THREE7.Matrix4().fromArray(pose.transform.matrix).premultiply(rigMatrix);
|
|
6156
6285
|
state.rayOrigin.setFromMatrixPosition(m);
|
|
6157
6286
|
state.rayDirection.set(0, 0, -1).applyQuaternion(
|
|
6158
6287
|
new THREE7.Quaternion().setFromRotationMatrix(m)
|
|
@@ -6162,7 +6291,7 @@ var _XRInputSystem = class _XRInputSystem {
|
|
|
6162
6291
|
if (frame && referenceSpace && source.gripSpace) {
|
|
6163
6292
|
const gripPose = frame.getPose(source.gripSpace, referenceSpace);
|
|
6164
6293
|
if (gripPose) {
|
|
6165
|
-
const m = new THREE7.Matrix4().fromArray(gripPose.transform.matrix);
|
|
6294
|
+
const m = new THREE7.Matrix4().fromArray(gripPose.transform.matrix).premultiply(rigMatrix);
|
|
6166
6295
|
state.gripPosition ?? (state.gripPosition = new THREE7.Vector3());
|
|
6167
6296
|
state.gripQuaternion ?? (state.gripQuaternion = new THREE7.Quaternion());
|
|
6168
6297
|
state.gripPosition.setFromMatrixPosition(m);
|
|
@@ -6172,6 +6301,17 @@ var _XRInputSystem = class _XRInputSystem {
|
|
|
6172
6301
|
state.gripPosition = null;
|
|
6173
6302
|
state.gripQuaternion = null;
|
|
6174
6303
|
}
|
|
6304
|
+
if (frame && referenceSpace && source.hand) {
|
|
6305
|
+
state.joints = readHandJoints(
|
|
6306
|
+
frame,
|
|
6307
|
+
source.hand,
|
|
6308
|
+
referenceSpace,
|
|
6309
|
+
rigMatrix,
|
|
6310
|
+
state.joints
|
|
6311
|
+
);
|
|
6312
|
+
} else {
|
|
6313
|
+
state.joints = null;
|
|
6314
|
+
}
|
|
6175
6315
|
const gamepad = source.gamepad;
|
|
6176
6316
|
if (gamepad) {
|
|
6177
6317
|
state.axes = Array.from(gamepad.axes);
|
|
@@ -6236,20 +6376,84 @@ var _XRInputSystem = class _XRInputSystem {
|
|
|
6236
6376
|
};
|
|
6237
6377
|
__name(_XRInputSystem, "XRInputSystem");
|
|
6238
6378
|
var XRInputSystem = _XRInputSystem;
|
|
6379
|
+
var VELOCITY_SAMPLES = 5;
|
|
6380
|
+
var MAX_SAMPLE_AGE_SECONDS = 0.2;
|
|
6381
|
+
var _ThrowVelocityTracker = class _ThrowVelocityTracker {
|
|
6382
|
+
constructor() {
|
|
6383
|
+
this.samples = [];
|
|
6384
|
+
}
|
|
6385
|
+
/** Record where the hand is now. Call once per frame while held. */
|
|
6386
|
+
record(position, time) {
|
|
6387
|
+
this.samples.push({ position: position.clone(), time });
|
|
6388
|
+
if (this.samples.length > VELOCITY_SAMPLES) {
|
|
6389
|
+
this.samples.shift();
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
/**
|
|
6393
|
+
* Velocity to release with, in metres/second.
|
|
6394
|
+
*
|
|
6395
|
+
* Uses the FASTEST segment inside the window rather than the most recent or
|
|
6396
|
+
* the window average. Users decelerate as they open their hand, so the last
|
|
6397
|
+
* segment understates the throw and the average washes it out; peak speed is
|
|
6398
|
+
* what matches the motion the user actually felt they made.
|
|
6399
|
+
*/
|
|
6400
|
+
computeVelocity(now) {
|
|
6401
|
+
const fresh = this.samples.filter(
|
|
6402
|
+
(sample) => now - sample.time <= MAX_SAMPLE_AGE_SECONDS
|
|
6403
|
+
);
|
|
6404
|
+
if (fresh.length < 2) return new THREE7.Vector3();
|
|
6405
|
+
let best = new THREE7.Vector3();
|
|
6406
|
+
let bestSpeed = 0;
|
|
6407
|
+
for (let i = 1; i < fresh.length; i += 1) {
|
|
6408
|
+
const previous = fresh[i - 1];
|
|
6409
|
+
const current = fresh[i];
|
|
6410
|
+
const dt = current.time - previous.time;
|
|
6411
|
+
if (dt <= 1e-5) continue;
|
|
6412
|
+
const velocity = new THREE7.Vector3().subVectors(current.position, previous.position).divideScalar(dt);
|
|
6413
|
+
const speed = velocity.length();
|
|
6414
|
+
if (speed > bestSpeed) {
|
|
6415
|
+
bestSpeed = speed;
|
|
6416
|
+
best = velocity;
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
return best;
|
|
6420
|
+
}
|
|
6421
|
+
clear() {
|
|
6422
|
+
this.samples.length = 0;
|
|
6423
|
+
}
|
|
6424
|
+
};
|
|
6425
|
+
__name(_ThrowVelocityTracker, "ThrowVelocityTracker");
|
|
6426
|
+
var ThrowVelocityTracker = _ThrowVelocityTracker;
|
|
6427
|
+
function angularVelocityFrom(previous, current, deltaTime) {
|
|
6428
|
+
if (deltaTime <= 1e-5) return new THREE7.Vector3();
|
|
6429
|
+
const delta = current.clone().multiply(previous.clone().invert());
|
|
6430
|
+
if (delta.w < 0) {
|
|
6431
|
+
delta.set(-delta.x, -delta.y, -delta.z, -delta.w);
|
|
6432
|
+
}
|
|
6433
|
+
const angle = 2 * Math.acos(THREE7.MathUtils.clamp(delta.w, -1, 1));
|
|
6434
|
+
const sinHalf = Math.sqrt(Math.max(0, 1 - delta.w * delta.w));
|
|
6435
|
+
if (sinHalf < 1e-6 || angle < 1e-6) return new THREE7.Vector3();
|
|
6436
|
+
return new THREE7.Vector3(delta.x, delta.y, delta.z).divideScalar(sinHalf).multiplyScalar(angle / deltaTime);
|
|
6437
|
+
}
|
|
6438
|
+
__name(angularVelocityFrom, "angularVelocityFrom");
|
|
6439
|
+
|
|
6440
|
+
// core/systems/XRInteractionSystem.ts
|
|
6239
6441
|
var RAYLESS_HOLD_DISTANCE = 0.5;
|
|
6240
6442
|
var _XRInteractionSystem = class _XRInteractionSystem {
|
|
6241
6443
|
constructor(inputSystem) {
|
|
6242
6444
|
this.raycaster = new THREE7.Raycaster();
|
|
6243
|
-
/** Which entity each source is holding. Keyed by source state identity. */
|
|
6244
6445
|
this.grabs = /* @__PURE__ */ new Map();
|
|
6245
6446
|
this.hoveredEntityId = null;
|
|
6447
|
+
/** Monotonic seconds, accumulated from deltaTime for velocity sampling. */
|
|
6448
|
+
this.elapsed = 0;
|
|
6246
6449
|
this.inputSystem = inputSystem;
|
|
6247
6450
|
}
|
|
6248
|
-
update(world) {
|
|
6451
|
+
update(world, deltaTime) {
|
|
6249
6452
|
if (!world.isXrPresenting()) {
|
|
6250
6453
|
this.releaseAll(world);
|
|
6251
6454
|
return;
|
|
6252
6455
|
}
|
|
6456
|
+
this.elapsed += Math.max(0, deltaTime);
|
|
6253
6457
|
const inputs = this.inputSystem.getInputs();
|
|
6254
6458
|
const interactables = this.collectInteractables(world);
|
|
6255
6459
|
this.hoveredEntityId = null;
|
|
@@ -6260,29 +6464,100 @@ var _XRInteractionSystem = class _XRInteractionSystem {
|
|
|
6260
6464
|
}
|
|
6261
6465
|
const held = this.grabs.get(input);
|
|
6262
6466
|
if (held) {
|
|
6263
|
-
this.moveHeld(world, input, held);
|
|
6264
|
-
if (
|
|
6467
|
+
this.moveHeld(world, input, held, deltaTime);
|
|
6468
|
+
if (this.wantsRelease(world, input, held)) {
|
|
6469
|
+
this.release(world, input);
|
|
6470
|
+
}
|
|
6265
6471
|
continue;
|
|
6266
6472
|
}
|
|
6267
|
-
const
|
|
6268
|
-
if (
|
|
6473
|
+
const candidate = this.findGrabCandidate(world, input, interactables);
|
|
6474
|
+
if (candidate === null) continue;
|
|
6269
6475
|
if (this.hoveredEntityId === null) {
|
|
6270
|
-
this.hoveredEntityId =
|
|
6476
|
+
this.hoveredEntityId = candidate.entityId;
|
|
6271
6477
|
}
|
|
6272
6478
|
const interactable = world.getComponent(
|
|
6273
|
-
|
|
6479
|
+
candidate.entityId,
|
|
6274
6480
|
XRInteractableComponent
|
|
6275
6481
|
);
|
|
6276
|
-
if (
|
|
6277
|
-
|
|
6482
|
+
if (interactable?.grabbable && this.gestureMatches(
|
|
6483
|
+
input,
|
|
6484
|
+
interactable,
|
|
6485
|
+
/* starting */
|
|
6486
|
+
true
|
|
6487
|
+
)) {
|
|
6488
|
+
this.grab(world, input, candidate.entityId, candidate.viaDistance);
|
|
6489
|
+
}
|
|
6490
|
+
}
|
|
6491
|
+
}
|
|
6492
|
+
/** Whether the grab gesture for this object is currently starting. */
|
|
6493
|
+
gestureMatches(input, interactable, starting) {
|
|
6494
|
+
const pinch = starting ? input.selectStart : input.select;
|
|
6495
|
+
const full = starting ? input.squeezeStart : input.squeeze;
|
|
6496
|
+
switch (interactable.grabGesture) {
|
|
6497
|
+
case "pinch":
|
|
6498
|
+
return pinch;
|
|
6499
|
+
case "full":
|
|
6500
|
+
return full;
|
|
6501
|
+
default:
|
|
6502
|
+
return pinch || full;
|
|
6503
|
+
}
|
|
6504
|
+
}
|
|
6505
|
+
/** True once the gesture that started the grab has ended. */
|
|
6506
|
+
wantsRelease(world, input, held) {
|
|
6507
|
+
const interactable = world.getComponent(
|
|
6508
|
+
held.entityId,
|
|
6509
|
+
XRInteractableComponent
|
|
6510
|
+
);
|
|
6511
|
+
if (!interactable) return true;
|
|
6512
|
+
switch (interactable.grabGesture) {
|
|
6513
|
+
case "pinch":
|
|
6514
|
+
return input.selectEnd || !input.select;
|
|
6515
|
+
case "full":
|
|
6516
|
+
return input.squeezeEnd || !input.squeeze;
|
|
6517
|
+
default:
|
|
6518
|
+
return !input.select && !input.squeeze;
|
|
6519
|
+
}
|
|
6520
|
+
}
|
|
6521
|
+
/**
|
|
6522
|
+
* Find what this hand would grab: a physically-reachable object first, then
|
|
6523
|
+
* one along the pointer ray.
|
|
6524
|
+
*
|
|
6525
|
+
* Proximity wins over the ray so that reaching for something you are touching
|
|
6526
|
+
* never picks up a distant object behind it, which feels broken.
|
|
6527
|
+
*/
|
|
6528
|
+
findGrabCandidate(world, input, interactables) {
|
|
6529
|
+
const hand = this.handPosition(input);
|
|
6530
|
+
let nearestId = null;
|
|
6531
|
+
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
6532
|
+
for (const entry of interactables) {
|
|
6533
|
+
const interactable2 = world.getComponent(
|
|
6534
|
+
entry.entityId,
|
|
6535
|
+
XRInteractableComponent
|
|
6536
|
+
);
|
|
6537
|
+
if (!interactable2) continue;
|
|
6538
|
+
if (interactable2.grabReach === "distance") continue;
|
|
6539
|
+
const position = new THREE7.Vector3();
|
|
6540
|
+
entry.object.getWorldPosition(position);
|
|
6541
|
+
const distance = position.distanceTo(hand);
|
|
6542
|
+
if (distance <= interactable2.grabRadius && distance < nearestDistance) {
|
|
6543
|
+
nearestDistance = distance;
|
|
6544
|
+
nearestId = entry.entityId;
|
|
6278
6545
|
}
|
|
6279
6546
|
}
|
|
6547
|
+
if (nearestId !== null) {
|
|
6548
|
+
return { entityId: nearestId, viaDistance: false };
|
|
6549
|
+
}
|
|
6550
|
+
const hitId = this.raycast(input, interactables);
|
|
6551
|
+
if (hitId === null) return null;
|
|
6552
|
+
const interactable = world.getComponent(hitId, XRInteractableComponent);
|
|
6553
|
+
const viaDistanceAllowed = interactable?.grabReach === "distance" || interactable?.grabReach === "both";
|
|
6554
|
+
return { entityId: hitId, viaDistance: viaDistanceAllowed };
|
|
6280
6555
|
}
|
|
6281
6556
|
collectInteractables(world) {
|
|
6282
6557
|
const out = [];
|
|
6283
6558
|
for (const entityId of world.query(XRInteractableComponent, Object3DRef)) {
|
|
6284
6559
|
const interactable = world.getComponent(entityId, XRInteractableComponent);
|
|
6285
|
-
if (!interactable?.enabled
|
|
6560
|
+
if (!interactable?.enabled) continue;
|
|
6286
6561
|
if (!world.isEntityVisible(entityId)) continue;
|
|
6287
6562
|
const object = world.getComponent(entityId, Object3DRef)?.object;
|
|
6288
6563
|
if (object) out.push({ entityId, object });
|
|
@@ -6293,6 +6568,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
|
|
|
6293
6568
|
raycast(input, interactables) {
|
|
6294
6569
|
if (interactables.length === 0) return null;
|
|
6295
6570
|
this.raycaster.set(input.rayOrigin, input.rayDirection);
|
|
6571
|
+
this.raycaster.far = Number.POSITIVE_INFINITY;
|
|
6296
6572
|
let nearestId = null;
|
|
6297
6573
|
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
6298
6574
|
for (const entry of interactables) {
|
|
@@ -6305,43 +6581,120 @@ var _XRInteractionSystem = class _XRInteractionSystem {
|
|
|
6305
6581
|
}
|
|
6306
6582
|
return nearestId;
|
|
6307
6583
|
}
|
|
6308
|
-
grab(world, input, entityId) {
|
|
6584
|
+
grab(world, input, entityId, viaDistance) {
|
|
6309
6585
|
const transform = world.getComponent(entityId, TransformComponent);
|
|
6310
|
-
|
|
6586
|
+
const interactable = world.getComponent(entityId, XRInteractableComponent);
|
|
6587
|
+
if (!transform || !interactable) return;
|
|
6311
6588
|
const hand = this.handPosition(input);
|
|
6312
|
-
const
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
)
|
|
6589
|
+
const handQuaternion = this.handQuaternion(input);
|
|
6590
|
+
const remote = viaDistance && interactable.distanceBehavior === "remote";
|
|
6591
|
+
let offset;
|
|
6592
|
+
let remoteDistance = 0;
|
|
6593
|
+
if (remote) {
|
|
6594
|
+
const position = new THREE7.Vector3(
|
|
6595
|
+
transform.position.x,
|
|
6596
|
+
transform.position.y,
|
|
6597
|
+
transform.position.z
|
|
6598
|
+
);
|
|
6599
|
+
remoteDistance = position.distanceTo(input.rayOrigin);
|
|
6600
|
+
offset = new THREE7.Vector3();
|
|
6601
|
+
} else if (viaDistance) {
|
|
6602
|
+
offset = new THREE7.Vector3(
|
|
6603
|
+
interactable.grabOffsetPosition.x,
|
|
6604
|
+
interactable.grabOffsetPosition.y,
|
|
6605
|
+
interactable.grabOffsetPosition.z
|
|
6606
|
+
);
|
|
6607
|
+
} else {
|
|
6608
|
+
offset = new THREE7.Vector3(
|
|
6609
|
+
transform.position.x - hand.x,
|
|
6610
|
+
transform.position.y - hand.y,
|
|
6611
|
+
transform.position.z - hand.z
|
|
6612
|
+
);
|
|
6613
|
+
}
|
|
6317
6614
|
const physics = world.getComponent(entityId, PhysicsComponent);
|
|
6318
6615
|
let restoreBodyType = null;
|
|
6319
|
-
|
|
6320
|
-
if (physics && interactable?.grabMode === "kinematic") {
|
|
6616
|
+
if (physics) {
|
|
6321
6617
|
restoreBodyType = physics.values.rigidbody.type;
|
|
6322
6618
|
physics.values.rigidbody.type = "kinematicPosition";
|
|
6323
6619
|
}
|
|
6324
|
-
|
|
6620
|
+
const tracker = new ThrowVelocityTracker();
|
|
6621
|
+
tracker.record(hand, this.elapsed);
|
|
6622
|
+
this.grabs.set(input, {
|
|
6623
|
+
entityId,
|
|
6624
|
+
offset,
|
|
6625
|
+
rotationOffset: new THREE7.Quaternion().setFromEuler(
|
|
6626
|
+
new THREE7.Euler(
|
|
6627
|
+
interactable.grabOffsetRotation.x,
|
|
6628
|
+
interactable.grabOffsetRotation.y,
|
|
6629
|
+
interactable.grabOffsetRotation.z
|
|
6630
|
+
)
|
|
6631
|
+
),
|
|
6632
|
+
remoteDistance,
|
|
6633
|
+
remote,
|
|
6634
|
+
restoreBodyType,
|
|
6635
|
+
tracker,
|
|
6636
|
+
lastHandQuaternion: handQuaternion.clone(),
|
|
6637
|
+
angularVelocity: new THREE7.Vector3()
|
|
6638
|
+
});
|
|
6325
6639
|
this.dispatch(world, entityId, (b) => b.onGrab(input));
|
|
6326
6640
|
}
|
|
6327
|
-
moveHeld(world, input, held) {
|
|
6641
|
+
moveHeld(world, input, held, deltaTime) {
|
|
6328
6642
|
const transform = world.getComponent(held.entityId, TransformComponent);
|
|
6329
6643
|
if (!transform) return;
|
|
6330
6644
|
const hand = this.handPosition(input);
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6645
|
+
if (held.remote) {
|
|
6646
|
+
const target = new THREE7.Vector3().copy(input.rayDirection).multiplyScalar(held.remoteDistance).add(input.rayOrigin);
|
|
6647
|
+
transform.position.x = target.x;
|
|
6648
|
+
transform.position.y = target.y;
|
|
6649
|
+
transform.position.z = target.z;
|
|
6650
|
+
} else {
|
|
6651
|
+
transform.position.x = hand.x + held.offset.x;
|
|
6652
|
+
transform.position.y = hand.y + held.offset.y;
|
|
6653
|
+
transform.position.z = hand.z + held.offset.z;
|
|
6654
|
+
}
|
|
6655
|
+
held.tracker.record(
|
|
6656
|
+
held.remote ? new THREE7.Vector3(
|
|
6657
|
+
transform.position.x,
|
|
6658
|
+
transform.position.y,
|
|
6659
|
+
transform.position.z
|
|
6660
|
+
) : hand,
|
|
6661
|
+
this.elapsed
|
|
6662
|
+
);
|
|
6663
|
+
const handQuaternion = this.handQuaternion(input);
|
|
6664
|
+
held.angularVelocity = angularVelocityFrom(
|
|
6665
|
+
held.lastHandQuaternion,
|
|
6666
|
+
handQuaternion,
|
|
6667
|
+
deltaTime
|
|
6668
|
+
);
|
|
6669
|
+
held.lastHandQuaternion = handQuaternion;
|
|
6334
6670
|
}
|
|
6335
6671
|
release(world, input) {
|
|
6336
6672
|
const held = this.grabs.get(input);
|
|
6337
6673
|
if (!held) return;
|
|
6338
6674
|
this.grabs.delete(input);
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6675
|
+
const interactable = world.getComponent(
|
|
6676
|
+
held.entityId,
|
|
6677
|
+
XRInteractableComponent
|
|
6678
|
+
);
|
|
6679
|
+
const physics = world.getComponent(held.entityId, PhysicsComponent);
|
|
6680
|
+
if (held.restoreBodyType !== null && physics) {
|
|
6681
|
+
physics.values.rigidbody.type = held.restoreBodyType;
|
|
6682
|
+
const body = physics.rigidBody;
|
|
6683
|
+
const dynamic = held.restoreBodyType === "dynamic";
|
|
6684
|
+
if (body && dynamic && interactable?.throwOnRelease) {
|
|
6685
|
+
const velocity = held.tracker.computeVelocity(this.elapsed).multiplyScalar(interactable.throwStrength);
|
|
6686
|
+
try {
|
|
6687
|
+
body.setLinvel(
|
|
6688
|
+
{ x: velocity.x, y: velocity.y, z: velocity.z },
|
|
6689
|
+
true
|
|
6690
|
+
);
|
|
6691
|
+
const spin = held.angularVelocity;
|
|
6692
|
+
body.setAngvel({ x: spin.x, y: spin.y, z: spin.z }, true);
|
|
6693
|
+
} catch {
|
|
6694
|
+
}
|
|
6343
6695
|
}
|
|
6344
6696
|
}
|
|
6697
|
+
held.tracker.clear();
|
|
6345
6698
|
this.dispatch(world, held.entityId, (b) => b.onRelease(input));
|
|
6346
6699
|
}
|
|
6347
6700
|
releaseAll(world) {
|
|
@@ -6350,14 +6703,25 @@ var _XRInteractionSystem = class _XRInteractionSystem {
|
|
|
6350
6703
|
}
|
|
6351
6704
|
}
|
|
6352
6705
|
/**
|
|
6353
|
-
* Where the "hand" is
|
|
6354
|
-
*
|
|
6355
|
-
*
|
|
6706
|
+
* Where the "hand" is. Controllers and tracked hands report a grip pose;
|
|
6707
|
+
* gaze and transient-pointer sources have none, so we hold the object a fixed
|
|
6708
|
+
* distance along the ray instead — which is what makes Vision Pro work.
|
|
6356
6709
|
*/
|
|
6357
6710
|
handPosition(input) {
|
|
6358
6711
|
if (input.gripPosition) return input.gripPosition;
|
|
6359
6712
|
return new THREE7.Vector3().copy(input.rayDirection).multiplyScalar(RAYLESS_HOLD_DISTANCE).add(input.rayOrigin);
|
|
6360
6713
|
}
|
|
6714
|
+
/**
|
|
6715
|
+
* Hand orientation. Grip-less sources (gaze / transient-pointer) have none,
|
|
6716
|
+
* so we derive one from the aim direction — enough for spin on release.
|
|
6717
|
+
*/
|
|
6718
|
+
handQuaternion(input) {
|
|
6719
|
+
if (input.gripQuaternion) return input.gripQuaternion.clone();
|
|
6720
|
+
return new THREE7.Quaternion().setFromUnitVectors(
|
|
6721
|
+
new THREE7.Vector3(0, 0, -1),
|
|
6722
|
+
input.rayDirection.clone().normalize()
|
|
6723
|
+
);
|
|
6724
|
+
}
|
|
6361
6725
|
dispatch(world, entityId, visit) {
|
|
6362
6726
|
const script = world.getComponent(entityId, ScriptComponent);
|
|
6363
6727
|
const instances = script?.scriptInstances;
|
|
@@ -6433,6 +6797,10 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
|
|
|
6433
6797
|
/** Curve up to the landing point, for the visual system. Empty when not aiming. */
|
|
6434
6798
|
this.aimCurve = [];
|
|
6435
6799
|
this.landingPoint = null;
|
|
6800
|
+
/** True when the current aim lands somewhere the player may actually stand. */
|
|
6801
|
+
this.aimValid = false;
|
|
6802
|
+
/** Sources currently aiming via thumbstick (hysteresis, so it doesn't chatter). */
|
|
6803
|
+
this.stickAiming = /* @__PURE__ */ new WeakSet();
|
|
6436
6804
|
/** True once we've tried to self-heal camera parenting for this session. */
|
|
6437
6805
|
this.parentingChecked = false;
|
|
6438
6806
|
this.inputSystem = inputSystem;
|
|
@@ -6440,6 +6808,7 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
|
|
|
6440
6808
|
update(world, deltaTime) {
|
|
6441
6809
|
this.aimCurve = [];
|
|
6442
6810
|
this.landingPoint = null;
|
|
6811
|
+
this.aimValid = false;
|
|
6443
6812
|
if (!world.isXrPresenting()) {
|
|
6444
6813
|
this.parentingChecked = false;
|
|
6445
6814
|
return;
|
|
@@ -6447,6 +6816,7 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
|
|
|
6447
6816
|
if (!this.parentingChecked) {
|
|
6448
6817
|
this.parentingChecked = true;
|
|
6449
6818
|
this.ensureCameraUnderRig(world);
|
|
6819
|
+
this.warnOnIncompleteSetup(world);
|
|
6450
6820
|
}
|
|
6451
6821
|
const rigEntityId = world.query(XRRigComponent, TransformComponent)[0];
|
|
6452
6822
|
if (typeof rigEntityId !== "number") return;
|
|
@@ -6464,6 +6834,26 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
|
|
|
6464
6834
|
this.applySmoothMove(world, rig, rigTransform, inputs, deltaTime);
|
|
6465
6835
|
}
|
|
6466
6836
|
}
|
|
6837
|
+
/**
|
|
6838
|
+
* Name what is missing, once per session.
|
|
6839
|
+
*
|
|
6840
|
+
* Inside a headset there is no console and no inspector, so a scene that
|
|
6841
|
+
* simply cannot move gives the author nothing to go on. These messages are
|
|
6842
|
+
* visible over remote debugging and say exactly which entity to add.
|
|
6843
|
+
*/
|
|
6844
|
+
warnOnIncompleteSetup(world) {
|
|
6845
|
+
if (typeof world.query(XRRigComponent)[0] !== "number") {
|
|
6846
|
+
console.warn(
|
|
6847
|
+
"[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)."
|
|
6848
|
+
);
|
|
6849
|
+
return;
|
|
6850
|
+
}
|
|
6851
|
+
if (world.query(VRTeleportAreaComponent).length === 0) {
|
|
6852
|
+
console.warn(
|
|
6853
|
+
"[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."
|
|
6854
|
+
);
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6467
6857
|
/**
|
|
6468
6858
|
* Guarantee the render camera is a CHILD of the rig.
|
|
6469
6859
|
*
|
|
@@ -6531,29 +6921,60 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
|
|
|
6531
6921
|
}
|
|
6532
6922
|
applyTeleport(world, transform, inputs) {
|
|
6533
6923
|
const targets = this.collectTeleportTargets(world);
|
|
6534
|
-
if (targets.length === 0) return;
|
|
6535
6924
|
for (const input of inputs) {
|
|
6536
|
-
|
|
6925
|
+
const source = input;
|
|
6926
|
+
const stick = this.readStickAim(input, source);
|
|
6927
|
+
const aiming = input.select || stick.aiming;
|
|
6928
|
+
const commit = input.selectEnd || stick.released;
|
|
6929
|
+
if (!aiming && !commit) continue;
|
|
6537
6930
|
const points = sampleTeleportArc(
|
|
6538
6931
|
input.rayOrigin,
|
|
6539
6932
|
input.rayDirection,
|
|
6540
6933
|
this.arcPoints
|
|
6541
6934
|
);
|
|
6542
6935
|
const hit = this.raycastArc(points, targets);
|
|
6543
|
-
|
|
6544
|
-
this.landingPoint = hit.point;
|
|
6545
|
-
this.
|
|
6546
|
-
if (
|
|
6936
|
+
this.aimCurve = hit ? points.slice(0, hit.segmentIndex + 1).concat(hit.point) : points.slice();
|
|
6937
|
+
this.landingPoint = hit ? hit.point : null;
|
|
6938
|
+
this.aimValid = hit !== null;
|
|
6939
|
+
if (commit && hit) {
|
|
6547
6940
|
transform.position.x = hit.point.x;
|
|
6548
6941
|
transform.position.y = hit.point.y;
|
|
6549
6942
|
transform.position.z = hit.point.z;
|
|
6550
6943
|
this.aimCurve = [];
|
|
6551
6944
|
this.landingPoint = null;
|
|
6945
|
+
this.aimValid = false;
|
|
6946
|
+
} else if (commit) {
|
|
6947
|
+
this.aimCurve = [];
|
|
6948
|
+
this.aimValid = false;
|
|
6552
6949
|
}
|
|
6553
6950
|
}
|
|
6554
6951
|
}
|
|
6952
|
+
/**
|
|
6953
|
+
* Thumbstick-forward aiming with hysteresis.
|
|
6954
|
+
*
|
|
6955
|
+
* Engaging at 0.6 and releasing at 0.3 stops a stick resting near the
|
|
6956
|
+
* threshold from flickering the arc on and off — and the release edge is what
|
|
6957
|
+
* commits the teleport, so chatter there would teleport the player randomly.
|
|
6958
|
+
*/
|
|
6959
|
+
readStickAim(input, source) {
|
|
6960
|
+
if (!input.hasAxes || input.axes.length < 4) {
|
|
6961
|
+
return { aiming: false, released: false };
|
|
6962
|
+
}
|
|
6963
|
+
const forward = -(input.axes[3] ?? 0);
|
|
6964
|
+
const wasAiming = this.stickAiming.has(source);
|
|
6965
|
+
if (!wasAiming && forward > 0.6) {
|
|
6966
|
+
this.stickAiming.add(source);
|
|
6967
|
+
return { aiming: true, released: false };
|
|
6968
|
+
}
|
|
6969
|
+
if (wasAiming && forward < 0.3) {
|
|
6970
|
+
this.stickAiming.delete(source);
|
|
6971
|
+
return { aiming: false, released: true };
|
|
6972
|
+
}
|
|
6973
|
+
return { aiming: wasAiming, released: false };
|
|
6974
|
+
}
|
|
6555
6975
|
/** Walk the arc segment-by-segment, returning the first surface hit. */
|
|
6556
6976
|
raycastArc(points, targets) {
|
|
6977
|
+
if (targets.length === 0) return null;
|
|
6557
6978
|
for (let i = 1; i < points.length; i += 1) {
|
|
6558
6979
|
const from = points[i - 1];
|
|
6559
6980
|
const to = points[i];
|
|
@@ -6584,6 +7005,10 @@ var _XRLocomotionSystem = class _XRLocomotionSystem {
|
|
|
6584
7005
|
getAimCurve() {
|
|
6585
7006
|
return this.aimCurve;
|
|
6586
7007
|
}
|
|
7008
|
+
/** True when the aim lands on a valid teleport area. */
|
|
7009
|
+
isAimValid() {
|
|
7010
|
+
return this.aimValid;
|
|
7011
|
+
}
|
|
6587
7012
|
/** Where the arc currently lands, or null when not aiming at a valid area. */
|
|
6588
7013
|
getLandingPoint() {
|
|
6589
7014
|
return this.landingPoint;
|
|
@@ -6866,6 +7291,7 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
|
|
|
6866
7291
|
this.handStyleSignature = "";
|
|
6867
7292
|
this.handMaterial = null;
|
|
6868
7293
|
this.interactionSystem = options?.interactionSystem ?? null;
|
|
7294
|
+
this.inputSystem = options?.inputSystem ?? null;
|
|
6869
7295
|
this.group.name = "XRControllerVisuals";
|
|
6870
7296
|
this.group.userData.ecsPreviewHidden = true;
|
|
6871
7297
|
}
|
|
@@ -6882,8 +7308,11 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
|
|
|
6882
7308
|
for (const { index, side } of SLOTS) {
|
|
6883
7309
|
const visuals = this.slots.get(index);
|
|
6884
7310
|
if (!visuals) continue;
|
|
6885
|
-
|
|
6886
|
-
|
|
7311
|
+
const liveKind = this.liveKindFor(side);
|
|
7312
|
+
const hasController = liveKind === null || liveKind === "controller";
|
|
7313
|
+
const hasHand = liveKind === null || liveKind === "hand";
|
|
7314
|
+
visuals.controllerModel.visible = showControllers && hasController;
|
|
7315
|
+
visuals.handModel.visible = showHands && hasHand;
|
|
6887
7316
|
visuals.ray.visible = this.shouldShowRay(interactor);
|
|
6888
7317
|
if (showHands && this.handMaterial) {
|
|
6889
7318
|
applyHandMaterial(visuals.handModel, this.handMaterial);
|
|
@@ -6891,6 +7320,20 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
|
|
|
6891
7320
|
this.syncCustomContent(world, visuals, side);
|
|
6892
7321
|
}
|
|
6893
7322
|
}
|
|
7323
|
+
/**
|
|
7324
|
+
* What kind of source is currently driving this hand, or null when we cannot
|
|
7325
|
+
* tell (no input system wired) — in which case callers fall back to showing
|
|
7326
|
+
* everything rather than hiding the user's hands entirely.
|
|
7327
|
+
*/
|
|
7328
|
+
liveKindFor(side) {
|
|
7329
|
+
if (!this.inputSystem) return null;
|
|
7330
|
+
const inputs = this.inputSystem.getInputs();
|
|
7331
|
+
if (inputs.length === 0) return null;
|
|
7332
|
+
for (const input of inputs) {
|
|
7333
|
+
if (input.handedness === side && input.connected) return input.kind;
|
|
7334
|
+
}
|
|
7335
|
+
return "none";
|
|
7336
|
+
}
|
|
6894
7337
|
/** The scene's interactor config, if one was authored. */
|
|
6895
7338
|
resolveInteractor(world) {
|
|
6896
7339
|
const entityId = world.query(XRInteractorComponent)[0];
|
|
@@ -7030,6 +7473,7 @@ __name(_XRControllerVisualSystem, "XRControllerVisualSystem");
|
|
|
7030
7473
|
var XRControllerVisualSystem = _XRControllerVisualSystem;
|
|
7031
7474
|
var MARKER_RADIUS = 0.28;
|
|
7032
7475
|
var ACCENT = new THREE7.Color("#4da3ff");
|
|
7476
|
+
var BLOCKED = new THREE7.Color("#ff5a5a");
|
|
7033
7477
|
var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
|
|
7034
7478
|
constructor(locomotionSystem) {
|
|
7035
7479
|
this.group = new THREE7.Group();
|
|
@@ -7060,16 +7504,22 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
|
|
|
7060
7504
|
}
|
|
7061
7505
|
const curve = this.locomotionSystem.getAimCurve();
|
|
7062
7506
|
const landing = this.locomotionSystem.getLandingPoint();
|
|
7063
|
-
|
|
7507
|
+
const valid = this.locomotionSystem.isAimValid();
|
|
7508
|
+
if (curve.length < 2) {
|
|
7064
7509
|
this.setVisible(false);
|
|
7065
7510
|
return;
|
|
7066
7511
|
}
|
|
7067
|
-
this.updateArcGeometry(curve);
|
|
7068
|
-
|
|
7069
|
-
|
|
7512
|
+
this.updateArcGeometry(curve, valid ? ACCENT : BLOCKED);
|
|
7513
|
+
if (valid && landing) {
|
|
7514
|
+
this.marker.position.copy(landing);
|
|
7515
|
+
this.marker.position.y += 0.01;
|
|
7516
|
+
this.marker.visible = true;
|
|
7517
|
+
} else {
|
|
7518
|
+
this.marker.visible = false;
|
|
7519
|
+
}
|
|
7070
7520
|
this.setVisible(true);
|
|
7071
7521
|
}
|
|
7072
|
-
updateArcGeometry(curve) {
|
|
7522
|
+
updateArcGeometry(curve, tint) {
|
|
7073
7523
|
const count = curve.length;
|
|
7074
7524
|
const positions = new Float32Array(count * 3);
|
|
7075
7525
|
const colors = new Float32Array(count * 3);
|
|
@@ -7080,9 +7530,9 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
|
|
|
7080
7530
|
positions[i * 3 + 2] = point.z;
|
|
7081
7531
|
const t = count > 1 ? i / (count - 1) : 0;
|
|
7082
7532
|
const fade = 1 - t * 0.75;
|
|
7083
|
-
colors[i * 3] =
|
|
7084
|
-
colors[i * 3 + 1] =
|
|
7085
|
-
colors[i * 3 + 2] =
|
|
7533
|
+
colors[i * 3] = tint.r * fade;
|
|
7534
|
+
colors[i * 3 + 1] = tint.g * fade;
|
|
7535
|
+
colors[i * 3 + 2] = tint.b * fade;
|
|
7086
7536
|
}
|
|
7087
7537
|
this.arcGeometry.setAttribute(
|
|
7088
7538
|
"position",
|
|
@@ -7143,7 +7593,512 @@ var _XRTeleportVisualSystem = class _XRTeleportVisualSystem {
|
|
|
7143
7593
|
};
|
|
7144
7594
|
__name(_XRTeleportVisualSystem, "XRTeleportVisualSystem");
|
|
7145
7595
|
var XRTeleportVisualSystem = _XRTeleportVisualSystem;
|
|
7596
|
+
var XR_FINGERS = [
|
|
7597
|
+
"thumb",
|
|
7598
|
+
"index",
|
|
7599
|
+
"middle",
|
|
7600
|
+
"ring",
|
|
7601
|
+
"pinky"
|
|
7602
|
+
];
|
|
7603
|
+
var PINCH_FINGERS = [
|
|
7604
|
+
"index",
|
|
7605
|
+
"middle",
|
|
7606
|
+
"ring",
|
|
7607
|
+
"pinky"
|
|
7608
|
+
];
|
|
7609
|
+
var FINGER_CHAINS = {
|
|
7610
|
+
thumb: [
|
|
7611
|
+
"thumb-metacarpal",
|
|
7612
|
+
"thumb-phalanx-proximal",
|
|
7613
|
+
"thumb-phalanx-distal",
|
|
7614
|
+
"thumb-tip"
|
|
7615
|
+
],
|
|
7616
|
+
index: [
|
|
7617
|
+
"index-finger-metacarpal",
|
|
7618
|
+
"index-finger-phalanx-proximal",
|
|
7619
|
+
"index-finger-phalanx-intermediate",
|
|
7620
|
+
"index-finger-phalanx-distal",
|
|
7621
|
+
"index-finger-tip"
|
|
7622
|
+
],
|
|
7623
|
+
middle: [
|
|
7624
|
+
"middle-finger-metacarpal",
|
|
7625
|
+
"middle-finger-phalanx-proximal",
|
|
7626
|
+
"middle-finger-phalanx-intermediate",
|
|
7627
|
+
"middle-finger-phalanx-distal",
|
|
7628
|
+
"middle-finger-tip"
|
|
7629
|
+
],
|
|
7630
|
+
ring: [
|
|
7631
|
+
"ring-finger-metacarpal",
|
|
7632
|
+
"ring-finger-phalanx-proximal",
|
|
7633
|
+
"ring-finger-phalanx-intermediate",
|
|
7634
|
+
"ring-finger-phalanx-distal",
|
|
7635
|
+
"ring-finger-tip"
|
|
7636
|
+
],
|
|
7637
|
+
pinky: [
|
|
7638
|
+
"pinky-finger-metacarpal",
|
|
7639
|
+
"pinky-finger-phalanx-proximal",
|
|
7640
|
+
"pinky-finger-phalanx-intermediate",
|
|
7641
|
+
"pinky-finger-phalanx-distal",
|
|
7642
|
+
"pinky-finger-tip"
|
|
7643
|
+
]
|
|
7644
|
+
};
|
|
7645
|
+
var TIP_JOINTS = {
|
|
7646
|
+
thumb: "thumb-tip",
|
|
7647
|
+
index: "index-finger-tip",
|
|
7648
|
+
middle: "middle-finger-tip",
|
|
7649
|
+
ring: "ring-finger-tip",
|
|
7650
|
+
pinky: "pinky-finger-tip"
|
|
7651
|
+
};
|
|
7652
|
+
var METACARPAL_JOINTS = {
|
|
7653
|
+
thumb: "thumb-metacarpal",
|
|
7654
|
+
index: "index-finger-metacarpal",
|
|
7655
|
+
middle: "middle-finger-metacarpal",
|
|
7656
|
+
ring: "ring-finger-metacarpal",
|
|
7657
|
+
pinky: "pinky-finger-metacarpal"
|
|
7658
|
+
};
|
|
7659
|
+
var FALLBACK_HAND_SCALE = 0.08;
|
|
7660
|
+
var STRAIGHT_COS = 0.95;
|
|
7661
|
+
var CURLED_COS = 0.55;
|
|
7662
|
+
function clamp01(value) {
|
|
7663
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
7664
|
+
}
|
|
7665
|
+
__name(clamp01, "clamp01");
|
|
7666
|
+
function boneDirections(joints, finger) {
|
|
7667
|
+
const chain = FINGER_CHAINS[finger];
|
|
7668
|
+
const directions = [];
|
|
7669
|
+
for (let i = 1; i < chain.length; i += 1) {
|
|
7670
|
+
const from = joints[chain[i - 1]];
|
|
7671
|
+
const to = joints[chain[i]];
|
|
7672
|
+
if (!from || !to) return null;
|
|
7673
|
+
const direction = new THREE7.Vector3().subVectors(to, from);
|
|
7674
|
+
if (direction.lengthSq() < 1e-12) return null;
|
|
7675
|
+
directions.push(direction.normalize());
|
|
7676
|
+
}
|
|
7677
|
+
return directions.length >= 2 ? directions : null;
|
|
7678
|
+
}
|
|
7679
|
+
__name(boneDirections, "boneDirections");
|
|
7680
|
+
function bendAmounts(directions) {
|
|
7681
|
+
const bends = [];
|
|
7682
|
+
for (let i = 1; i < directions.length; i += 1) {
|
|
7683
|
+
const cos = directions[i - 1].dot(directions[i]);
|
|
7684
|
+
const straightness = clamp01((cos - CURLED_COS) / (STRAIGHT_COS - CURLED_COS));
|
|
7685
|
+
bends.push(1 - straightness);
|
|
7686
|
+
}
|
|
7687
|
+
return bends;
|
|
7688
|
+
}
|
|
7689
|
+
__name(bendAmounts, "bendAmounts");
|
|
7690
|
+
function average(values) {
|
|
7691
|
+
if (values.length === 0) return 0;
|
|
7692
|
+
let sum = 0;
|
|
7693
|
+
for (const value of values) sum += value;
|
|
7694
|
+
return sum / values.length;
|
|
7695
|
+
}
|
|
7696
|
+
__name(average, "average");
|
|
7697
|
+
function computeFingerCurl(joints, finger) {
|
|
7698
|
+
const directions = boneDirections(joints, finger);
|
|
7699
|
+
if (!directions) return null;
|
|
7700
|
+
const bends = bendAmounts(directions);
|
|
7701
|
+
if (bends.length === 0) return null;
|
|
7702
|
+
return {
|
|
7703
|
+
baseCurl: bends[0],
|
|
7704
|
+
tipCurl: average(bends.slice(1)) || bends[bends.length - 1],
|
|
7705
|
+
curl: average(bends)
|
|
7706
|
+
};
|
|
7707
|
+
}
|
|
7708
|
+
__name(computeFingerCurl, "computeFingerCurl");
|
|
7709
|
+
function computePalmWidth(joints) {
|
|
7710
|
+
const index = joints["index-finger-metacarpal"];
|
|
7711
|
+
const pinky = joints["pinky-finger-metacarpal"];
|
|
7712
|
+
if (!index || !pinky) return null;
|
|
7713
|
+
const width = index.distanceTo(pinky);
|
|
7714
|
+
return width > 1e-6 ? width : null;
|
|
7715
|
+
}
|
|
7716
|
+
__name(computePalmWidth, "computePalmWidth");
|
|
7717
|
+
function estimateHandScale(joints) {
|
|
7718
|
+
const measurements = [];
|
|
7719
|
+
const width = computePalmWidth(joints);
|
|
7720
|
+
if (width !== null) measurements.push(width);
|
|
7721
|
+
const wrist = joints.wrist;
|
|
7722
|
+
const middleTip = joints["middle-finger-tip"];
|
|
7723
|
+
if (wrist && middleTip) {
|
|
7724
|
+
const span = wrist.distanceTo(middleTip);
|
|
7725
|
+
if (span > 1e-6) measurements.push(span * 0.5);
|
|
7726
|
+
}
|
|
7727
|
+
if (measurements.length === 0) return FALLBACK_HAND_SCALE;
|
|
7728
|
+
return average(measurements);
|
|
7729
|
+
}
|
|
7730
|
+
__name(estimateHandScale, "estimateHandScale");
|
|
7731
|
+
function computePinch(joints, finger) {
|
|
7732
|
+
const thumbTip = joints["thumb-tip"];
|
|
7733
|
+
const fingerTip = joints[TIP_JOINTS[finger]];
|
|
7734
|
+
if (!thumbTip || !fingerTip) return null;
|
|
7735
|
+
const scale = estimateHandScale(joints);
|
|
7736
|
+
const distance = thumbTip.distanceTo(fingerTip);
|
|
7737
|
+
return clamp01(1 - distance / (scale * 0.7));
|
|
7738
|
+
}
|
|
7739
|
+
__name(computePinch, "computePinch");
|
|
7740
|
+
function computeSpread(joints, a, b) {
|
|
7741
|
+
const aBase = joints[METACARPAL_JOINTS[a]];
|
|
7742
|
+
const aTip = joints[TIP_JOINTS[a]];
|
|
7743
|
+
const bBase = joints[METACARPAL_JOINTS[b]];
|
|
7744
|
+
const bTip = joints[TIP_JOINTS[b]];
|
|
7745
|
+
if (!aBase || !aTip || !bBase || !bTip) return null;
|
|
7746
|
+
const dirA = new THREE7.Vector3().subVectors(aTip, aBase);
|
|
7747
|
+
const dirB = new THREE7.Vector3().subVectors(bTip, bBase);
|
|
7748
|
+
if (dirA.lengthSq() < 1e-12 || dirB.lengthSq() < 1e-12) return null;
|
|
7749
|
+
return clamp01((1 - dirA.normalize().dot(dirB.normalize())) / 0.45);
|
|
7750
|
+
}
|
|
7751
|
+
__name(computeSpread, "computeSpread");
|
|
7752
|
+
function computePalmPose(joints, handedness) {
|
|
7753
|
+
const wrist = joints.wrist;
|
|
7754
|
+
const indexBase = joints["index-finger-metacarpal"];
|
|
7755
|
+
const pinkyBase = joints["pinky-finger-metacarpal"];
|
|
7756
|
+
if (!wrist || !indexBase || !pinkyBase) return null;
|
|
7757
|
+
const u = new THREE7.Vector3().subVectors(indexBase, wrist);
|
|
7758
|
+
const v = new THREE7.Vector3().subVectors(pinkyBase, wrist);
|
|
7759
|
+
const normal = new THREE7.Vector3().crossVectors(u, v);
|
|
7760
|
+
if (normal.lengthSq() < 1e-12) return null;
|
|
7761
|
+
normal.normalize();
|
|
7762
|
+
if (handedness === "left") normal.multiplyScalar(-1);
|
|
7763
|
+
const right = new THREE7.Vector3().subVectors(indexBase, pinkyBase);
|
|
7764
|
+
if (right.lengthSq() < 1e-12) return null;
|
|
7765
|
+
right.normalize();
|
|
7766
|
+
if (handedness === "left") right.multiplyScalar(-1);
|
|
7767
|
+
const up = new THREE7.Vector3().crossVectors(normal, right).normalize();
|
|
7768
|
+
const center = new THREE7.Vector3().add(wrist).add(indexBase).add(pinkyBase).divideScalar(3);
|
|
7769
|
+
return {
|
|
7770
|
+
center,
|
|
7771
|
+
normal,
|
|
7772
|
+
right,
|
|
7773
|
+
up,
|
|
7774
|
+
width: computePalmWidth(joints) ?? FALLBACK_HAND_SCALE
|
|
7775
|
+
};
|
|
7776
|
+
}
|
|
7777
|
+
__name(computePalmPose, "computePalmPose");
|
|
7778
|
+
function computeHandShape(joints, handedness) {
|
|
7779
|
+
const fingers = {};
|
|
7780
|
+
let tracked = 0;
|
|
7781
|
+
for (const finger of XR_FINGERS) {
|
|
7782
|
+
const curl = computeFingerCurl(joints, finger);
|
|
7783
|
+
if (curl) {
|
|
7784
|
+
fingers[finger] = curl;
|
|
7785
|
+
tracked += 1;
|
|
7786
|
+
} else {
|
|
7787
|
+
fingers[finger] = { baseCurl: 0, tipCurl: 0, curl: 0 };
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7790
|
+
if (tracked === 0) return null;
|
|
7791
|
+
const pinch = {};
|
|
7792
|
+
for (const finger of PINCH_FINGERS) {
|
|
7793
|
+
pinch[finger] = computePinch(joints, finger) ?? 0;
|
|
7794
|
+
}
|
|
7795
|
+
const thumbBase = joints["thumb-metacarpal"];
|
|
7796
|
+
const thumbTip = joints["thumb-tip"];
|
|
7797
|
+
let thumbDirection = null;
|
|
7798
|
+
if (thumbBase && thumbTip) {
|
|
7799
|
+
const direction = new THREE7.Vector3().subVectors(thumbTip, thumbBase);
|
|
7800
|
+
if (direction.lengthSq() > 1e-12) thumbDirection = direction.normalize();
|
|
7801
|
+
}
|
|
7802
|
+
return {
|
|
7803
|
+
handedness,
|
|
7804
|
+
fingers,
|
|
7805
|
+
pinch,
|
|
7806
|
+
spread: {
|
|
7807
|
+
thumbIndex: computeSpread(joints, "thumb", "index") ?? 0,
|
|
7808
|
+
indexMiddle: computeSpread(joints, "index", "middle") ?? 0,
|
|
7809
|
+
middleRing: computeSpread(joints, "middle", "ring") ?? 0,
|
|
7810
|
+
ringPinky: computeSpread(joints, "ring", "pinky") ?? 0
|
|
7811
|
+
},
|
|
7812
|
+
palm: computePalmPose(joints, handedness),
|
|
7813
|
+
scale: estimateHandScale(joints),
|
|
7814
|
+
thumbDirection
|
|
7815
|
+
};
|
|
7816
|
+
}
|
|
7817
|
+
__name(computeHandShape, "computeHandShape");
|
|
7818
|
+
var MATCH_FALLOFF = 0.25;
|
|
7819
|
+
var NO_MATCH = {
|
|
7820
|
+
matched: false,
|
|
7821
|
+
confidence: 0,
|
|
7822
|
+
worstConstraint: null
|
|
7823
|
+
};
|
|
7824
|
+
function scoreAgainst(actual, target, tolerance) {
|
|
7825
|
+
const error = Math.abs(actual - target);
|
|
7826
|
+
if (error <= tolerance) return 1;
|
|
7827
|
+
return clamp01(1 - (error - tolerance) / MATCH_FALLOFF);
|
|
7828
|
+
}
|
|
7829
|
+
__name(scoreAgainst, "scoreAgainst");
|
|
7830
|
+
var WORLD_DIRECTIONS = {
|
|
7831
|
+
up: new THREE7.Vector3(0, 1, 0),
|
|
7832
|
+
down: new THREE7.Vector3(0, -1, 0),
|
|
7833
|
+
forward: new THREE7.Vector3(0, 0, -1),
|
|
7834
|
+
back: new THREE7.Vector3(0, 0, 1),
|
|
7835
|
+
left: new THREE7.Vector3(-1, 0, 0),
|
|
7836
|
+
right: new THREE7.Vector3(1, 0, 0)
|
|
7837
|
+
};
|
|
7838
|
+
function axisVector(shape, axis) {
|
|
7839
|
+
if (axis === "thumb") return shape.thumbDirection;
|
|
7840
|
+
if (!shape.palm) return null;
|
|
7841
|
+
return axis === "palm-normal" ? shape.palm.normal : shape.palm.up;
|
|
7842
|
+
}
|
|
7843
|
+
__name(axisVector, "axisVector");
|
|
7844
|
+
function resolveDirection(direction, shape, context) {
|
|
7845
|
+
if (direction !== "toward-head" && direction !== "away-from-head") {
|
|
7846
|
+
return WORLD_DIRECTIONS[direction];
|
|
7847
|
+
}
|
|
7848
|
+
const head = context?.headPosition;
|
|
7849
|
+
const origin = shape.palm?.center;
|
|
7850
|
+
if (!head || !origin) return null;
|
|
7851
|
+
const toHead = new THREE7.Vector3().subVectors(head, origin);
|
|
7852
|
+
if (toHead.lengthSq() < 1e-12) return null;
|
|
7853
|
+
toHead.normalize();
|
|
7854
|
+
return direction === "toward-head" ? toHead : toHead.multiplyScalar(-1);
|
|
7855
|
+
}
|
|
7856
|
+
__name(resolveDirection, "resolveDirection");
|
|
7857
|
+
function matchHandPose(shape, definition, context) {
|
|
7858
|
+
if (definition.hand && definition.hand !== shape.handedness) return NO_MATCH;
|
|
7859
|
+
let confidence = 1;
|
|
7860
|
+
let worstScore = 1;
|
|
7861
|
+
let worstConstraint = null;
|
|
7862
|
+
const consider = /* @__PURE__ */ __name((score, label) => {
|
|
7863
|
+
if (score < confidence) confidence = score;
|
|
7864
|
+
if (score < worstScore) {
|
|
7865
|
+
worstScore = score;
|
|
7866
|
+
worstConstraint = label;
|
|
7867
|
+
}
|
|
7868
|
+
}, "consider");
|
|
7869
|
+
for (const finger of XR_FINGERS) {
|
|
7870
|
+
const target = definition.fingers[finger];
|
|
7871
|
+
if (!target) continue;
|
|
7872
|
+
const actual = shape.fingers[finger];
|
|
7873
|
+
consider(scoreAgainst(actual.curl, target.curl, target.tolerance), finger);
|
|
7874
|
+
if (target.baseCurl !== void 0) {
|
|
7875
|
+
consider(
|
|
7876
|
+
scoreAgainst(actual.baseCurl, target.baseCurl, target.tolerance),
|
|
7877
|
+
`${finger} knuckle`
|
|
7878
|
+
);
|
|
7879
|
+
}
|
|
7880
|
+
if (target.tipCurl !== void 0) {
|
|
7881
|
+
consider(
|
|
7882
|
+
scoreAgainst(actual.tipCurl, target.tipCurl, target.tolerance),
|
|
7883
|
+
`${finger} tip`
|
|
7884
|
+
);
|
|
7885
|
+
}
|
|
7886
|
+
}
|
|
7887
|
+
if (definition.pinch) {
|
|
7888
|
+
const { finger, target, tolerance } = definition.pinch;
|
|
7889
|
+
consider(
|
|
7890
|
+
scoreAgainst(shape.pinch[finger], target, tolerance),
|
|
7891
|
+
`${finger} pinch`
|
|
7892
|
+
);
|
|
7893
|
+
}
|
|
7894
|
+
if (definition.orientation) {
|
|
7895
|
+
const { axis, direction, toleranceDegrees } = definition.orientation;
|
|
7896
|
+
const handAxis = axisVector(shape, axis);
|
|
7897
|
+
const wanted = resolveDirection(direction, shape, context);
|
|
7898
|
+
if (!handAxis || !wanted) {
|
|
7899
|
+
consider(0, "orientation");
|
|
7900
|
+
} else {
|
|
7901
|
+
const angle = THREE7.MathUtils.radToDeg(
|
|
7902
|
+
Math.acos(THREE7.MathUtils.clamp(handAxis.dot(wanted), -1, 1))
|
|
7903
|
+
);
|
|
7904
|
+
const normalizedError = Math.max(0, angle - toleranceDegrees) / 90;
|
|
7905
|
+
consider(clamp01(1 - normalizedError / MATCH_FALLOFF), "orientation");
|
|
7906
|
+
}
|
|
7907
|
+
}
|
|
7908
|
+
return {
|
|
7909
|
+
matched: confidence >= 1,
|
|
7910
|
+
confidence,
|
|
7911
|
+
worstConstraint: confidence >= 1 ? null : worstConstraint
|
|
7912
|
+
};
|
|
7913
|
+
}
|
|
7914
|
+
__name(matchHandPose, "matchHandPose");
|
|
7915
|
+
var DEFAULT_ENTER_CONFIDENCE = 0.8;
|
|
7916
|
+
var DEFAULT_EXIT_CONFIDENCE = 0.55;
|
|
7917
|
+
var DEFAULT_MIN_HOLD_SECONDS = 0.15;
|
|
7918
|
+
var DEFAULT_LOST_GRACE_SECONDS = 0.25;
|
|
7919
|
+
var _HandPoseTracker = class _HandPoseTracker {
|
|
7920
|
+
constructor(options) {
|
|
7921
|
+
this.active = false;
|
|
7922
|
+
this.holdingSince = null;
|
|
7923
|
+
this.lostSince = null;
|
|
7924
|
+
this.enterConfidence = options?.enterConfidence ?? DEFAULT_ENTER_CONFIDENCE;
|
|
7925
|
+
const exit = options?.exitConfidence ?? DEFAULT_EXIT_CONFIDENCE;
|
|
7926
|
+
this.exitConfidence = Math.min(exit, this.enterConfidence - 1e-3);
|
|
7927
|
+
this.minHoldSeconds = options?.minHoldSeconds ?? DEFAULT_MIN_HOLD_SECONDS;
|
|
7928
|
+
this.lostGraceSeconds = options?.lostGraceSeconds ?? DEFAULT_LOST_GRACE_SECONDS;
|
|
7929
|
+
}
|
|
7930
|
+
isActive() {
|
|
7931
|
+
return this.active;
|
|
7932
|
+
}
|
|
7933
|
+
/** Feed this frame's confidence. Returns an event on a transition. */
|
|
7934
|
+
update(confidence, now) {
|
|
7935
|
+
this.lostSince = null;
|
|
7936
|
+
if (this.active) {
|
|
7937
|
+
if (confidence < this.exitConfidence) {
|
|
7938
|
+
this.active = false;
|
|
7939
|
+
this.holdingSince = null;
|
|
7940
|
+
return "end";
|
|
7941
|
+
}
|
|
7942
|
+
return null;
|
|
7943
|
+
}
|
|
7944
|
+
if (confidence >= this.enterConfidence) {
|
|
7945
|
+
if (this.holdingSince === null) this.holdingSince = now;
|
|
7946
|
+
if (now - this.holdingSince >= this.minHoldSeconds) {
|
|
7947
|
+
this.active = true;
|
|
7948
|
+
return "start";
|
|
7949
|
+
}
|
|
7950
|
+
return null;
|
|
7951
|
+
}
|
|
7952
|
+
this.holdingSince = null;
|
|
7953
|
+
return null;
|
|
7954
|
+
}
|
|
7955
|
+
/** Feed a frame where the hand isn't tracked at all. */
|
|
7956
|
+
miss(now) {
|
|
7957
|
+
this.holdingSince = null;
|
|
7958
|
+
if (!this.active) return null;
|
|
7959
|
+
if (this.lostSince === null) this.lostSince = now;
|
|
7960
|
+
if (now - this.lostSince < this.lostGraceSeconds) return null;
|
|
7961
|
+
this.active = false;
|
|
7962
|
+
this.lostSince = null;
|
|
7963
|
+
return "end";
|
|
7964
|
+
}
|
|
7965
|
+
reset() {
|
|
7966
|
+
this.active = false;
|
|
7967
|
+
this.holdingSince = null;
|
|
7968
|
+
this.lostSince = null;
|
|
7969
|
+
}
|
|
7970
|
+
};
|
|
7971
|
+
__name(_HandPoseTracker, "HandPoseTracker");
|
|
7972
|
+
var HandPoseTracker = _HandPoseTracker;
|
|
7973
|
+
|
|
7974
|
+
// core/systems/XRHandPoseSystem.ts
|
|
7975
|
+
var _XRHandPoseSystem = class _XRHandPoseSystem {
|
|
7976
|
+
constructor(inputSystem) {
|
|
7977
|
+
this.trackers = /* @__PURE__ */ new Map();
|
|
7978
|
+
this.shapes = /* @__PURE__ */ new Map();
|
|
7979
|
+
/** Reused so head-relative orientation checks allocate nothing per frame. */
|
|
7980
|
+
this.headPosition = new THREE7.Vector3();
|
|
7981
|
+
this.elapsed = 0;
|
|
7982
|
+
this.inputSystem = inputSystem;
|
|
7983
|
+
}
|
|
7984
|
+
update(world, deltaTime) {
|
|
7985
|
+
if (!world.isXrPresenting()) {
|
|
7986
|
+
this.resetAll();
|
|
7987
|
+
return;
|
|
7988
|
+
}
|
|
7989
|
+
this.elapsed += deltaTime;
|
|
7990
|
+
this.refreshShapes();
|
|
7991
|
+
const entries = this.collectEntries(world);
|
|
7992
|
+
if (entries.length === 0) return;
|
|
7993
|
+
world.getCamera().getWorldPosition(this.headPosition);
|
|
7994
|
+
const context = { headPosition: this.headPosition };
|
|
7995
|
+
for (const { entityId, entry } of entries) {
|
|
7996
|
+
const definition = handPoseDefinitionFor(entry, getHandPosePreset);
|
|
7997
|
+
if (!definition) continue;
|
|
7998
|
+
for (const handedness of ["left", "right"]) {
|
|
7999
|
+
if (entry.hand && entry.hand !== handedness) continue;
|
|
8000
|
+
const key = `${entityId}:${entry.id}`;
|
|
8001
|
+
const trackers = this.trackersFor(handedness);
|
|
8002
|
+
let tracker = trackers.get(key);
|
|
8003
|
+
if (!tracker) {
|
|
8004
|
+
tracker = new HandPoseTracker({
|
|
8005
|
+
minHoldSeconds: entry.minHoldSeconds
|
|
8006
|
+
});
|
|
8007
|
+
trackers.set(key, tracker);
|
|
8008
|
+
}
|
|
8009
|
+
const shape = this.shapes.get(handedness) ?? null;
|
|
8010
|
+
if (!shape) {
|
|
8011
|
+
if (tracker.miss(this.elapsed) === "end") {
|
|
8012
|
+
this.dispatch(world, entityId, "end", {
|
|
8013
|
+
poseId: entry.id,
|
|
8014
|
+
poseName: entry.name,
|
|
8015
|
+
handedness,
|
|
8016
|
+
confidence: 0
|
|
8017
|
+
});
|
|
8018
|
+
}
|
|
8019
|
+
continue;
|
|
8020
|
+
}
|
|
8021
|
+
const match = matchHandPose(shape, definition, context);
|
|
8022
|
+
const event = tracker.update(match.confidence, this.elapsed);
|
|
8023
|
+
if (event) {
|
|
8024
|
+
this.dispatch(world, entityId, event, {
|
|
8025
|
+
poseId: entry.id,
|
|
8026
|
+
poseName: entry.name,
|
|
8027
|
+
handedness,
|
|
8028
|
+
confidence: match.confidence
|
|
8029
|
+
});
|
|
8030
|
+
}
|
|
8031
|
+
}
|
|
8032
|
+
}
|
|
8033
|
+
}
|
|
8034
|
+
/** Derive each hand's shape once for the whole frame. */
|
|
8035
|
+
refreshShapes() {
|
|
8036
|
+
for (const handedness of ["left", "right"]) {
|
|
8037
|
+
const input = this.inputSystem.getInput(handedness);
|
|
8038
|
+
const joints = input?.joints ?? null;
|
|
8039
|
+
this.shapes.set(
|
|
8040
|
+
handedness,
|
|
8041
|
+
joints ? computeHandShape(joints, handedness) : null
|
|
8042
|
+
);
|
|
8043
|
+
}
|
|
8044
|
+
}
|
|
8045
|
+
collectEntries(world) {
|
|
8046
|
+
const collected = [];
|
|
8047
|
+
for (const entityId of world.query(XRHandPoseComponent)) {
|
|
8048
|
+
const component = world.getComponent(entityId, XRHandPoseComponent);
|
|
8049
|
+
if (!component?.enabled) continue;
|
|
8050
|
+
for (const entry of component.poses) {
|
|
8051
|
+
if (entry.enabled) collected.push({ entityId, entry });
|
|
8052
|
+
}
|
|
8053
|
+
}
|
|
8054
|
+
return collected;
|
|
8055
|
+
}
|
|
8056
|
+
trackersFor(handedness) {
|
|
8057
|
+
let trackers = this.trackers.get(handedness);
|
|
8058
|
+
if (!trackers) {
|
|
8059
|
+
trackers = /* @__PURE__ */ new Map();
|
|
8060
|
+
this.trackers.set(handedness, trackers);
|
|
8061
|
+
}
|
|
8062
|
+
return trackers;
|
|
8063
|
+
}
|
|
8064
|
+
/**
|
|
8065
|
+
* Deliver to the entity's own Behaviours.
|
|
8066
|
+
*
|
|
8067
|
+
* Scoped to the entity that declared the pose rather than broadcast, so two
|
|
8068
|
+
* objects listening for the same gesture stay independent.
|
|
8069
|
+
*/
|
|
8070
|
+
dispatch(world, entityId, event, detail) {
|
|
8071
|
+
const script = world.getComponent(entityId, ScriptComponent);
|
|
8072
|
+
const instances = script?.scriptInstances;
|
|
8073
|
+
if (!instances) return;
|
|
8074
|
+
for (const instance of instances.values()) {
|
|
8075
|
+
if (!(instance instanceof Behaviour)) continue;
|
|
8076
|
+
try {
|
|
8077
|
+
if (event === "start") instance.onHandPoseStart(detail);
|
|
8078
|
+
else instance.onHandPoseEnd(detail);
|
|
8079
|
+
} catch (error) {
|
|
8080
|
+
console.error("[XRHandPoseSystem] Behaviour hook failed:", error);
|
|
8081
|
+
}
|
|
8082
|
+
}
|
|
8083
|
+
}
|
|
8084
|
+
resetAll() {
|
|
8085
|
+
for (const trackers of this.trackers.values()) {
|
|
8086
|
+
for (const tracker of trackers.values()) tracker.reset();
|
|
8087
|
+
}
|
|
8088
|
+
this.shapes.clear();
|
|
8089
|
+
}
|
|
8090
|
+
/** Live shape for a hand — powers the editor's pose readout. */
|
|
8091
|
+
getHandShape(handedness) {
|
|
8092
|
+
return this.shapes.get(handedness) ?? null;
|
|
8093
|
+
}
|
|
8094
|
+
/** Whether a given entity's pose is currently held. */
|
|
8095
|
+
isPoseActive(entityId, poseId, handedness) {
|
|
8096
|
+
return this.trackers.get(handedness)?.get(`${entityId}:${poseId}`)?.isActive() ?? false;
|
|
8097
|
+
}
|
|
8098
|
+
};
|
|
8099
|
+
__name(_XRHandPoseSystem, "XRHandPoseSystem");
|
|
8100
|
+
var XRHandPoseSystem = _XRHandPoseSystem;
|
|
7146
8101
|
|
|
7147
|
-
export { ADDED_NODE_MARKER_COMPONENT, ARC_GRAVITY, ARC_MAX_TIME, ARC_SPEED, ARC_STEPS, AnimationSystem, AudioSystem, Behaviour, CameraSyncSystem, EnvironmentSyncSystem, HYDRATABLE_ENTITY_KINDS, InputSystem, InstancedMeshSystem, KeyboardKeys, LightSyncSystem, MaterialSyncSystem, MeshGeometrySystem, NODE_KEY_SEPARATOR, NO_XR_CAPABILITIES, OVERRIDE_DIFF_EXCLUDED_COMPONENTS, OVERRIDE_ROOT_EXCLUDED_COMPONENTS, ObjectVisibilitySystem, PREFAB_FILE_VERSION, PREFAB_FILE_VERSION_NESTED, PhysicsSyncSystem, PostprocessingSyncSystem, RenderSystem, SCRIPT_OBJECT_ROOT_FLAG, ScriptBehaviourSystem, ScriptObject, ScriptObjectSystem, TransformSyncSystem, VRTeleportAreaSystem, VideoSystem, XRControllerVisualSystem, XRInputSystem, XRInteractionSystem, XRLocomotionSystem, XRSessionSystem, XRTeleportVisualSystem, applyHandMaterial, arcGroundCrossing, buildComponentValueMaps, buildNodeKeyMapsFromSnapshot, buildNodeMapsFromSnapshot, canonicalizeComponentValue, clearPrefabCache, collectDiffPaths, collectEntitySubtree, collectSnapshotSubtreeIds, computeInstanceOverrides, createHandMaterial, decanonicalizeComponentValue, despawnPrefab, encodeOverride, findSnapshotRootEntityIds, findTopLevelNestedRegions, groupOverridePaths, hydrateWorldFromSnapshot, hydrateWorldFromSnapshotDetailed, indexSnapshotByEntity, instantiateSubtree, loadPrefab, mergeComponentValue, parseOverride, parsePrefabAsset, prefabFileVersionFor, readXrCapabilities, restoreComponentState, restorePostCreateComponents, sampleTeleportArc, setPrefabAssetUrlResolver, spawnPrefab };
|
|
7148
|
-
//# sourceMappingURL=chunk-
|
|
7149
|
-
//# sourceMappingURL=chunk-
|
|
8102
|
+
export { ADDED_NODE_MARKER_COMPONENT, ARC_GRAVITY, ARC_MAX_TIME, ARC_SPEED, ARC_STEPS, AnimationSystem, AudioSystem, Behaviour, CURLED_COS, CameraSyncSystem, DEFAULT_ENTER_CONFIDENCE, DEFAULT_EXIT_CONFIDENCE, DEFAULT_LOST_GRACE_SECONDS, DEFAULT_MIN_HOLD_SECONDS, EnvironmentSyncSystem, FALLBACK_HAND_SCALE, HYDRATABLE_ENTITY_KINDS, HandPoseTracker, InputSystem, InstancedMeshSystem, KeyboardKeys, LightSyncSystem, MATCH_FALLOFF, MAX_SAMPLE_AGE_SECONDS, MaterialSyncSystem, MeshGeometrySystem, NODE_KEY_SEPARATOR, NO_XR_CAPABILITIES, OVERRIDE_DIFF_EXCLUDED_COMPONENTS, OVERRIDE_ROOT_EXCLUDED_COMPONENTS, ObjectVisibilitySystem, PINCH_FINGERS, PREFAB_FILE_VERSION, PREFAB_FILE_VERSION_NESTED, PhysicsSyncSystem, PostprocessingSyncSystem, RenderSystem, SCRIPT_OBJECT_ROOT_FLAG, STRAIGHT_COS, ScriptBehaviourSystem, ScriptObject, ScriptObjectSystem, ThrowVelocityTracker, TransformSyncSystem, VELOCITY_SAMPLES, VRTeleportAreaSystem, VideoSystem, XRControllerVisualSystem, XRHandPoseSystem, XRInputSystem, XRInteractionSystem, XRLocomotionSystem, XRSessionSystem, XRTeleportVisualSystem, XR_FINGERS, angularVelocityFrom, applyHandMaterial, arcGroundCrossing, buildComponentValueMaps, buildNodeKeyMapsFromSnapshot, buildNodeMapsFromSnapshot, canonicalizeComponentValue, clearPrefabCache, collectDiffPaths, collectEntitySubtree, collectSnapshotSubtreeIds, computeFingerCurl, computeHandShape, computeInstanceOverrides, computePalmPose, computePalmWidth, computePinch, computeSpread, createHandMaterial, decanonicalizeComponentValue, despawnPrefab, encodeOverride, estimateHandScale, findSnapshotRootEntityIds, findTopLevelNestedRegions, groupOverridePaths, hydrateWorldFromSnapshot, hydrateWorldFromSnapshotDetailed, indexSnapshotByEntity, instantiateSubtree, loadPrefab, matchHandPose, mergeComponentValue, parseOverride, parsePrefabAsset, prefabFileVersionFor, readXrCapabilities, restoreComponentState, restorePostCreateComponents, sampleTeleportArc, setPrefabAssetUrlResolver, spawnPrefab };
|
|
8103
|
+
//# sourceMappingURL=chunk-LMTB4KQ4.js.map
|
|
8104
|
+
//# sourceMappingURL=chunk-LMTB4KQ4.js.map
|