@relu-interactives/spatial-ecs 0.3.19 → 0.3.20
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/{chunk-JYKFLQB5.js → chunk-CDSE7476.js} +38 -3
- package/dist/chunk-CDSE7476.js.map +1 -0
- package/dist/docs/guide/vr-and-hand-poses.md +100 -0
- package/dist/{index-C_589fc4.d.ts → index-6l4hn2Sh.d.ts} +14 -0
- package/dist/{index-3pNbH0iP.d.cts → index-C2I5PxCI.d.cts} +14 -0
- package/dist/index.cjs +36 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/systems/index.cjs +36 -1
- package/dist/systems/index.cjs.map +1 -1
- package/dist/systems/index.d.cts +1 -1
- package/dist/systems/index.d.ts +1 -1
- package/dist/systems/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-JYKFLQB5.js.map +0 -1
|
@@ -125,6 +125,66 @@ Vision Pro drives WebXR entirely through **gaze and pinch**. There are no contro
|
|
|
125
125
|
- Hand tracking (and therefore hand poses) is available on visionOS 2 and later, but asks the user for a second permission. The OS draws the user's real hands.
|
|
126
126
|
- `immersive-ar` is not supported by Safari on visionOS, so VR projects are the only kind that run there.
|
|
127
127
|
|
|
128
|
+
### Scripting VR interactions
|
|
129
|
+
|
|
130
|
+
A few patterns that cover most scenes.
|
|
131
|
+
|
|
132
|
+
**Score when a thrown object passes through something.** Give the ball a dynamic rigidbody and an XR Interactable; the throw itself is handled for you.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { Behaviour, type XrInputState } from "@relu-interactives/spatial-ecs";
|
|
136
|
+
|
|
137
|
+
export default class Ball extends Behaviour {
|
|
138
|
+
onRelease(input: XrInputState) {
|
|
139
|
+
// Fires the moment it leaves the hand — the engine has already applied the
|
|
140
|
+
// velocity, so this is where scoring or audio goes.
|
|
141
|
+
console.log("released by", input.handedness);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**A panel you point at but cannot pick up.** Set `pointable: true` and `grabbable: false`, then act on the pointer.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
export default class Button extends Behaviour {
|
|
150
|
+
onSelectStart() {
|
|
151
|
+
this.world.emit?.("door:open");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**React to a two-handed hold.** Useful for steadying a weapon, showing a bipod, or gating an action on both hands being committed.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
export default class Rifle extends Behaviour {
|
|
160
|
+
private braced = false;
|
|
161
|
+
|
|
162
|
+
onSecondaryGrab() {
|
|
163
|
+
this.braced = true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
onSecondaryRelease() {
|
|
167
|
+
this.braced = false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
onUpdate() {
|
|
171
|
+
if (this.braced) { /* tighter aim, less sway… */ }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Adapt to the device.** Branch on what the session granted, never on a device name.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
onXRSessionStart(capabilities: XrCapabilities) {
|
|
180
|
+
if (!capabilities.handTracking) {
|
|
181
|
+
// Controllers only — hide anything that depends on finger poses.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Two rules worth internalising: **move the rig, never the camera** (the headset owns the camera while presenting and overwrites it every frame), and give any destructive gesture a longer **Hold Time** so it cannot fire in passing.
|
|
187
|
+
|
|
128
188
|
## Hand poses
|
|
129
189
|
|
|
130
190
|
Add an **XR Hand Pose** component and list the poses to watch for. Each is either a built-in preset or a custom set of finger curls.
|
|
@@ -154,6 +214,46 @@ export default class Portal extends Behaviour {
|
|
|
154
214
|
|
|
155
215
|
`XrHandPoseEventDetail` is `{ poseId, poseName, handedness, confidence }`. Match on `poseId` — it is the id set in the inspector, not the preset name.
|
|
156
216
|
|
|
217
|
+
**Changing another object's colour from a pose.** `MaterialComponent.updateValue` takes an index and a PATCH OBJECT — `updateValue(0, { color })`, not `updateValue(0, "color", value)`.
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import {
|
|
221
|
+
Behaviour,
|
|
222
|
+
MaterialComponent,
|
|
223
|
+
type EntityInput,
|
|
224
|
+
type WorldEntityView,
|
|
225
|
+
type XrHandPoseEventDetail,
|
|
226
|
+
} from "@relu-interactives/spatial-ecs";
|
|
227
|
+
|
|
228
|
+
export default class PoseColor extends Behaviour {
|
|
229
|
+
data = { target: { type: "entity", value: null } as unknown as EntityInput };
|
|
230
|
+
private original = "#ffffff";
|
|
231
|
+
|
|
232
|
+
init() {
|
|
233
|
+
const material = this.material();
|
|
234
|
+
const current = material?.values?.[0]?.color;
|
|
235
|
+
if (typeof current === "string") this.original = current;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
onHandPoseStart(event: XrHandPoseEventDetail) {
|
|
239
|
+
if (event.poseId === "approve") this.material()?.updateValue(0, { color: "#0066ff" });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
onHandPoseEnd() {
|
|
243
|
+
this.material()?.updateValue(0, { color: this.original });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private material() {
|
|
247
|
+
const target = this.data.target.value as WorldEntityView | null;
|
|
248
|
+
return target
|
|
249
|
+
? this.world.getComponent(target.entityId, MaterialComponent)
|
|
250
|
+
: null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Hand poses require HAND TRACKING.** They are read from the 25 tracked finger joints, so nothing fires while the user is holding controllers — put them down and enable hand tracking on the headset. If a scene has poses configured and no tracked hand is seen for a few seconds, the runtime says so in the console.
|
|
256
|
+
|
|
157
257
|
### Presets
|
|
158
258
|
|
|
159
259
|
`fist`, `open-palm`, `point`, `thumbs-up`, `thumbs-down`, `peace`, `pinch`, `ok`, `gun`, `rock`, `shaka`, `palm-up`.
|
|
@@ -401,6 +401,18 @@ declare class XRInputSystem implements EcsSystem {
|
|
|
401
401
|
getInputs(): XrInputState[];
|
|
402
402
|
/** The first source for a hand, or null. Prefer this for hand-specific logic. */
|
|
403
403
|
getInput(handedness: XrHandedness): XrInputState | null;
|
|
404
|
+
/**
|
|
405
|
+
* The ARTICULATED source for a hand — the one carrying joint poses.
|
|
406
|
+
*
|
|
407
|
+
* Distinct from {@link getInput} because a hand can be backed by several
|
|
408
|
+
* sources at once: a Quest reports a controller AND a tracked hand for the
|
|
409
|
+
* same handedness, and Vision Pro adds a transient pinch alongside its two
|
|
410
|
+
* persistent hands. `getInput` returns whichever was seen first, so anything
|
|
411
|
+
* needing JOINTS has to ask for them specifically — taking the first match
|
|
412
|
+
* silently handed pose detection a controller with no joints, and no pose
|
|
413
|
+
* ever fired.
|
|
414
|
+
*/
|
|
415
|
+
getHandInput(handedness: XrHandedness): XrInputState | null;
|
|
404
416
|
/** True while ANY source is selecting — the portable "is the user acting" check. */
|
|
405
417
|
isSelecting(): boolean;
|
|
406
418
|
dispose(): void;
|
|
@@ -800,6 +812,8 @@ declare class XRHandPoseSystem implements EcsSystem {
|
|
|
800
812
|
/** Reused so head-relative orientation checks allocate nothing per frame. */
|
|
801
813
|
private readonly headPosition;
|
|
802
814
|
private elapsed;
|
|
815
|
+
private warnedNoHands;
|
|
816
|
+
private noHandSeconds;
|
|
803
817
|
constructor(inputSystem: XRInputSystem);
|
|
804
818
|
update(world: World, deltaTime: number): void;
|
|
805
819
|
/** Derive each hand's shape once for the whole frame. */
|
|
@@ -401,6 +401,18 @@ declare class XRInputSystem implements EcsSystem {
|
|
|
401
401
|
getInputs(): XrInputState[];
|
|
402
402
|
/** The first source for a hand, or null. Prefer this for hand-specific logic. */
|
|
403
403
|
getInput(handedness: XrHandedness): XrInputState | null;
|
|
404
|
+
/**
|
|
405
|
+
* The ARTICULATED source for a hand — the one carrying joint poses.
|
|
406
|
+
*
|
|
407
|
+
* Distinct from {@link getInput} because a hand can be backed by several
|
|
408
|
+
* sources at once: a Quest reports a controller AND a tracked hand for the
|
|
409
|
+
* same handedness, and Vision Pro adds a transient pinch alongside its two
|
|
410
|
+
* persistent hands. `getInput` returns whichever was seen first, so anything
|
|
411
|
+
* needing JOINTS has to ask for them specifically — taking the first match
|
|
412
|
+
* silently handed pose detection a controller with no joints, and no pose
|
|
413
|
+
* ever fired.
|
|
414
|
+
*/
|
|
415
|
+
getHandInput(handedness: XrHandedness): XrInputState | null;
|
|
404
416
|
/** True while ANY source is selecting — the portable "is the user acting" check. */
|
|
405
417
|
isSelecting(): boolean;
|
|
406
418
|
dispose(): void;
|
|
@@ -800,6 +812,8 @@ declare class XRHandPoseSystem implements EcsSystem {
|
|
|
800
812
|
/** Reused so head-relative orientation checks allocate nothing per frame. */
|
|
801
813
|
private readonly headPosition;
|
|
802
814
|
private elapsed;
|
|
815
|
+
private warnedNoHands;
|
|
816
|
+
private noHandSeconds;
|
|
803
817
|
constructor(inputSystem: XRInputSystem);
|
|
804
818
|
update(world: World, deltaTime: number): void;
|
|
805
819
|
/** Derive each hand's shape once for the whole frame. */
|
package/dist/index.cjs
CHANGED
|
@@ -11019,6 +11019,25 @@ var _XRInputSystem = class _XRInputSystem {
|
|
|
11019
11019
|
}
|
|
11020
11020
|
return null;
|
|
11021
11021
|
}
|
|
11022
|
+
/**
|
|
11023
|
+
* The ARTICULATED source for a hand — the one carrying joint poses.
|
|
11024
|
+
*
|
|
11025
|
+
* Distinct from {@link getInput} because a hand can be backed by several
|
|
11026
|
+
* sources at once: a Quest reports a controller AND a tracked hand for the
|
|
11027
|
+
* same handedness, and Vision Pro adds a transient pinch alongside its two
|
|
11028
|
+
* persistent hands. `getInput` returns whichever was seen first, so anything
|
|
11029
|
+
* needing JOINTS has to ask for them specifically — taking the first match
|
|
11030
|
+
* silently handed pose detection a controller with no joints, and no pose
|
|
11031
|
+
* ever fired.
|
|
11032
|
+
*/
|
|
11033
|
+
getHandInput(handedness) {
|
|
11034
|
+
for (const record of this.records.values()) {
|
|
11035
|
+
const state = record.state;
|
|
11036
|
+
if (state.handedness !== handedness) continue;
|
|
11037
|
+
if (state.joints) return state;
|
|
11038
|
+
}
|
|
11039
|
+
return null;
|
|
11040
|
+
}
|
|
11022
11041
|
/** True while ANY source is selecting — the portable "is the user acting" check. */
|
|
11023
11042
|
isSelecting() {
|
|
11024
11043
|
for (const record of this.records.values()) {
|
|
@@ -13283,6 +13302,8 @@ var _XRHandPoseSystem = class _XRHandPoseSystem {
|
|
|
13283
13302
|
/** Reused so head-relative orientation checks allocate nothing per frame. */
|
|
13284
13303
|
this.headPosition = new THREE51__namespace.Vector3();
|
|
13285
13304
|
this.elapsed = 0;
|
|
13305
|
+
this.warnedNoHands = false;
|
|
13306
|
+
this.noHandSeconds = 0;
|
|
13286
13307
|
this.inputSystem = inputSystem;
|
|
13287
13308
|
}
|
|
13288
13309
|
update(world, deltaTime) {
|
|
@@ -13294,6 +13315,20 @@ var _XRHandPoseSystem = class _XRHandPoseSystem {
|
|
|
13294
13315
|
this.refreshShapes();
|
|
13295
13316
|
const entries = this.collectEntries(world);
|
|
13296
13317
|
if (entries.length === 0) return;
|
|
13318
|
+
if (!this.warnedNoHands) {
|
|
13319
|
+
const tracked = this.shapes.get("left") !== null || this.shapes.get("right") !== null;
|
|
13320
|
+
if (tracked) {
|
|
13321
|
+
this.warnedNoHands = true;
|
|
13322
|
+
} else {
|
|
13323
|
+
this.noHandSeconds += deltaTime;
|
|
13324
|
+
if (this.noHandSeconds > 5) {
|
|
13325
|
+
this.warnedNoHands = true;
|
|
13326
|
+
console.warn(
|
|
13327
|
+
"[XRHandPoseSystem] Hand poses are configured but no tracked hands have been seen. Hand poses need HAND TRACKING \u2014 put the controllers down and enable hand tracking on the headset."
|
|
13328
|
+
);
|
|
13329
|
+
}
|
|
13330
|
+
}
|
|
13331
|
+
}
|
|
13297
13332
|
world.getCamera().getWorldPosition(this.headPosition);
|
|
13298
13333
|
const context = { headPosition: this.headPosition };
|
|
13299
13334
|
for (const { entityId, entry } of entries) {
|
|
@@ -13338,7 +13373,7 @@ var _XRHandPoseSystem = class _XRHandPoseSystem {
|
|
|
13338
13373
|
/** Derive each hand's shape once for the whole frame. */
|
|
13339
13374
|
refreshShapes() {
|
|
13340
13375
|
for (const handedness of ["left", "right"]) {
|
|
13341
|
-
const input = this.inputSystem.
|
|
13376
|
+
const input = this.inputSystem.getHandInput(handedness);
|
|
13342
13377
|
const joints = input?.joints ?? null;
|
|
13343
13378
|
this.shapes.set(
|
|
13344
13379
|
handedness,
|