@relu-interactives/spatial-ecs 0.2.17 → 0.2.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.
Files changed (54) hide show
  1. package/dist/{System-BeuJ1xt5.d.cts → System-me3iAcMF.d.cts} +150 -1
  2. package/dist/{System-BeuJ1xt5.d.ts → System-me3iAcMF.d.ts} +150 -1
  3. package/dist/{Video-Cym2C52L.d.ts → Video-DHnxjGhW.d.ts} +17 -3
  4. package/dist/{Video-D4B-z74P.d.cts → Video-U9hAnf9y.d.cts} +17 -3
  5. package/dist/{chunk-EPK6MA7X.js → chunk-5U3YS4JD.js} +1809 -8
  6. package/dist/chunk-5U3YS4JD.js.map +1 -0
  7. package/dist/{chunk-GRUK5TNL.js → chunk-AXRCGF63.js} +3 -3
  8. package/dist/{chunk-GRUK5TNL.js.map → chunk-AXRCGF63.js.map} +1 -1
  9. package/dist/{chunk-CJW6K3CL.js → chunk-K6KMKKJ6.js} +62 -5
  10. package/dist/chunk-K6KMKKJ6.js.map +1 -0
  11. package/dist/{chunk-FTIQQK62.js → chunk-V7BNVTJW.js} +50 -3
  12. package/dist/chunk-V7BNVTJW.js.map +1 -0
  13. package/dist/{chunk-R2WMVXZZ.js → chunk-ZXGHKX7N.js} +5 -3
  14. package/dist/chunk-ZXGHKX7N.js.map +1 -0
  15. package/dist/components/index.cjs +47 -0
  16. package/dist/components/index.cjs.map +1 -1
  17. package/dist/components/index.d.cts +3 -2
  18. package/dist/components/index.d.ts +3 -2
  19. package/dist/components/index.js +1 -1
  20. package/dist/docs/guide/components/material.md +4 -0
  21. package/dist/docs/guide/components/physics.md +10 -0
  22. package/dist/docs/guide/getting-started/editor/assets-browser.md +1 -1
  23. package/dist/docs/guide/prefabs.md +152 -0
  24. package/dist/docs/guide/world-api.md +1 -1
  25. package/dist/factories/index.cjs +136 -64
  26. package/dist/factories/index.cjs.map +1 -1
  27. package/dist/factories/index.d.cts +5 -4
  28. package/dist/factories/index.d.ts +5 -4
  29. package/dist/factories/index.js +3 -3
  30. package/dist/{index-BfoOC2CG.d.cts → index-CZo5I5Gs.d.cts} +76 -4
  31. package/dist/{index-BoJ3_6a3.d.ts → index-D7FSg0Au.d.ts} +76 -4
  32. package/dist/index.cjs +6386 -4962
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +313 -7
  35. package/dist/index.d.ts +313 -7
  36. package/dist/index.js +407 -926
  37. package/dist/index.js.map +1 -1
  38. package/dist/systems/index.cjs +1386 -0
  39. package/dist/systems/index.cjs.map +1 -1
  40. package/dist/systems/index.d.cts +2 -2
  41. package/dist/systems/index.d.ts +2 -2
  42. package/dist/systems/index.js +3 -3
  43. package/dist/types.d.cts +1 -1
  44. package/dist/types.d.ts +1 -1
  45. package/dist/worldSnapshotUtils.cjs +3 -1
  46. package/dist/worldSnapshotUtils.cjs.map +1 -1
  47. package/dist/worldSnapshotUtils.d.cts +1 -1
  48. package/dist/worldSnapshotUtils.d.ts +1 -1
  49. package/dist/worldSnapshotUtils.js +1 -1
  50. package/package.json +4 -2
  51. package/dist/chunk-CJW6K3CL.js.map +0 -1
  52. package/dist/chunk-EPK6MA7X.js.map +0 -1
  53. package/dist/chunk-FTIQQK62.js.map +0 -1
  54. package/dist/chunk-R2WMVXZZ.js.map +0 -1
@@ -1,4 +1,5 @@
1
- export { A as AnimationComponent, a as AnimationComponentOptions, b as AudioComponent, C as CameraComponent, c as CameraComponentOptions, d as CameraProjection, e as CameraRuntimeType, f as ColliderShape, g as ColliderState, h as CubeMeshParameters, D as DEFAULT_COLLIDER_STATE, i as DEFAULT_INSTANCED_MESH_PARAMETERS, j as DEFAULT_INSTANCE_TRANSFORM, k as DEFAULT_MESH_GEOMETRY_PARAMETERS, l as DEFAULT_PHYSICS_STATE, m as DEFAULT_RIGIDBODY_STATE, E as EditableMeshObjectKind, n as EntityType, I as ImageComponent, o as ImageTarget, p as ImageTargetAnchor, q as ImageTargetAnchorOptions, r as ImageTargetReference, s as InstanceSlotComponent, t as InstanceTransform, u as InstancedMeshComponent, v as InstancedMeshKind, L as LightComponent, w as LightValueState, M as MaterialComponent, x as MaterialTypeValue, y as MaterialValueState, z as MeshComponent, B as MeshGeometryComponent, F as MeshGeometryParameters, G as MeshGeometryParametersByKind, H as ModelComponent, N as Name, O as Object3DRef, P as PhysicsComponent, J as PhysicsState, K as PlaneMeshParameters, R as RadiusHeightMeshParameters, Q as RadiusMeshParameters, S as RigidbodyState, T as RigidbodyType, U as ScriptComponent, V as ScriptEntry, W as Selectable, X as SelectionTransformState, Y as SpriteComponent, Z as TextureFilteringValue, _ as TransformModeValue, $ as TransformSpaceValue, a0 as VideoComponent, a1 as normalizePhysicsState } from '../Video-D4B-z74P.cjs';
2
- export { D as DEFAULT_ENVIRONMENT_STATE, a as DEFAULT_POSTPROCESSING_STATE, E as Environment, b as EnvironmentFogState, c as EnvironmentFogType, d as EnvironmentGradientEffect, e as EnvironmentSkyboxState, f as EnvironmentSkyboxType, g as EnvironmentState, P as ParentId, h as Postprocessing, i as PostprocessingState, T as TransformComponent, j as TransformData, V as Vec3 } from '../System-BeuJ1xt5.cjs';
1
+ export { A as AnimationComponent, a as AnimationComponentOptions, b as AudioComponent, C as CameraComponent, c as CameraComponentOptions, d as CameraProjection, e as CameraRuntimeType, f as ColliderShape, g as ColliderState, h as CubeMeshParameters, D as DEFAULT_COLLIDER_STATE, i as DEFAULT_INSTANCED_MESH_PARAMETERS, j as DEFAULT_INSTANCE_TRANSFORM, k as DEFAULT_MESH_GEOMETRY_PARAMETERS, l as DEFAULT_PHYSICS_STATE, m as DEFAULT_RIGIDBODY_STATE, E as EditableMeshObjectKind, n as EntityType, I as ImageComponent, o as ImageTarget, p as ImageTargetAnchor, q as ImageTargetAnchorOptions, r as ImageTargetReference, s as InstanceSlotComponent, t as InstanceTransform, u as InstancedMeshComponent, v as InstancedMeshKind, L as LightComponent, w as LightValueState, M as MaterialComponent, x as MaterialTypeValue, y as MaterialValueState, z as MeshComponent, B as MeshGeometryComponent, F as MeshGeometryParameters, G as MeshGeometryParametersByKind, H as ModelComponent, N as Name, O as Object3DRef, P as PhysicsComponent, J as PhysicsState, K as PlaneMeshParameters, R as RadiusHeightMeshParameters, Q as RadiusMeshParameters, S as RigidbodyState, T as RigidbodyType, U as ScriptComponent, V as ScriptEntry, W as Selectable, X as SelectionTransformState, Y as SpriteComponent, Z as TextureFilteringValue, _ as TransformModeValue, $ as TransformSpaceValue, a0 as VideoComponent, a1 as defaultMaterialValue, a2 as normalizePhysicsState } from '../Video-U9hAnf9y.cjs';
2
+ export { D as DEFAULT_ENVIRONMENT_STATE, a as DEFAULT_POSTPROCESSING_STATE, E as Environment, b as EnvironmentFogState, c as EnvironmentFogType, d as EnvironmentGradientEffect, e as EnvironmentSkyboxState, f as EnvironmentSkyboxType, g as EnvironmentState, P as ParentId, h as Postprocessing, i as PostprocessingState, T as TransformComponent, j as TransformData, V as Vec3 } from '../System-me3iAcMF.cjs';
3
+ export { P as PrefabInstance, a as PrefabNode } from '../index-CZo5I5Gs.cjs';
3
4
  import 'three';
4
5
  import '@dimforge/rapier3d-compat';
@@ -1,4 +1,5 @@
1
- export { A as AnimationComponent, a as AnimationComponentOptions, b as AudioComponent, C as CameraComponent, c as CameraComponentOptions, d as CameraProjection, e as CameraRuntimeType, f as ColliderShape, g as ColliderState, h as CubeMeshParameters, D as DEFAULT_COLLIDER_STATE, i as DEFAULT_INSTANCED_MESH_PARAMETERS, j as DEFAULT_INSTANCE_TRANSFORM, k as DEFAULT_MESH_GEOMETRY_PARAMETERS, l as DEFAULT_PHYSICS_STATE, m as DEFAULT_RIGIDBODY_STATE, E as EditableMeshObjectKind, n as EntityType, I as ImageComponent, o as ImageTarget, p as ImageTargetAnchor, q as ImageTargetAnchorOptions, r as ImageTargetReference, s as InstanceSlotComponent, t as InstanceTransform, u as InstancedMeshComponent, v as InstancedMeshKind, L as LightComponent, w as LightValueState, M as MaterialComponent, x as MaterialTypeValue, y as MaterialValueState, z as MeshComponent, B as MeshGeometryComponent, F as MeshGeometryParameters, G as MeshGeometryParametersByKind, H as ModelComponent, N as Name, O as Object3DRef, P as PhysicsComponent, J as PhysicsState, K as PlaneMeshParameters, R as RadiusHeightMeshParameters, Q as RadiusMeshParameters, S as RigidbodyState, T as RigidbodyType, U as ScriptComponent, V as ScriptEntry, W as Selectable, X as SelectionTransformState, Y as SpriteComponent, Z as TextureFilteringValue, _ as TransformModeValue, $ as TransformSpaceValue, a0 as VideoComponent, a1 as normalizePhysicsState } from '../Video-Cym2C52L.js';
2
- export { D as DEFAULT_ENVIRONMENT_STATE, a as DEFAULT_POSTPROCESSING_STATE, E as Environment, b as EnvironmentFogState, c as EnvironmentFogType, d as EnvironmentGradientEffect, e as EnvironmentSkyboxState, f as EnvironmentSkyboxType, g as EnvironmentState, P as ParentId, h as Postprocessing, i as PostprocessingState, T as TransformComponent, j as TransformData, V as Vec3 } from '../System-BeuJ1xt5.js';
1
+ export { A as AnimationComponent, a as AnimationComponentOptions, b as AudioComponent, C as CameraComponent, c as CameraComponentOptions, d as CameraProjection, e as CameraRuntimeType, f as ColliderShape, g as ColliderState, h as CubeMeshParameters, D as DEFAULT_COLLIDER_STATE, i as DEFAULT_INSTANCED_MESH_PARAMETERS, j as DEFAULT_INSTANCE_TRANSFORM, k as DEFAULT_MESH_GEOMETRY_PARAMETERS, l as DEFAULT_PHYSICS_STATE, m as DEFAULT_RIGIDBODY_STATE, E as EditableMeshObjectKind, n as EntityType, I as ImageComponent, o as ImageTarget, p as ImageTargetAnchor, q as ImageTargetAnchorOptions, r as ImageTargetReference, s as InstanceSlotComponent, t as InstanceTransform, u as InstancedMeshComponent, v as InstancedMeshKind, L as LightComponent, w as LightValueState, M as MaterialComponent, x as MaterialTypeValue, y as MaterialValueState, z as MeshComponent, B as MeshGeometryComponent, F as MeshGeometryParameters, G as MeshGeometryParametersByKind, H as ModelComponent, N as Name, O as Object3DRef, P as PhysicsComponent, J as PhysicsState, K as PlaneMeshParameters, R as RadiusHeightMeshParameters, Q as RadiusMeshParameters, S as RigidbodyState, T as RigidbodyType, U as ScriptComponent, V as ScriptEntry, W as Selectable, X as SelectionTransformState, Y as SpriteComponent, Z as TextureFilteringValue, _ as TransformModeValue, $ as TransformSpaceValue, a0 as VideoComponent, a1 as defaultMaterialValue, a2 as normalizePhysicsState } from '../Video-DHnxjGhW.js';
2
+ export { D as DEFAULT_ENVIRONMENT_STATE, a as DEFAULT_POSTPROCESSING_STATE, E as Environment, b as EnvironmentFogState, c as EnvironmentFogType, d as EnvironmentGradientEffect, e as EnvironmentSkyboxState, f as EnvironmentSkyboxType, g as EnvironmentState, P as ParentId, h as Postprocessing, i as PostprocessingState, T as TransformComponent, j as TransformData, V as Vec3 } from '../System-me3iAcMF.js';
3
+ export { P as PrefabInstance, a as PrefabNode } from '../index-D7FSg0Au.js';
3
4
  import 'three';
4
5
  import '@dimforge/rapier3d-compat';
@@ -1,4 +1,4 @@
1
- export { AnimationComponent, AudioComponent, CameraComponent, DEFAULT_COLLIDER_STATE, DEFAULT_ENVIRONMENT_STATE, DEFAULT_INSTANCED_MESH_PARAMETERS, DEFAULT_INSTANCE_TRANSFORM, DEFAULT_MESH_GEOMETRY_PARAMETERS, DEFAULT_PHYSICS_STATE, DEFAULT_POSTPROCESSING_STATE, DEFAULT_RIGIDBODY_STATE, EntityType, Environment, ImageComponent, ImageTarget, ImageTargetAnchor, InstanceSlotComponent, InstancedMeshComponent, LightComponent, MaterialComponent, MeshComponent, MeshGeometryComponent, ModelComponent, Name, Object3DRef, ParentId, PhysicsComponent, Postprocessing, ScriptComponent, Selectable, SelectionTransformState, SpriteComponent, TransformComponent, VideoComponent, normalizePhysicsState } from '../chunk-FTIQQK62.js';
1
+ export { AnimationComponent, AudioComponent, CameraComponent, DEFAULT_COLLIDER_STATE, DEFAULT_ENVIRONMENT_STATE, DEFAULT_INSTANCED_MESH_PARAMETERS, DEFAULT_INSTANCE_TRANSFORM, DEFAULT_MESH_GEOMETRY_PARAMETERS, DEFAULT_PHYSICS_STATE, DEFAULT_POSTPROCESSING_STATE, DEFAULT_RIGIDBODY_STATE, EntityType, Environment, ImageComponent, ImageTarget, ImageTargetAnchor, InstanceSlotComponent, InstancedMeshComponent, LightComponent, MaterialComponent, MeshComponent, MeshGeometryComponent, ModelComponent, Name, Object3DRef, ParentId, PhysicsComponent, Postprocessing, PrefabInstance, PrefabNode, ScriptComponent, Selectable, SelectionTransformState, SpriteComponent, TransformComponent, VideoComponent, defaultMaterialValue, normalizePhysicsState } from '../chunk-V7BNVTJW.js';
2
2
  import '../chunk-OY7ZODZP.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
@@ -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
@@ -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
  ![Physics inspector](/images/components/Physics.png)
@@ -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 Component** 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).
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,152 @@
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
+ 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 supported).
64
+ - "Unpack" in the inspector detaches ONE level — the objects lose their
65
+ link but any nested instances inside survive as standalone instances.
66
+
67
+ ## Nested prefabs
68
+
69
+ Prefabs can contain other prefab instances: drag a prefab into an open
70
+ prefab (Prefab Mode), or create a prefab from a subtree that contains whole
71
+ instances. Node identity is scoped per placement, so a prefab may nest two
72
+ copies of the same child prefab and each keeps independent overrides.
73
+
74
+ - Editing a child prefab **cascades**: every prefab file that nests it is
75
+ patched (overrides preserved), and every scene instance updates when you
76
+ close the editor — transitively through any depth of nesting.
77
+ - A prefab can never contain itself, directly or through another prefab
78
+ (the editor rejects the drop).
79
+ - Only *whole* instances nest — a partial slice of an instance can't become
80
+ a prefab.
81
+ - In the scene, content can't be re-parented across a nested instance
82
+ boundary (its identity would change); open the host prefab to
83
+ restructure.
84
+ - Runtime spawning is unaffected: files are fully materialized, so
85
+ `spawnPrefab` needs no resolution regardless of nesting depth.
86
+
87
+ ## Spawning at runtime
88
+
89
+ Scripts spawn prefabs with `this.spawnPrefab(source, options?)`. Declare an
90
+ `AssetInput` field and drag the `.prefab` onto it in the editor:
91
+
92
+ ```ts
93
+ import { Behaviour } from "@relu-interactives/spatial-ecs";
94
+
95
+ export default class EnemySpawner extends Behaviour {
96
+ data = {
97
+ // AssetInput — drag Enemy.prefab onto this field in the inspector
98
+ enemyPrefab: { path: "", type: "prefab" },
99
+ spawned: [] as number[],
100
+ };
101
+
102
+ private async spawnWave() {
103
+ for (let i = 0; i < 3; i++) {
104
+ const rootId = await this.spawnPrefab(this.data.enemyPrefab, {
105
+ position: { x: i * 2 - 2, y: 0, z: -6 },
106
+ });
107
+ this.data.spawned.push(rootId);
108
+ }
109
+ }
110
+
111
+ private despawnWave() {
112
+ for (const id of this.data.spawned) {
113
+ this.despawn(id); // removes the root and all of its children
114
+ }
115
+ this.data.spawned = [];
116
+ }
117
+ }
118
+ ```
119
+
120
+ Details:
121
+
122
+ - `spawnPrefab` resolves once the entities exist. Heavy assets inside
123
+ (models, textures, audio) stream in reactively — repeated spawns of the
124
+ same prefab share geometry through the model cache, so waves are cheap.
125
+ - `source` can be an `AssetInput` (`{ path, type }`), a URL string, or a
126
+ pre-loaded `PrefabAsset`.
127
+ - Options: `{ parentId, position, rotation, scale }` — the transform applies
128
+ to the spawned root in its parent's local space.
129
+ - `world.spawnPrefab(...)` / `world.despawnPrefab(id)` expose the same calls
130
+ outside of behaviours.
131
+
132
+ ## Preloading and caching
133
+
134
+ Each prefab URL is fetched and parsed once per session (`loadPrefab` caches
135
+ the parsed file; failed loads retry). Published experiences preload prefab
136
+ files referenced by script `AssetInput` fields — plus the models and
137
+ textures inside them — before the experience starts, so the first spawn
138
+ does not hitch.
139
+
140
+ ## Custom hosts
141
+
142
+ Prefab files store editor-space asset URLs. Published and preview builds
143
+ register a resolver that maps them onto the deployed `assets/` folder:
144
+
145
+ ```ts
146
+ import { setPrefabAssetUrlResolver } from "@relu-interactives/spatial-ecs";
147
+
148
+ setPrefabAssetUrlResolver((url) => rewriteToMyCdn(url));
149
+ ```
150
+
151
+ The standard project templates already do this — you only need it when
152
+ embedding the runtime in a custom host.
@@ -430,7 +430,7 @@ export default class PauseOnHit extends Behaviour {
430
430
  > [!TIP]
431
431
  > `pause()` and `resume()` affect the **entire world**. If you only want to
432
432
  > stop a subset of entities (for example, freeze enemies while a menu is open)
433
- > use a flag on a custom component instead and guard your `onUpdate` logic.
433
+ > use a flag in a custom script (`Behaviour`) instead and guard your `onUpdate` logic.
434
434
 
435
435
  ## See also
436
436
 
@@ -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 component = new PhysicsComponent(values);
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) {