@relu-interactives/spatial-ecs 0.2.22 → 0.2.24
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-GZ2NKSMV.js → chunk-CCE5K2TU.js} +101 -68
- package/dist/chunk-CCE5K2TU.js.map +1 -0
- package/dist/docs/guide/authoring-a-script-object.md +24 -11
- package/dist/docs/guide/systems/physics-sync-system.md +1 -1
- package/dist/index.cjs +99 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/systems/index.cjs +99 -66
- package/dist/systems/index.cjs.map +1 -1
- package/dist/systems/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-GZ2NKSMV.js.map +0 -1
|
@@ -14,18 +14,20 @@ import { ScriptObject, type ColorInput } from "@relu-interactives/spatial-ecs";
|
|
|
14
14
|
import * as THREE from "three";
|
|
15
15
|
|
|
16
16
|
export default class WavePlane extends ScriptObject {
|
|
17
|
-
//
|
|
18
|
-
declare data
|
|
17
|
+
// Inspector fields: an INLINE `data = { … }` literal (same envelopes as
|
|
18
|
+
// Behaviours). NEVER `declare data` — that is a type-only annotation with no
|
|
19
|
+
// runtime object, so the editor can't discover your fields and the inspector
|
|
20
|
+
// shows nothing to edit.
|
|
21
|
+
data = { color: "#3aa0ff" as ColorInput };
|
|
19
22
|
|
|
20
23
|
private mat!: THREE.ShaderMaterial;
|
|
21
24
|
|
|
22
25
|
// REQUIRED — build and return the visual root.
|
|
23
26
|
build(): THREE.Object3D {
|
|
24
|
-
this.data.color ??= { type: "color", value: "#3aa0ff" };
|
|
25
27
|
this.mat = new THREE.ShaderMaterial({
|
|
26
28
|
uniforms: {
|
|
27
29
|
uTime: { value: 0 },
|
|
28
|
-
uColor: { value: new THREE.Color(this.data.color
|
|
30
|
+
uColor: { value: new THREE.Color(this.data.color) },
|
|
29
31
|
},
|
|
30
32
|
vertexShader: /* glsl */ `
|
|
31
33
|
uniform float uTime;
|
|
@@ -66,22 +68,33 @@ export default class WavePlane extends ScriptObject {
|
|
|
66
68
|
| --- | --- | --- |
|
|
67
69
|
| `this.world` | `World` | The owning ECS world. |
|
|
68
70
|
| `this.entityId` | `number` | The entity this script is bound to. |
|
|
69
|
-
| `this.data` | `Record<string, unknown>` | Inspector-bound payload. Declare
|
|
71
|
+
| `this.data` | `Record<string, unknown>` | Inspector-bound payload. Declare it with an INLINE literal (`data = { … }`) — never `declare data` (a type-only annotation with no runtime object → no inspector fields). |
|
|
70
72
|
| `this.root` | `THREE.Object3D \| null` | The object `build()` returned (set after build). |
|
|
71
73
|
|
|
72
74
|
## Data fields (inspector)
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
Declare `data` as an **inline literal** (`data = { … }`) with the same input envelopes as Behaviours. **Never use `declare data`** — it is a type-only annotation that produces no runtime object, so the editor can't discover the fields and the inspector shows nothing to edit. (Likewise, don't set defaults with `this.data.x ??= …` inside `build()` — put the default in the literal.)
|
|
77
|
+
|
|
78
|
+
Simple scalars take a value plus an `as <Input>` cast and are read directly:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
data = {
|
|
82
|
+
color: "#3aa0ff" as ColorInput, // color swatch → this.data.color
|
|
83
|
+
speed: 1.0 as FloatInput, // number field → this.data.speed
|
|
84
|
+
enabled: true as ToggleInput, // toggle → this.data.enabled
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Rich inputs that carry metadata are authored as inline objects and read via `.value` (or the resolved view for entity/component pickers):
|
|
75
89
|
|
|
76
90
|
```ts
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
target: EntityInput; // entity picker
|
|
91
|
+
data = {
|
|
92
|
+
amplitude: { type: "slider", min: 0, max: 1, value: 0.5 }, // → this.data.amplitude.value
|
|
93
|
+
target: { type: "entity", value: null }, // → this.data.target.value
|
|
81
94
|
};
|
|
82
95
|
```
|
|
83
96
|
|
|
84
|
-
|
|
97
|
+
See [Data binding](./behaviour-data-binding/) for the full envelope catalog. Edits made in the inspector are pushed to the live instance's `this.data` between frames.
|
|
85
98
|
|
|
86
99
|
## Rules
|
|
87
100
|
|
|
@@ -45,7 +45,7 @@ Initializes and manages the Rapier physics world, creates rigid bodies and colli
|
|
|
45
45
|
## Behavior notes
|
|
46
46
|
|
|
47
47
|
- **Async init** — the Rapier WASM module is loaded once on the first `update()` call. Physics simulation does not start until the module is ready; entities created before init are processed on the first frame after init completes.
|
|
48
|
-
- **Trimesh** — `trimesh` colliders
|
|
48
|
+
- **Trimesh** — `trimesh` colliders merge the vertex/index data of **every mesh** under the entity (multi-primitive GLBs are fully covered) with the entity's **world scale baked into the vertices** — Rapier bodies carry no scale, so scaling the entity is how you resize a trimesh; the collider rebuilds automatically when the scale or geometry changes. The extracted data is cached per entity by a geometry+scale signature, and the heavy vertex merge runs only when that signature changes.
|
|
49
49
|
- **Sensor colliders** — setting `values.collider.isSensor = true` creates a Rapier sensor collider. Sensor contacts do not generate forces but can be used for overlap/trigger queries.
|
|
50
50
|
- **Gravity** — the global gravity vector is read from the `Environment` component (`gravity` field, default `{ x: 0, y: -9.81, z: 0 }`) and applied to the Rapier world gravity setting each frame.
|
|
51
51
|
- **Preview only** — the system is typically constructed with `{ isEditor: false }` for preview. In the editor the physics world step is skipped so objects don't fall during authoring.
|
package/dist/index.cjs
CHANGED
|
@@ -6789,82 +6789,108 @@ function createColliderDesc(RAPIER, values, triMeshData) {
|
|
|
6789
6789
|
return desc;
|
|
6790
6790
|
}
|
|
6791
6791
|
__name(createColliderDesc, "createColliderDesc");
|
|
6792
|
-
function
|
|
6793
|
-
if (object
|
|
6794
|
-
return
|
|
6792
|
+
function collectTriMeshParts(object) {
|
|
6793
|
+
if (!object) {
|
|
6794
|
+
return null;
|
|
6795
6795
|
}
|
|
6796
|
-
|
|
6796
|
+
object.updateWorldMatrix(true, true);
|
|
6797
|
+
const worldScale = new THREE36__namespace.Vector3();
|
|
6798
|
+
object.getWorldScale(worldScale);
|
|
6799
|
+
const scale = {
|
|
6800
|
+
x: Math.abs(worldScale.x) || 1,
|
|
6801
|
+
y: Math.abs(worldScale.y) || 1,
|
|
6802
|
+
z: Math.abs(worldScale.z) || 1
|
|
6803
|
+
};
|
|
6804
|
+
const worldToObject = new THREE36__namespace.Matrix4().copy(object.matrixWorld).invert();
|
|
6805
|
+
const parts = [];
|
|
6797
6806
|
object.traverse((child) => {
|
|
6798
|
-
if (
|
|
6807
|
+
if (!(child instanceof THREE36__namespace.Mesh) || !child.geometry) {
|
|
6799
6808
|
return;
|
|
6800
6809
|
}
|
|
6801
|
-
|
|
6802
|
-
|
|
6810
|
+
const geometry = child.geometry;
|
|
6811
|
+
const positionAttribute = geometry.getAttribute("position");
|
|
6812
|
+
if (!positionAttribute || positionAttribute.itemSize < 3) {
|
|
6813
|
+
return;
|
|
6803
6814
|
}
|
|
6815
|
+
const positionCount = positionAttribute.count;
|
|
6816
|
+
if (positionCount < 3) {
|
|
6817
|
+
return;
|
|
6818
|
+
}
|
|
6819
|
+
const indexAttribute = geometry.getIndex();
|
|
6820
|
+
const indexCount = indexAttribute ? indexAttribute.count : Math.floor(positionCount / 3) * 3;
|
|
6821
|
+
if (indexCount < 3) {
|
|
6822
|
+
return;
|
|
6823
|
+
}
|
|
6824
|
+
const relativeMatrix = new THREE36__namespace.Matrix4().multiplyMatrices(
|
|
6825
|
+
worldToObject,
|
|
6826
|
+
child.matrixWorld
|
|
6827
|
+
);
|
|
6828
|
+
const matrixSignature = relativeMatrix.elements.map((value) => value.toFixed(5)).join(",");
|
|
6829
|
+
parts.push({
|
|
6830
|
+
positionAttribute,
|
|
6831
|
+
indexAttribute,
|
|
6832
|
+
positionCount,
|
|
6833
|
+
indexCount,
|
|
6834
|
+
relativeMatrix,
|
|
6835
|
+
signature: `${child.uuid}:${geometry.uuid}:${positionCount}:${indexCount}:${matrixSignature}`
|
|
6836
|
+
});
|
|
6804
6837
|
});
|
|
6805
|
-
|
|
6806
|
-
}
|
|
6807
|
-
__name(findFirstMeshWithGeometry, "findFirstMeshWithGeometry");
|
|
6808
|
-
function createTriMeshData(object) {
|
|
6809
|
-
if (!object) {
|
|
6838
|
+
if (parts.length === 0) {
|
|
6810
6839
|
return null;
|
|
6811
6840
|
}
|
|
6812
|
-
const
|
|
6813
|
-
|
|
6841
|
+
const scaleSignature = `${scale.x.toFixed(5)},${scale.y.toFixed(5)},${scale.z.toFixed(5)}`;
|
|
6842
|
+
return {
|
|
6843
|
+
parts,
|
|
6844
|
+
scale,
|
|
6845
|
+
signature: `${scaleSignature}|${parts.map((p) => p.signature).join("|")}`
|
|
6846
|
+
};
|
|
6847
|
+
}
|
|
6848
|
+
__name(collectTriMeshParts, "collectTriMeshParts");
|
|
6849
|
+
function createTriMeshData(collected) {
|
|
6850
|
+
if (!collected) {
|
|
6814
6851
|
return null;
|
|
6815
6852
|
}
|
|
6816
|
-
const
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6853
|
+
const { parts, scale } = collected;
|
|
6854
|
+
let vertexTotal = 0;
|
|
6855
|
+
let indexTotal = 0;
|
|
6856
|
+
for (const part of parts) {
|
|
6857
|
+
vertexTotal += part.positionCount;
|
|
6858
|
+
indexTotal += part.indexCount;
|
|
6820
6859
|
}
|
|
6821
|
-
|
|
6822
|
-
if (positionCount < 3) {
|
|
6860
|
+
if (vertexTotal < 3 || indexTotal < 3) {
|
|
6823
6861
|
return null;
|
|
6824
6862
|
}
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
const worldToObject = new THREE36__namespace.Matrix4().copy(object.matrixWorld).invert();
|
|
6828
|
-
const relativeMatrix = new THREE36__namespace.Matrix4().multiplyMatrices(
|
|
6829
|
-
worldToObject,
|
|
6830
|
-
mesh.matrixWorld
|
|
6831
|
-
);
|
|
6832
|
-
const vertices = new Float32Array(positionCount * 3);
|
|
6863
|
+
const vertices = new Float32Array(vertexTotal * 3);
|
|
6864
|
+
const indices = new Uint32Array(indexTotal);
|
|
6833
6865
|
const point = new THREE36__namespace.Vector3();
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
indices = new Uint32Array(triangleIndexCount);
|
|
6858
|
-
for (let index = 0; index < triangleIndexCount; index += 1) {
|
|
6859
|
-
indices[index] = index;
|
|
6866
|
+
let vertexOffset = 0;
|
|
6867
|
+
let indexOffset = 0;
|
|
6868
|
+
for (const part of parts) {
|
|
6869
|
+
const { positionAttribute, indexAttribute, relativeMatrix } = part;
|
|
6870
|
+
for (let index = 0; index < part.positionCount; index += 1) {
|
|
6871
|
+
point.set(
|
|
6872
|
+
positionAttribute.getX(index),
|
|
6873
|
+
positionAttribute.getY(index),
|
|
6874
|
+
positionAttribute.getZ(index)
|
|
6875
|
+
).applyMatrix4(relativeMatrix);
|
|
6876
|
+
const outputIndex = (vertexOffset + index) * 3;
|
|
6877
|
+
vertices[outputIndex] = point.x * scale.x;
|
|
6878
|
+
vertices[outputIndex + 1] = point.y * scale.y;
|
|
6879
|
+
vertices[outputIndex + 2] = point.z * scale.z;
|
|
6880
|
+
}
|
|
6881
|
+
if (indexAttribute) {
|
|
6882
|
+
for (let index = 0; index < part.indexCount; index += 1) {
|
|
6883
|
+
indices[indexOffset + index] = vertexOffset + indexAttribute.getX(index);
|
|
6884
|
+
}
|
|
6885
|
+
} else {
|
|
6886
|
+
for (let index = 0; index < part.indexCount; index += 1) {
|
|
6887
|
+
indices[indexOffset + index] = vertexOffset + index;
|
|
6888
|
+
}
|
|
6860
6889
|
}
|
|
6890
|
+
vertexOffset += part.positionCount;
|
|
6891
|
+
indexOffset += part.indexCount;
|
|
6861
6892
|
}
|
|
6862
|
-
|
|
6863
|
-
return {
|
|
6864
|
-
vertices,
|
|
6865
|
-
indices,
|
|
6866
|
-
signature: `${mesh.uuid}:${geometry.uuid}:${positionCount}:${indices.length}:${matrixSignature}`
|
|
6867
|
-
};
|
|
6893
|
+
return { vertices, indices, signature: collected.signature };
|
|
6868
6894
|
}
|
|
6869
6895
|
__name(createTriMeshData, "createTriMeshData");
|
|
6870
6896
|
function syncBodyFromTransform(body, transform) {
|
|
@@ -7041,7 +7067,7 @@ var _PhysicsSyncSystem = class _PhysicsSyncSystem {
|
|
|
7041
7067
|
syncBodyFromTransform(body, transform);
|
|
7042
7068
|
}
|
|
7043
7069
|
const effectiveCollider = computeColliderState(physics.values.collider, object);
|
|
7044
|
-
const
|
|
7070
|
+
const triMeshParts = effectiveCollider.shape === "trimesh" ? collectTriMeshParts(object) : null;
|
|
7045
7071
|
if (!effectiveCollider || !effectiveCollider.enabled) {
|
|
7046
7072
|
const colliderHandle = this.colliderHandles.get(entityId);
|
|
7047
7073
|
if (typeof colliderHandle === "number") {
|
|
@@ -7055,7 +7081,7 @@ var _PhysicsSyncSystem = class _PhysicsSyncSystem {
|
|
|
7055
7081
|
physics.collider = null;
|
|
7056
7082
|
continue;
|
|
7057
7083
|
}
|
|
7058
|
-
const colliderSignature = `${JSON.stringify(effectiveCollider)}:${
|
|
7084
|
+
const colliderSignature = `${JSON.stringify(effectiveCollider)}:${triMeshParts?.signature ?? ""}`;
|
|
7059
7085
|
const previousColliderSignature = this.colliderSignatures.get(entityId);
|
|
7060
7086
|
if (!this.colliderHandles.has(entityId) || previousColliderSignature !== colliderSignature) {
|
|
7061
7087
|
const previousColliderHandle = this.colliderHandles.get(entityId);
|
|
@@ -7068,7 +7094,7 @@ var _PhysicsSyncSystem = class _PhysicsSyncSystem {
|
|
|
7068
7094
|
const colliderDesc = createColliderDesc(
|
|
7069
7095
|
this.rapier,
|
|
7070
7096
|
effectiveCollider,
|
|
7071
|
-
|
|
7097
|
+
createTriMeshData(triMeshParts)
|
|
7072
7098
|
);
|
|
7073
7099
|
const createdCollider = physicsWorld.createCollider(colliderDesc, body);
|
|
7074
7100
|
this.colliderHandles.set(entityId, createdCollider.handle);
|
|
@@ -9419,9 +9445,9 @@ var _ScriptObjectSystem = class _ScriptObjectSystem {
|
|
|
9419
9445
|
} catch {
|
|
9420
9446
|
instance = new ctor(world, entityId, currentData);
|
|
9421
9447
|
}
|
|
9422
|
-
const
|
|
9423
|
-
const
|
|
9424
|
-
instance.data = resolveInputData(world,
|
|
9448
|
+
const literalDefaults = normalizeScriptData(instance.data);
|
|
9449
|
+
const seed = Object.keys(literalDefaults).length > 0 ? mergeDataByCurrentDefaults(literalDefaults, currentData) : { ...currentData };
|
|
9450
|
+
instance.data = resolveInputData(world, seed);
|
|
9425
9451
|
const parent = world.getComponent(entityId, Object3DRef)?.object;
|
|
9426
9452
|
if (!parent) {
|
|
9427
9453
|
throw new Error("Script object entity has no Object3D to attach to.");
|
|
@@ -9451,6 +9477,13 @@ var _ScriptObjectSystem = class _ScriptObjectSystem {
|
|
|
9451
9477
|
built.userData[SCRIPT_OBJECT_ROOT_FLAG] = true;
|
|
9452
9478
|
parent.add(built);
|
|
9453
9479
|
instance.root = built;
|
|
9480
|
+
const postBuildData = normalizeScriptData(instance.data);
|
|
9481
|
+
const schemaDefaults = { ...literalDefaults };
|
|
9482
|
+
for (const [key2, value] of Object.entries(postBuildData)) {
|
|
9483
|
+
if (!(key2 in schemaDefaults)) schemaDefaults[key2] = value;
|
|
9484
|
+
}
|
|
9485
|
+
const merged = mergeDataByCurrentDefaults(schemaDefaults, currentData);
|
|
9486
|
+
instance.data = resolveInputData(world, merged);
|
|
9454
9487
|
const stored = serializeDataForStorage(merged, merged);
|
|
9455
9488
|
component.data = stored;
|
|
9456
9489
|
component.runtimeState = "ready";
|