@relu-interactives/spatial-ecs 0.3.19 → 0.3.21
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/accessing-components.md +7 -6
- package/dist/docs/guide/behaviour-data-binding/component-input.md +10 -6
- package/dist/docs/guide/behaviour-data-binding/entity-input.md +13 -5
- package/dist/docs/guide/camera-rigs.md +2 -2
- package/dist/docs/guide/components/animation.md +12 -5
- package/dist/docs/guide/components/audio.md +13 -6
- package/dist/docs/guide/components/camera.md +10 -4
- package/dist/docs/guide/components/entity-type.md +13 -6
- package/dist/docs/guide/components/image-target-anchor.md +6 -2
- package/dist/docs/guide/components/image-target.md +10 -5
- package/dist/docs/guide/components/image.md +10 -4
- package/dist/docs/guide/components/instance-slot.md +7 -3
- package/dist/docs/guide/components/instanced-mesh.md +15 -7
- package/dist/docs/guide/components/light.md +10 -4
- package/dist/docs/guide/components/material.md +14 -8
- package/dist/docs/guide/components/mesh-geometry.md +10 -4
- package/dist/docs/guide/components/mesh.md +10 -4
- package/dist/docs/guide/components/model.md +4 -2
- package/dist/docs/guide/components/name.md +11 -6
- package/dist/docs/guide/components/object3d-ref.md +7 -3
- package/dist/docs/guide/components/parent-id.md +10 -4
- package/dist/docs/guide/components/physics.md +12 -6
- package/dist/docs/guide/components/script.md +10 -4
- package/dist/docs/guide/components/sprite.md +10 -4
- package/dist/docs/guide/components/transform.md +6 -2
- package/dist/docs/guide/components/video.md +10 -4
- package/dist/docs/guide/prefabs.md +2 -0
- package/dist/docs/guide/runtime-entity-management.md +1 -1
- package/dist/docs/guide/spawning/instanced-mesh.md +11 -6
- package/dist/docs/guide/systems/input-system.md +4 -1
- package/dist/docs/guide/systems/script-object-system.md +9 -1
- package/dist/docs/guide/vr-and-hand-poses.md +105 -1
- package/dist/docs/guide/world-api.md +23 -15
- 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 +4 -2
- 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.
|
|
@@ -132,9 +192,11 @@ Add an **XR Hand Pose** component and list the poses to watch for. Each is eithe
|
|
|
132
192
|
```ts
|
|
133
193
|
import {
|
|
134
194
|
Behaviour,
|
|
135
|
-
Object3DRef,
|
|
136
195
|
type XrHandPoseEventDetail,
|
|
137
196
|
} from "@relu-interactives/spatial-ecs";
|
|
197
|
+
import {
|
|
198
|
+
Object3DRef,
|
|
199
|
+
} from "@relu-interactives/spatial-ecs/components";
|
|
138
200
|
|
|
139
201
|
export default class Portal extends Behaviour {
|
|
140
202
|
onHandPoseStart(event: XrHandPoseEventDetail) {
|
|
@@ -154,6 +216,48 @@ export default class Portal extends Behaviour {
|
|
|
154
216
|
|
|
155
217
|
`XrHandPoseEventDetail` is `{ poseId, poseName, handedness, confidence }`. Match on `poseId` — it is the id set in the inspector, not the preset name.
|
|
156
218
|
|
|
219
|
+
**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 })`.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import {
|
|
223
|
+
Behaviour,
|
|
224
|
+
type EntityInput,
|
|
225
|
+
type WorldEntityView,
|
|
226
|
+
type XrHandPoseEventDetail,
|
|
227
|
+
} from "@relu-interactives/spatial-ecs";
|
|
228
|
+
import {
|
|
229
|
+
MaterialComponent,
|
|
230
|
+
} from "@relu-interactives/spatial-ecs/components";
|
|
231
|
+
|
|
232
|
+
export default class PoseColor extends Behaviour {
|
|
233
|
+
data = { target: { type: "entity", value: null } as unknown as EntityInput };
|
|
234
|
+
private original = "#ffffff";
|
|
235
|
+
|
|
236
|
+
init() {
|
|
237
|
+
const material = this.material();
|
|
238
|
+
const current = material?.values?.[0]?.color;
|
|
239
|
+
if (typeof current === "string") this.original = current;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
onHandPoseStart(event: XrHandPoseEventDetail) {
|
|
243
|
+
if (event.poseId === "approve") this.material()?.updateValue(0, { color: "#0066ff" });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
onHandPoseEnd() {
|
|
247
|
+
this.material()?.updateValue(0, { color: this.original });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private material() {
|
|
251
|
+
const target = this.data.target.value as WorldEntityView | null;
|
|
252
|
+
return target
|
|
253
|
+
? this.world.getComponent(target.entityId, MaterialComponent)
|
|
254
|
+
: null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**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.
|
|
260
|
+
|
|
157
261
|
### Presets
|
|
158
262
|
|
|
159
263
|
`fist`, `open-palm`, `point`, `thumbs-up`, `thumbs-down`, `peace`, `pinch`, `ok`, `gun`, `rock`, `shaka`, `palm-up`.
|
|
@@ -10,7 +10,8 @@ This page summarises the methods most useful from a `Behaviour` and shows how to
|
|
|
10
10
|
## Inside a Behaviour
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
|
-
import { Behaviour
|
|
13
|
+
import { Behaviour } from "@relu-interactives/spatial-ecs";
|
|
14
|
+
import { TransformComponent } from "@relu-interactives/spatial-ecs/components";
|
|
14
15
|
|
|
15
16
|
export default class Spinner extends Behaviour {
|
|
16
17
|
onUpdate() {
|
|
@@ -120,7 +121,8 @@ Both methods accept `Vec3` (`THREE.Vector3` or `{ x, y, z }`) and return `Inters
|
|
|
120
121
|
### Find an entity by name and move it
|
|
121
122
|
|
|
122
123
|
```ts
|
|
123
|
-
import { Behaviour
|
|
124
|
+
import { Behaviour } from "@relu-interactives/spatial-ecs";
|
|
125
|
+
import { TransformComponent } from "@relu-interactives/spatial-ecs/components";
|
|
124
126
|
|
|
125
127
|
export default class TargetMover extends Behaviour {
|
|
126
128
|
onUpdate() {
|
|
@@ -138,7 +140,8 @@ export default class TargetMover extends Behaviour {
|
|
|
138
140
|
### Iterate every entity that owns a component
|
|
139
141
|
|
|
140
142
|
```ts
|
|
141
|
-
import { Behaviour
|
|
143
|
+
import { Behaviour } from "@relu-interactives/spatial-ecs";
|
|
144
|
+
import { LightComponent } from "@relu-interactives/spatial-ecs/components";
|
|
142
145
|
|
|
143
146
|
export default class FlickerAllLights extends Behaviour {
|
|
144
147
|
onUpdate() {
|
|
@@ -188,7 +191,10 @@ export default class HierarchyWalker extends Behaviour {
|
|
|
188
191
|
|
|
189
192
|
```ts
|
|
190
193
|
import { Behaviour } from "@relu-interactives/spatial-ecs";
|
|
191
|
-
|
|
194
|
+
// Your own Behaviour, in a sibling file.
|
|
195
|
+
declare class Health extends Behaviour {
|
|
196
|
+
takeDamage(amount: number): void;
|
|
197
|
+
}
|
|
192
198
|
|
|
193
199
|
export default class DamageDealer extends Behaviour {
|
|
194
200
|
fire() {
|
|
@@ -229,12 +235,12 @@ export default class Spawner extends Behaviour {
|
|
|
229
235
|
);
|
|
230
236
|
this.world.getScene().add(mesh);
|
|
231
237
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
// Each factory takes (world, entityId, ...constructorArgs) and attaches
|
|
239
|
+
// the component itself — there is nothing to pass to addComponents.
|
|
240
|
+
createNameComponent(this.world, id, "RuntimeCube");
|
|
241
|
+
createEntityTypeComponent(this.world, id, "cube");
|
|
242
|
+
createTransformComponent(this.world, id, { position: { x: 0, y: 1, z: 0 } });
|
|
243
|
+
createObject3DRefComponent(this.world, id, mesh);
|
|
238
244
|
}
|
|
239
245
|
}
|
|
240
246
|
```
|
|
@@ -343,7 +349,8 @@ export default class LookDetector extends Behaviour {
|
|
|
343
349
|
### Read keyboard and mouse input
|
|
344
350
|
|
|
345
351
|
```ts
|
|
346
|
-
import { Behaviour,
|
|
352
|
+
import { Behaviour, KeyboardKeys } from "@relu-interactives/spatial-ecs";
|
|
353
|
+
import { TransformComponent } from "@relu-interactives/spatial-ecs/components";
|
|
347
354
|
|
|
348
355
|
export default class WASDMove extends Behaviour {
|
|
349
356
|
speed = 3;
|
|
@@ -392,11 +399,12 @@ export default class FpsLook extends Behaviour {
|
|
|
392
399
|
### Reach into the physics world
|
|
393
400
|
|
|
394
401
|
```ts
|
|
395
|
-
import { Behaviour,
|
|
402
|
+
import { Behaviour, KeyboardKeys } from "@relu-interactives/spatial-ecs";
|
|
403
|
+
import { PhysicsComponent } from "@relu-interactives/spatial-ecs/components";
|
|
396
404
|
|
|
397
405
|
export default class Jump extends Behaviour {
|
|
398
406
|
onUpdate() {
|
|
399
|
-
if (!this.world.getKeyDown(
|
|
407
|
+
if (!this.world.getKeyDown(KeyboardKeys.Space)) return;
|
|
400
408
|
|
|
401
409
|
const physics = this.world.getComponent(this.entity.entityId, PhysicsComponent);
|
|
402
410
|
physics?.rigidBody?.applyImpulse({ x: 0, y: 5, z: 0 }, true);
|
|
@@ -411,12 +419,12 @@ ticks stop when paused — physics, animation, scripts, and the render pass.
|
|
|
411
419
|
The last rendered frame stays frozen on the canvas.
|
|
412
420
|
|
|
413
421
|
```ts
|
|
414
|
-
import { Behaviour } from "@relu-interactives/spatial-ecs";
|
|
422
|
+
import { Behaviour, KeyboardKeys } from "@relu-interactives/spatial-ecs";
|
|
415
423
|
|
|
416
424
|
export default class PauseOnHit extends Behaviour {
|
|
417
425
|
onUpdate() {
|
|
418
426
|
// Pause when the spacebar is pressed, resume when pressed again.
|
|
419
|
-
if (!this.world.getKeyDown(
|
|
427
|
+
if (!this.world.getKeyDown(KeyboardKeys.Space)) return;
|
|
420
428
|
|
|
421
429
|
if (this.world.isPaused()) {
|
|
422
430
|
this.world.resume();
|
|
@@ -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,
|