@relu-interactives/spatial-ecs 0.2.16 → 0.2.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/{System-BeuJ1xt5.d.cts → System-BjwOBx81.d.cts} +146 -1
- package/dist/{System-BeuJ1xt5.d.ts → System-BjwOBx81.d.ts} +146 -1
- package/dist/{Video-D4B-z74P.d.cts → Video-B8yVI1tF.d.cts} +17 -3
- package/dist/{Video-Cym2C52L.d.ts → Video-B9z5AtIf.d.ts} +17 -3
- package/dist/{chunk-EPK6MA7X.js → chunk-67MFQDHX.js} +1646 -8
- package/dist/chunk-67MFQDHX.js.map +1 -0
- package/dist/{chunk-GRUK5TNL.js → chunk-DY4ETMP2.js} +3 -3
- package/dist/{chunk-GRUK5TNL.js.map → chunk-DY4ETMP2.js.map} +1 -1
- package/dist/{chunk-CJW6K3CL.js → chunk-HUHN6IFD.js} +62 -5
- package/dist/chunk-HUHN6IFD.js.map +1 -0
- package/dist/{chunk-FTIQQK62.js → chunk-Z6OMYEXU.js} +42 -3
- package/dist/chunk-Z6OMYEXU.js.map +1 -0
- package/dist/{chunk-R2WMVXZZ.js → chunk-ZXGHKX7N.js} +5 -3
- package/dist/chunk-ZXGHKX7N.js.map +1 -0
- package/dist/components/index.cjs +39 -0
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.cts +3 -2
- package/dist/components/index.d.ts +3 -2
- package/dist/components/index.js +1 -1
- package/dist/docs/guide/accessing-components.md +9 -9
- package/dist/docs/guide/camera-rigs.md +525 -0
- package/dist/docs/guide/components/animation.md +5 -5
- package/dist/docs/guide/components/entity-type.md +2 -2
- package/dist/docs/guide/components/image.md +2 -2
- package/dist/docs/guide/components/instanced-mesh.md +3 -3
- package/dist/docs/guide/components/material.md +4 -0
- package/dist/docs/guide/components/mesh-geometry.md +3 -3
- package/dist/docs/guide/components/mesh.md +3 -3
- package/dist/docs/guide/components/model.md +2 -2
- package/dist/docs/guide/components/name.md +4 -4
- package/dist/docs/guide/components/object3d-ref.md +2 -2
- package/dist/docs/guide/components/parent-id.md +2 -2
- package/dist/docs/guide/components/physics.md +10 -0
- package/dist/docs/guide/components/video.md +2 -2
- package/dist/docs/guide/getting-started/editor/assets-browser.md +1 -1
- package/dist/docs/guide/prefabs.md +133 -0
- package/dist/docs/guide/world-api.md +4 -3
- package/dist/factories/index.cjs +136 -64
- package/dist/factories/index.cjs.map +1 -1
- package/dist/factories/index.d.cts +5 -4
- package/dist/factories/index.d.ts +5 -4
- package/dist/factories/index.js +3 -3
- package/dist/{index-BoJ3_6a3.d.ts → index-BBI2YdCu.d.ts} +60 -4
- package/dist/{index-BfoOC2CG.d.cts → index-LHLKpJhZ.d.cts} +60 -4
- package/dist/index.cjs +5978 -4972
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +215 -7
- package/dist/index.d.ts +215 -7
- package/dist/index.js +203 -960
- package/dist/index.js.map +1 -1
- package/dist/systems/index.cjs +1367 -0
- package/dist/systems/index.cjs.map +1 -1
- package/dist/systems/index.d.cts +2 -2
- package/dist/systems/index.d.ts +2 -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.cjs +3 -1
- package/dist/worldSnapshotUtils.cjs.map +1 -1
- package/dist/worldSnapshotUtils.d.cts +1 -1
- package/dist/worldSnapshotUtils.d.ts +1 -1
- package/dist/worldSnapshotUtils.js +1 -1
- package/package.json +4 -2
- package/dist/chunk-CJW6K3CL.js.map +0 -1
- package/dist/chunk-EPK6MA7X.js.map +0 -1
- package/dist/chunk-FTIQQK62.js.map +0 -1
- package/dist/chunk-R2WMVXZZ.js.map +0 -1
|
@@ -59,11 +59,11 @@ Iterate all entities in the scene and apply logic only to those of a specific ty
|
|
|
59
59
|
|
|
60
60
|
```typescript
|
|
61
61
|
import { Behaviour, EntityType, TransformComponent } from "@relu-interactives/spatial-ecs";
|
|
62
|
-
import type {
|
|
62
|
+
import type { TextInput, FloatInput } from "@relu-interactives/spatial-ecs";
|
|
63
63
|
|
|
64
64
|
export default class TypeFilter extends Behaviour {
|
|
65
65
|
data = {
|
|
66
|
-
targetKind: "cube" as
|
|
66
|
+
targetKind: "cube" as TextInput,
|
|
67
67
|
spinSpeed: 1 as FloatInput,
|
|
68
68
|
};
|
|
69
69
|
|
|
@@ -70,11 +70,11 @@ Drive the quad's visibility from the inspector or from game logic via `Object3DR
|
|
|
70
70
|
|
|
71
71
|
```typescript
|
|
72
72
|
import { Behaviour, ImageComponent, Object3DRef } from "@relu-interactives/spatial-ecs";
|
|
73
|
-
import type {
|
|
73
|
+
import type { ToggleInput } from "@relu-interactives/spatial-ecs";
|
|
74
74
|
|
|
75
75
|
export default class ImageVisibility extends Behaviour {
|
|
76
76
|
data = {
|
|
77
|
-
visible: true as
|
|
77
|
+
visible: true as ToggleInput,
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
protected onUpdate() {
|
|
@@ -63,12 +63,12 @@ export default class InstanceCounter extends Behaviour {
|
|
|
63
63
|
|
|
64
64
|
```typescript
|
|
65
65
|
import { Behaviour, InstancedMeshComponent } from "@relu-interactives/spatial-ecs";
|
|
66
|
-
import type {
|
|
66
|
+
import type { ToggleInput } from "@relu-interactives/spatial-ecs";
|
|
67
67
|
|
|
68
68
|
export default class ToggleShadows extends Behaviour {
|
|
69
69
|
data = {
|
|
70
|
-
castShadow: true as
|
|
71
|
-
receiveShadow: true as
|
|
70
|
+
castShadow: true as ToggleInput,
|
|
71
|
+
receiveShadow: true as ToggleInput,
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
protected init() {
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Material catalog for an entity. Tracks the live three.js materials (`materials`, `material`) plus serialized authoring state (`values[]`) mirrored from the inspector. Supports `standard`, `basic`, `phong`, and `physical` PBR types with optional texture map slots. Use `setMaterials`, `updateValue`, `setMaterialType`, `setTextureFiltering`, and `setUseMipmaps` to mutate state — these keep `values[]` and the live three.js materials in sync. Texture URLs use the `localasset://` protocol inside the editor.
|
|
4
4
|
|
|
5
|
+
## Shared material assets (editor)
|
|
6
|
+
|
|
7
|
+
The editor can link a material slot to a **material asset** (a `.material` file in the project's asset browser) via the `assetRefs` field — one entry per slot, holding the asset's `localasset://` URL or `null` for a local material. The editor materializes the asset's values into `values[]` and re-applies them to every linked entity whenever the asset is edited. Rendering and the published runtime read only `values[]`; `assetRefs` is editor bookkeeping that persists with the project.
|
|
8
|
+
|
|
5
9
|
> **Managed by:** [`MaterialSyncSystem`](../systems/material-sync-system)
|
|
6
10
|
|
|
7
11
|
## Editor inspector
|
|
@@ -44,12 +44,12 @@ Let the inspector or game logic control whether the mesh casts and receives shad
|
|
|
44
44
|
|
|
45
45
|
```typescript
|
|
46
46
|
import { Behaviour, MeshGeometryComponent } from "@relu-interactives/spatial-ecs";
|
|
47
|
-
import type {
|
|
47
|
+
import type { ToggleInput } from "@relu-interactives/spatial-ecs";
|
|
48
48
|
|
|
49
49
|
export default class ShadowDriver extends Behaviour {
|
|
50
50
|
data = {
|
|
51
|
-
castShadow: true as
|
|
52
|
-
receiveShadow: true as
|
|
51
|
+
castShadow: true as ToggleInput,
|
|
52
|
+
receiveShadow: true as ToggleInput,
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
protected onUpdate() {
|
|
@@ -40,12 +40,12 @@ Walk the mesh hierarchy and enable or disable shadow casting based on a boolean
|
|
|
40
40
|
|
|
41
41
|
```typescript
|
|
42
42
|
import { Behaviour, MeshComponent } from "@relu-interactives/spatial-ecs";
|
|
43
|
-
import type {
|
|
43
|
+
import type { ToggleInput } from "@relu-interactives/spatial-ecs";
|
|
44
44
|
|
|
45
45
|
export default class ShadowToggle extends Behaviour {
|
|
46
46
|
data = {
|
|
47
|
-
castShadow: true as
|
|
48
|
-
receiveShadow: true as
|
|
47
|
+
castShadow: true as ToggleInput,
|
|
48
|
+
receiveShadow: true as ToggleInput,
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
protected onUpdate() {
|
|
@@ -73,11 +73,11 @@ Show or hide the loaded model at runtime using the `Object3DRef` visibility flag
|
|
|
73
73
|
|
|
74
74
|
```typescript
|
|
75
75
|
import { Behaviour, ModelComponent, Object3DRef } from "@relu-interactives/spatial-ecs";
|
|
76
|
-
import type {
|
|
76
|
+
import type { ToggleInput } from "@relu-interactives/spatial-ecs";
|
|
77
77
|
|
|
78
78
|
export default class ModelVisibility extends Behaviour {
|
|
79
79
|
data = {
|
|
80
|
-
visible: true as
|
|
80
|
+
visible: true as ToggleInput,
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
protected onUpdate() {
|
|
@@ -40,11 +40,11 @@ Rename the entity at runtime from an inspector string field. Useful for dynamic
|
|
|
40
40
|
|
|
41
41
|
```typescript
|
|
42
42
|
import { Behaviour, Name } from "@relu-interactives/spatial-ecs";
|
|
43
|
-
import type {
|
|
43
|
+
import type { TextInput } from "@relu-interactives/spatial-ecs";
|
|
44
44
|
|
|
45
45
|
export default class EntityRenamer extends Behaviour {
|
|
46
46
|
data = {
|
|
47
|
-
displayName: "My Entity" as
|
|
47
|
+
displayName: "My Entity" as TextInput,
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
protected onUpdate() {
|
|
@@ -62,11 +62,11 @@ Query all `Name` components to locate a sibling entity and read its position.
|
|
|
62
62
|
|
|
63
63
|
```typescript
|
|
64
64
|
import { Behaviour, Name, TransformComponent } from "@relu-interactives/spatial-ecs";
|
|
65
|
-
import type {
|
|
65
|
+
import type { TextInput } from "@relu-interactives/spatial-ecs";
|
|
66
66
|
|
|
67
67
|
export default class LookAtTarget extends Behaviour {
|
|
68
68
|
data = {
|
|
69
|
-
targetName: "Target" as
|
|
69
|
+
targetName: "Target" as TextInput,
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
protected onUpdate() {
|
|
@@ -39,11 +39,11 @@ Expose a boolean that shows or hides the entity's scene object at runtime.
|
|
|
39
39
|
|
|
40
40
|
```typescript
|
|
41
41
|
import { Behaviour, Object3DRef } from "@relu-interactives/spatial-ecs";
|
|
42
|
-
import type {
|
|
42
|
+
import type { ToggleInput } from "@relu-interactives/spatial-ecs";
|
|
43
43
|
|
|
44
44
|
export default class VisibilityDriver extends Behaviour {
|
|
45
45
|
data = {
|
|
46
|
-
visible: true as
|
|
46
|
+
visible: true as ToggleInput,
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
protected onUpdate() {
|
|
@@ -54,11 +54,11 @@ Assign a new parent entity ID to move the entity (and its children) under a diff
|
|
|
54
54
|
|
|
55
55
|
```typescript
|
|
56
56
|
import { Behaviour, ParentId, Name, KeyboardKeys } from "@relu-interactives/spatial-ecs";
|
|
57
|
-
import type {
|
|
57
|
+
import type { TextInput } from "@relu-interactives/spatial-ecs";
|
|
58
58
|
|
|
59
59
|
export default class Reparenter extends Behaviour {
|
|
60
60
|
data = {
|
|
61
|
-
newParentName: "Holder" as
|
|
61
|
+
newParentName: "Holder" as TextInput,
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
protected onUpdate() {
|
|
@@ -4,6 +4,16 @@ Combined rigid body and collider component backed by Rapier. `values.rigidbody.t
|
|
|
4
4
|
|
|
5
5
|
> **Managed by:** [`PhysicsSyncSystem`](../systems/physics-sync-system)
|
|
6
6
|
|
|
7
|
+
## Collider sizing rules (read before authoring colliders)
|
|
8
|
+
|
|
9
|
+
- **Primitives auto-fit on add** (v0.2.18+): adding a `PhysicsComponent` to a `plane`, `cube`, `sphere`, `capsule`, or `cylinder` derives the collider from the mesh-geometry parameters automatically — a plane gets a thin local-Z slab and a `fixed` body, a cube gets its exact box, and so on. Models and entities without mesh geometry keep the generic 1 m cuboid default and must be sized explicitly. Auto-fit runs only when no explicit values are passed; saved/duplicated components are never altered. If you resize the mesh geometry AFTER adding physics, re-author the collider — it does not track later geometry edits.
|
|
10
|
+
|
|
11
|
+
- `values.collider.size` holds the cuboid's **FULL edge lengths** in meters — NOT half-extents. A 1 m cube is `size: { x: 1, y: 1, z: 1 }`.
|
|
12
|
+
- Collider dimensions (`size`, `radius`, `height`) are authored in **unscaled local units**: `PhysicsSyncSystem` multiplies them by the entity's world scale when building the collider. Author the dimensions of the *unscaled* mesh; scaling the entity keeps mesh and collider matched.
|
|
13
|
+
- `offsetPosition` is **not** multiplied by scale — it is applied in raw units. On a scaled entity, compute the offset in final world units.
|
|
14
|
+
- The collider is axis-aligned to the entity and follows the entity's `TransformComponent` rotation (plus `offsetRotation`). A `plane` primitive is a vertical quad pre-rotated `rotation.x = -π/2` to lie flat — keep that rotation, and put a plane floor's thin cuboid axis on **local Z** (geometry is 100×100 → `size: { x: 100, y: 100, z: 0.05 }`). A flattened `cube` is usually a more robust ground.
|
|
15
|
+
- Model (GLB) colliders are never auto-fitted from geometry — measure the mesh bounds and author dimensions explicitly, or use `trimesh` for complex static geometry.
|
|
16
|
+
|
|
7
17
|
## Editor inspector
|
|
8
18
|
|
|
9
19
|

|
|
@@ -73,12 +73,12 @@ Expose volume and loop settings as inspector fields so they can be tuned without
|
|
|
73
73
|
|
|
74
74
|
```typescript
|
|
75
75
|
import { Behaviour, VideoComponent } from "@relu-interactives/spatial-ecs";
|
|
76
|
-
import type { FloatInput,
|
|
76
|
+
import type { FloatInput, ToggleInput } from "@relu-interactives/spatial-ecs";
|
|
77
77
|
|
|
78
78
|
export default class VideoSettings extends Behaviour {
|
|
79
79
|
data = {
|
|
80
80
|
volume: 1 as FloatInput,
|
|
81
|
-
loop: true as
|
|
81
|
+
loop: true as ToggleInput,
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
protected onUpdate() {
|
|
@@ -46,7 +46,7 @@ Folders are real directories under `assets/`. You can:
|
|
|
46
46
|
|
|
47
47
|
## Scripts
|
|
48
48
|
|
|
49
|
-
Right-click in the asset browser and pick **New
|
|
49
|
+
Right-click in the asset browser and pick **New Script** to create a `Behaviour` script in place. The editor compiles it and exposes it for attachment via the inspector's **Script** panel. See [Custom Behaviours](../../authoring-a-behaviour).
|
|
50
50
|
|
|
51
51
|
## Related
|
|
52
52
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Prefabs
|
|
2
|
+
|
|
3
|
+
A prefab packages a fully configured entity — its children, components,
|
|
4
|
+
materials, physics, animations, and scripts — into a reusable `.prefab`
|
|
5
|
+
asset. Configure something once (a third-person character, a pickup, an
|
|
6
|
+
enemy), save it as a prefab, then reuse it everywhere: drag copies into the
|
|
7
|
+
scene, or spawn them from scripts at runtime.
|
|
8
|
+
|
|
9
|
+
## File anatomy
|
|
10
|
+
|
|
11
|
+
A `.prefab` file is JSON:
|
|
12
|
+
|
|
13
|
+
```jsonc
|
|
14
|
+
{
|
|
15
|
+
"version": 1,
|
|
16
|
+
"name": "Enemy",
|
|
17
|
+
"snapshot": {
|
|
18
|
+
"entities": [1, 2, 3], // ids renumbered 1..N, root first
|
|
19
|
+
"components": [ // same columnar shape as a world snapshot
|
|
20
|
+
{ "component": "PrefabNode", "entries": [ /* stable per-node uuids */ ] },
|
|
21
|
+
{ "component": "TransformComponent", "entries": [ /* … */ ] }
|
|
22
|
+
// …every other serialized component
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- Every entity carries a `PrefabNode { nodeId }` — a stable uuid used to
|
|
29
|
+
match instance entities back to the file across sessions and machines.
|
|
30
|
+
- The root's `ParentId` is always `null`; entity references inside the
|
|
31
|
+
subtree (script `EntityInput`/`ComponentInput` fields, instanced-mesh
|
|
32
|
+
slots) are remapped on instantiation. References that pointed *outside*
|
|
33
|
+
the subtree are nulled when the prefab is created.
|
|
34
|
+
- Scene singletons (`Environment`, `Postprocessing`) never serialize into a
|
|
35
|
+
prefab. Cameras and image targets cannot be turned into prefabs.
|
|
36
|
+
|
|
37
|
+
## Creating and editing (editor)
|
|
38
|
+
|
|
39
|
+
- **Create**: drag an entity from the Hierarchy into the Asset Browser. The
|
|
40
|
+
dragged entity automatically becomes the first linked *instance*.
|
|
41
|
+
- **Edit**: double-click the `.prefab` tile — the editor enters **Prefab
|
|
42
|
+
Mode**, showing only the prefab's contents with the full inspector.
|
|
43
|
+
Saving writes the file; closing returns to the scene and applies the
|
|
44
|
+
changes to every instance.
|
|
45
|
+
- **Instantiate**: drag the `.prefab` from the Asset Browser into the
|
|
46
|
+
viewport or onto a Hierarchy row.
|
|
47
|
+
|
|
48
|
+
## Instances and per-property overrides
|
|
49
|
+
|
|
50
|
+
Scene instances stay fully editable. Any property you change on an instance
|
|
51
|
+
(a material color, a script field, a child's transform) becomes an
|
|
52
|
+
**override**: when the prefab asset is edited later, overridden properties
|
|
53
|
+
keep their per-instance values while everything else syncs from the file.
|
|
54
|
+
|
|
55
|
+
Always per-instance (never synced): the instance **root's** position /
|
|
56
|
+
rotation / scale, name, and place in the scene hierarchy.
|
|
57
|
+
|
|
58
|
+
v1 rules of thumb:
|
|
59
|
+
|
|
60
|
+
- Components or children you *add* to an instance are yours — prefab
|
|
61
|
+
updates never remove them.
|
|
62
|
+
- Components or children you *delete* from an instance come back on the
|
|
63
|
+
next prefab apply (deletion tracking is not part of v1).
|
|
64
|
+
- "Unpack" in the inspector detaches an instance permanently — it becomes
|
|
65
|
+
ordinary objects with no link.
|
|
66
|
+
- Nested prefabs (a prefab inside another prefab) are not supported in v1.
|
|
67
|
+
|
|
68
|
+
## Spawning at runtime
|
|
69
|
+
|
|
70
|
+
Scripts spawn prefabs with `this.spawnPrefab(source, options?)`. Declare an
|
|
71
|
+
`AssetInput` field and drag the `.prefab` onto it in the editor:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { Behaviour } from "@relu-interactives/spatial-ecs";
|
|
75
|
+
|
|
76
|
+
export default class EnemySpawner extends Behaviour {
|
|
77
|
+
data = {
|
|
78
|
+
// AssetInput — drag Enemy.prefab onto this field in the inspector
|
|
79
|
+
enemyPrefab: { path: "", type: "prefab" },
|
|
80
|
+
spawned: [] as number[],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
private async spawnWave() {
|
|
84
|
+
for (let i = 0; i < 3; i++) {
|
|
85
|
+
const rootId = await this.spawnPrefab(this.data.enemyPrefab, {
|
|
86
|
+
position: { x: i * 2 - 2, y: 0, z: -6 },
|
|
87
|
+
});
|
|
88
|
+
this.data.spawned.push(rootId);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private despawnWave() {
|
|
93
|
+
for (const id of this.data.spawned) {
|
|
94
|
+
this.despawn(id); // removes the root and all of its children
|
|
95
|
+
}
|
|
96
|
+
this.data.spawned = [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Details:
|
|
102
|
+
|
|
103
|
+
- `spawnPrefab` resolves once the entities exist. Heavy assets inside
|
|
104
|
+
(models, textures, audio) stream in reactively — repeated spawns of the
|
|
105
|
+
same prefab share geometry through the model cache, so waves are cheap.
|
|
106
|
+
- `source` can be an `AssetInput` (`{ path, type }`), a URL string, or a
|
|
107
|
+
pre-loaded `PrefabAsset`.
|
|
108
|
+
- Options: `{ parentId, position, rotation, scale }` — the transform applies
|
|
109
|
+
to the spawned root in its parent's local space.
|
|
110
|
+
- `world.spawnPrefab(...)` / `world.despawnPrefab(id)` expose the same calls
|
|
111
|
+
outside of behaviours.
|
|
112
|
+
|
|
113
|
+
## Preloading and caching
|
|
114
|
+
|
|
115
|
+
Each prefab URL is fetched and parsed once per session (`loadPrefab` caches
|
|
116
|
+
the parsed file; failed loads retry). Published experiences preload prefab
|
|
117
|
+
files referenced by script `AssetInput` fields — plus the models and
|
|
118
|
+
textures inside them — before the experience starts, so the first spawn
|
|
119
|
+
does not hitch.
|
|
120
|
+
|
|
121
|
+
## Custom hosts
|
|
122
|
+
|
|
123
|
+
Prefab files store editor-space asset URLs. Published and preview builds
|
|
124
|
+
register a resolver that maps them onto the deployed `assets/` folder:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { setPrefabAssetUrlResolver } from "@relu-interactives/spatial-ecs";
|
|
128
|
+
|
|
129
|
+
setPrefabAssetUrlResolver((url) => rewriteToMyCdn(url));
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The standard project templates already do this — you only need it when
|
|
133
|
+
embedding the runtime in a custom host.
|
|
@@ -283,7 +283,8 @@ intersected mesh sorted by distance. Each hit carries the owning `entityId`
|
|
|
283
283
|
so you can drive game logic from the result.
|
|
284
284
|
|
|
285
285
|
```ts
|
|
286
|
-
import { Behaviour
|
|
286
|
+
import { Behaviour } from "@relu-interactives/spatial-ecs";
|
|
287
|
+
import { Name } from "@relu-interactives/spatial-ecs/components";
|
|
287
288
|
import * as THREE from "three";
|
|
288
289
|
|
|
289
290
|
export default class ClickToInspect extends Behaviour {
|
|
@@ -299,7 +300,7 @@ export default class ClickToInspect extends Behaviour {
|
|
|
299
300
|
const first = hits.find((hit) => hit.entityId !== null);
|
|
300
301
|
if (!first || first.entityId === null) return;
|
|
301
302
|
|
|
302
|
-
const name = this.world.getComponent(first.entityId,
|
|
303
|
+
const name = this.world.getComponent(first.entityId, Name);
|
|
303
304
|
console.log(
|
|
304
305
|
"clicked",
|
|
305
306
|
name?.value ?? first.entityId,
|
|
@@ -429,7 +430,7 @@ export default class PauseOnHit extends Behaviour {
|
|
|
429
430
|
> [!TIP]
|
|
430
431
|
> `pause()` and `resume()` affect the **entire world**. If you only want to
|
|
431
432
|
> stop a subset of entities (for example, freeze enemies while a menu is open)
|
|
432
|
-
> use a flag
|
|
433
|
+
> use a flag in a custom script (`Behaviour`) instead and guard your `onUpdate` logic.
|
|
433
434
|
|
|
434
435
|
## See also
|
|
435
436
|
|
package/dist/factories/index.cjs
CHANGED
|
@@ -929,10 +929,130 @@ var _PhysicsComponent = class _PhysicsComponent extends Component {
|
|
|
929
929
|
};
|
|
930
930
|
__name(_PhysicsComponent, "PhysicsComponent");
|
|
931
931
|
var PhysicsComponent = _PhysicsComponent;
|
|
932
|
+
var DEFAULT_MESH_GEOMETRY_PARAMETERS = {
|
|
933
|
+
capsule: { radius: 0.5, height: 1 },
|
|
934
|
+
cube: { width: 1, height: 1, depth: 1 },
|
|
935
|
+
cylinder: { radius: 0.5, height: 1 },
|
|
936
|
+
plane: { width: 100, height: 100 },
|
|
937
|
+
sphere: { radius: 0.5 }
|
|
938
|
+
};
|
|
939
|
+
var GEOMETRY_TYPE_BY_KIND = {
|
|
940
|
+
capsule: "CapsuleGeometry",
|
|
941
|
+
cube: "BoxGeometry",
|
|
942
|
+
cylinder: "CylinderGeometry",
|
|
943
|
+
model: "Model",
|
|
944
|
+
plane: "PlaneGeometry",
|
|
945
|
+
sphere: "SphereGeometry"
|
|
946
|
+
};
|
|
947
|
+
var _MeshGeometryComponent = class _MeshGeometryComponent extends Component {
|
|
948
|
+
constructor(kind, object, parameters, options) {
|
|
949
|
+
super();
|
|
950
|
+
this.kind = kind;
|
|
951
|
+
this.geometryType = GEOMETRY_TYPE_BY_KIND[kind];
|
|
952
|
+
this.object = object;
|
|
953
|
+
this.modelName = options?.modelName ?? "";
|
|
954
|
+
this.castShadow = options?.castShadow ?? false;
|
|
955
|
+
this.receiveShadow = options?.receiveShadow ?? false;
|
|
956
|
+
switch (kind) {
|
|
957
|
+
case "cube":
|
|
958
|
+
this.parameters = {
|
|
959
|
+
...DEFAULT_MESH_GEOMETRY_PARAMETERS.cube,
|
|
960
|
+
...parameters
|
|
961
|
+
};
|
|
962
|
+
break;
|
|
963
|
+
case "plane":
|
|
964
|
+
this.parameters = {
|
|
965
|
+
...DEFAULT_MESH_GEOMETRY_PARAMETERS.plane,
|
|
966
|
+
...parameters
|
|
967
|
+
};
|
|
968
|
+
break;
|
|
969
|
+
case "sphere":
|
|
970
|
+
this.parameters = {
|
|
971
|
+
...DEFAULT_MESH_GEOMETRY_PARAMETERS.sphere,
|
|
972
|
+
...parameters
|
|
973
|
+
};
|
|
974
|
+
break;
|
|
975
|
+
case "cylinder":
|
|
976
|
+
this.parameters = {
|
|
977
|
+
...DEFAULT_MESH_GEOMETRY_PARAMETERS.cylinder,
|
|
978
|
+
...parameters
|
|
979
|
+
};
|
|
980
|
+
break;
|
|
981
|
+
case "capsule":
|
|
982
|
+
this.parameters = {
|
|
983
|
+
...DEFAULT_MESH_GEOMETRY_PARAMETERS.capsule,
|
|
984
|
+
...parameters
|
|
985
|
+
};
|
|
986
|
+
break;
|
|
987
|
+
case "model":
|
|
988
|
+
this.parameters = {};
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
__name(_MeshGeometryComponent, "MeshGeometryComponent");
|
|
994
|
+
var MeshGeometryComponent = _MeshGeometryComponent;
|
|
932
995
|
|
|
933
996
|
// core/factories/components/PhysicsFactory.ts
|
|
997
|
+
function deriveColliderFromGeometry(world, entityId) {
|
|
998
|
+
const meshGeometry = world.getComponent(entityId, MeshGeometryComponent);
|
|
999
|
+
if (!meshGeometry) {
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
switch (meshGeometry.kind) {
|
|
1003
|
+
case "plane": {
|
|
1004
|
+
const params = meshGeometry.parameters;
|
|
1005
|
+
return {
|
|
1006
|
+
collider: {
|
|
1007
|
+
shape: "cuboid",
|
|
1008
|
+
size: { x: params.width, y: params.height, z: 0.05 }
|
|
1009
|
+
},
|
|
1010
|
+
// An (effectively) zero-thickness sheet is only physically meaningful
|
|
1011
|
+
// as static geometry — planes are floors/walls, not dynamic bodies.
|
|
1012
|
+
rigidbody: { type: "fixed" }
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
case "cube": {
|
|
1016
|
+
const params = meshGeometry.parameters;
|
|
1017
|
+
return {
|
|
1018
|
+
collider: {
|
|
1019
|
+
shape: "cuboid",
|
|
1020
|
+
size: { x: params.width, y: params.height, z: params.depth }
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
case "sphere": {
|
|
1025
|
+
const params = meshGeometry.parameters;
|
|
1026
|
+
return { collider: { shape: "ball", radius: params.radius } };
|
|
1027
|
+
}
|
|
1028
|
+
case "capsule": {
|
|
1029
|
+
const params = meshGeometry.parameters;
|
|
1030
|
+
return {
|
|
1031
|
+
collider: {
|
|
1032
|
+
shape: "capsule",
|
|
1033
|
+
radius: params.radius,
|
|
1034
|
+
height: params.height
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
case "cylinder": {
|
|
1039
|
+
const params = meshGeometry.parameters;
|
|
1040
|
+
return {
|
|
1041
|
+
collider: {
|
|
1042
|
+
shape: "cylinder",
|
|
1043
|
+
radius: params.radius,
|
|
1044
|
+
height: params.height
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
default:
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
__name(deriveColliderFromGeometry, "deriveColliderFromGeometry");
|
|
934
1053
|
function createPhysicsComponent(world, entityId, values) {
|
|
935
|
-
const
|
|
1054
|
+
const initial = values ?? deriveColliderFromGeometry(world, entityId) ?? void 0;
|
|
1055
|
+
const component = new PhysicsComponent(initial);
|
|
936
1056
|
world.addComponent(entityId, component);
|
|
937
1057
|
return component;
|
|
938
1058
|
}
|
|
@@ -1286,8 +1406,23 @@ var _MaterialComponent = class _MaterialComponent extends Component {
|
|
|
1286
1406
|
this.materials = [];
|
|
1287
1407
|
this.material = null;
|
|
1288
1408
|
this.values = [];
|
|
1409
|
+
this.assetRefs = [];
|
|
1289
1410
|
this.setMaterials(materialOrMaterials);
|
|
1290
1411
|
}
|
|
1412
|
+
/** Link/unlink a slot to a material asset URL (editor bookkeeping only). */
|
|
1413
|
+
setAssetRef(index, url) {
|
|
1414
|
+
if (index < 0) {
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
while (this.assetRefs.length <= index) {
|
|
1418
|
+
this.assetRefs.push(null);
|
|
1419
|
+
}
|
|
1420
|
+
this.assetRefs[index] = url;
|
|
1421
|
+
}
|
|
1422
|
+
/** The material-asset URL a slot is linked to, or null when local. */
|
|
1423
|
+
getAssetRef(index) {
|
|
1424
|
+
return this.assetRefs[index] ?? null;
|
|
1425
|
+
}
|
|
1291
1426
|
setMaterials(materialOrMaterials = []) {
|
|
1292
1427
|
this.materials = Array.isArray(materialOrMaterials) ? materialOrMaterials.filter(Boolean) : materialOrMaterials ? [materialOrMaterials] : [];
|
|
1293
1428
|
this.material = this.materials[0] ?? null;
|
|
@@ -1356,69 +1491,6 @@ function createMeshComponent(world, entityId, ...args) {
|
|
|
1356
1491
|
return component;
|
|
1357
1492
|
}
|
|
1358
1493
|
__name(createMeshComponent, "createMeshComponent");
|
|
1359
|
-
var DEFAULT_MESH_GEOMETRY_PARAMETERS = {
|
|
1360
|
-
capsule: { radius: 0.5, height: 1 },
|
|
1361
|
-
cube: { width: 1, height: 1, depth: 1 },
|
|
1362
|
-
cylinder: { radius: 0.5, height: 1 },
|
|
1363
|
-
plane: { width: 100, height: 100 },
|
|
1364
|
-
sphere: { radius: 0.5 }
|
|
1365
|
-
};
|
|
1366
|
-
var GEOMETRY_TYPE_BY_KIND = {
|
|
1367
|
-
capsule: "CapsuleGeometry",
|
|
1368
|
-
cube: "BoxGeometry",
|
|
1369
|
-
cylinder: "CylinderGeometry",
|
|
1370
|
-
model: "Model",
|
|
1371
|
-
plane: "PlaneGeometry",
|
|
1372
|
-
sphere: "SphereGeometry"
|
|
1373
|
-
};
|
|
1374
|
-
var _MeshGeometryComponent = class _MeshGeometryComponent extends Component {
|
|
1375
|
-
constructor(kind, object, parameters, options) {
|
|
1376
|
-
super();
|
|
1377
|
-
this.kind = kind;
|
|
1378
|
-
this.geometryType = GEOMETRY_TYPE_BY_KIND[kind];
|
|
1379
|
-
this.object = object;
|
|
1380
|
-
this.modelName = options?.modelName ?? "";
|
|
1381
|
-
this.castShadow = options?.castShadow ?? false;
|
|
1382
|
-
this.receiveShadow = options?.receiveShadow ?? false;
|
|
1383
|
-
switch (kind) {
|
|
1384
|
-
case "cube":
|
|
1385
|
-
this.parameters = {
|
|
1386
|
-
...DEFAULT_MESH_GEOMETRY_PARAMETERS.cube,
|
|
1387
|
-
...parameters
|
|
1388
|
-
};
|
|
1389
|
-
break;
|
|
1390
|
-
case "plane":
|
|
1391
|
-
this.parameters = {
|
|
1392
|
-
...DEFAULT_MESH_GEOMETRY_PARAMETERS.plane,
|
|
1393
|
-
...parameters
|
|
1394
|
-
};
|
|
1395
|
-
break;
|
|
1396
|
-
case "sphere":
|
|
1397
|
-
this.parameters = {
|
|
1398
|
-
...DEFAULT_MESH_GEOMETRY_PARAMETERS.sphere,
|
|
1399
|
-
...parameters
|
|
1400
|
-
};
|
|
1401
|
-
break;
|
|
1402
|
-
case "cylinder":
|
|
1403
|
-
this.parameters = {
|
|
1404
|
-
...DEFAULT_MESH_GEOMETRY_PARAMETERS.cylinder,
|
|
1405
|
-
...parameters
|
|
1406
|
-
};
|
|
1407
|
-
break;
|
|
1408
|
-
case "capsule":
|
|
1409
|
-
this.parameters = {
|
|
1410
|
-
...DEFAULT_MESH_GEOMETRY_PARAMETERS.capsule,
|
|
1411
|
-
...parameters
|
|
1412
|
-
};
|
|
1413
|
-
break;
|
|
1414
|
-
case "model":
|
|
1415
|
-
this.parameters = {};
|
|
1416
|
-
break;
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
};
|
|
1420
|
-
__name(_MeshGeometryComponent, "MeshGeometryComponent");
|
|
1421
|
-
var MeshGeometryComponent = _MeshGeometryComponent;
|
|
1422
1494
|
|
|
1423
1495
|
// core/factories/components/MeshGeometryFactory.ts
|
|
1424
1496
|
function createMeshGeometryComponent(world, entityId, ...args) {
|