@forgeax/engine-scene 0.1.21 → 0.1.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/README.md +82 -11
- package/dist/__tests__/fixtures/malformed-hierarchy-edge.d.ts +5 -3
- package/dist/__tests__/fixtures/malformed-hierarchy-edge.d.ts.map +1 -1
- package/dist/__tests__/flat-propagation.contract.test.d.ts +2 -0
- package/dist/__tests__/flat-propagation.contract.test.d.ts.map +1 -0
- package/dist/__tests__/flat-propagation.derived-writer.contract.test.d.ts +2 -0
- package/dist/__tests__/flat-propagation.derived-writer.contract.test.d.ts.map +1 -0
- package/dist/__tests__/flat-propagation.perf.test.d.ts +2 -0
- package/dist/__tests__/flat-propagation.perf.test.d.ts.map +1 -0
- package/dist/__tests__/hierarchy-propagation.perf.test.d.ts +2 -0
- package/dist/__tests__/hierarchy-propagation.perf.test.d.ts.map +1 -0
- package/dist/__tests__/scene-binding.integration.test.d.ts +2 -0
- package/dist/__tests__/scene-binding.integration.test.d.ts.map +1 -0
- package/dist/__tests__/schedule-order.contract.test.d.ts +2 -0
- package/dist/__tests__/schedule-order.contract.test.d.ts.map +1 -0
- package/dist/__tests__/structural.test.d.ts +2 -0
- package/dist/__tests__/structural.test.d.ts.map +1 -0
- package/dist/__tests__/transform-carrier-consumers.test-d.d.ts +2 -0
- package/dist/__tests__/transform-carrier-consumers.test-d.d.ts.map +1 -0
- package/dist/__tests__/transform-carrier-prototype.bench.d.ts +39 -0
- package/dist/__tests__/transform-carrier-prototype.bench.d.ts.map +1 -0
- package/dist/__tests__/transform-carrier-prototype.test-d.d.ts +2 -0
- package/dist/__tests__/transform-carrier-prototype.test-d.d.ts.map +1 -0
- package/dist/__tests__/transform-carrier-prototype.test.d.ts +2 -0
- package/dist/__tests__/transform-carrier-prototype.test.d.ts.map +1 -0
- package/dist/__tests__/transform-component-version.unit.test.d.ts +2 -0
- package/dist/__tests__/transform-component-version.unit.test.d.ts.map +1 -0
- package/dist/assets/scene-decoder.d.ts.map +1 -1
- package/dist/components/child-of.d.ts +23 -14
- package/dist/components/child-of.d.ts.map +1 -1
- package/dist/components/children.d.ts +14 -13
- package/dist/components/children.d.ts.map +1 -1
- package/dist/components/transform.d.ts +7 -43
- package/dist/components/transform.d.ts.map +1 -1
- package/dist/errors.d.ts +23 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +1122 -358
- package/dist/index.mjs.map +1 -1
- package/dist/instances/binding.d.ts +29 -0
- package/dist/instances/binding.d.ts.map +1 -0
- package/dist/instances/externalization.d.ts.map +1 -1
- package/dist/instances/scene-instances.d.ts +6 -18
- package/dist/instances/scene-instances.d.ts.map +1 -1
- package/dist/instances/state.d.ts +28 -0
- package/dist/instances/state.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/systems/hierarchy-projection.d.ts.map +1 -1
- package/dist/systems/index.d.ts +1 -1
- package/dist/systems/index.d.ts.map +1 -1
- package/dist/systems/propagate-transforms.d.ts +22 -2
- package/dist/systems/propagate-transforms.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/components.unit.test.ts +34 -2
- package/src/__tests__/fixtures/malformed-hierarchy-edge.ts +26 -4
- package/src/__tests__/flat-propagation.contract.test.ts +128 -0
- package/src/__tests__/flat-propagation.derived-writer.contract.test.ts +39 -0
- package/src/__tests__/flat-propagation.perf.test.ts +336 -0
- package/src/__tests__/hierarchy-propagation.perf.test.ts +149 -0
- package/src/__tests__/plugin.integration.test.ts +2 -2
- package/src/__tests__/propagation.unit.test.ts +271 -91
- package/src/__tests__/scene-binding.integration.test.ts +124 -0
- package/src/__tests__/schedule-order.contract.test.ts +43 -0
- package/src/__tests__/structural.test.ts +23 -0
- package/src/__tests__/transform-carrier-consumers.test-d.ts +156 -0
- package/src/__tests__/transform-carrier-prototype.bench.ts +168 -0
- package/src/__tests__/transform-carrier-prototype.test-d.ts +71 -0
- package/src/__tests__/transform-carrier-prototype.test.ts +170 -0
- package/src/__tests__/{transform-change-journal.unit.test.ts → transform-component-version.unit.test.ts} +32 -39
- package/src/assets/scene-decoder.ts +6 -1
- package/src/components/child-of.ts +38 -27
- package/src/components/children.ts +36 -37
- package/src/components/transform.ts +23 -81
- package/src/errors.ts +26 -2
- package/src/index.ts +10 -1
- package/src/instances/binding.ts +77 -0
- package/src/instances/externalization.ts +7 -1
- package/src/instances/scene-instances.ts +51 -80
- package/src/instances/state.ts +62 -0
- package/src/plugin.ts +9 -2
- package/src/systems/hierarchy-projection.ts +25 -8
- package/src/systems/index.ts +3 -0
- package/src/systems/propagate-transforms.ts +1142 -240
- package/dist/.tsbuildinfo +0 -1
- package/dist/__tests__/transform-change-journal.unit.test.d.ts +0 -2
- package/dist/__tests__/transform-change-journal.unit.test.d.ts.map +0 -1
|
@@ -110,7 +110,11 @@ function resolveSceneWireRefs(payload: SceneAsset, refs: readonly string[]): Sce
|
|
|
110
110
|
// component registry.
|
|
111
111
|
components[componentName] = { ...(rawFields as Record<string, unknown>) };
|
|
112
112
|
}
|
|
113
|
-
entities.push({
|
|
113
|
+
entities.push({
|
|
114
|
+
localId: entity.localId,
|
|
115
|
+
...(entity.bindingKey === undefined ? {} : { bindingKey: entity.bindingKey }),
|
|
116
|
+
components,
|
|
117
|
+
});
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
const mounts = resolveMounts(payload.mounts, refs);
|
|
@@ -125,6 +129,7 @@ function resolveSceneWireRefs(payload: SceneAsset, refs: readonly string[]): Sce
|
|
|
125
129
|
ok: true,
|
|
126
130
|
value: {
|
|
127
131
|
kind: 'scene',
|
|
132
|
+
...(payload.sourceKey === undefined ? {} : { sourceKey: payload.sourceKey }),
|
|
128
133
|
entities,
|
|
129
134
|
...(mounts.value === undefined ? {} : { mounts: mounts.value }),
|
|
130
135
|
...(skinGuids.value === undefined ? {} : { skinGuids: skinGuids.value }),
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// u32 against the live record on read; `world.get(child, ChildOf)` returns
|
|
8
8
|
// the raw encoded Entity and consumers check liveness themselves (via
|
|
9
9
|
// `world.get(parent, Entity)` — returns `err(stale-entity)` for despawned handles).
|
|
10
|
+
// Public source writes validate target liveness before committing; only
|
|
11
|
+
// controlled internal fixtures can create a stale source for diagnostics.
|
|
10
12
|
//
|
|
11
13
|
// feat-20260531-ecs-relationship-abstraction-bidirectional-sync M4 / t20:
|
|
12
14
|
// ChildOf is declared as the holder side of a Bevy-style bidirectional
|
|
@@ -30,30 +32,30 @@
|
|
|
30
32
|
// the D-1 default from false to true). When linkedSpawn is set to true,
|
|
31
33
|
// world.despawn(parent) recursively despawns the entire subtree.
|
|
32
34
|
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
35
|
+
// `ChildOf` uses linkedSpawn: true, so despawning a parent recursively
|
|
36
|
+
// despawns its linked subtree. A generic relationship may opt out with
|
|
37
|
+
// linkedSpawn: false, but that is not this scene hierarchy's lifecycle.
|
|
36
38
|
//
|
|
37
|
-
// Child despawn auto-detaches from
|
|
39
|
+
// Child despawn auto-detaches from its parent's Children list: the
|
|
38
40
|
// relationship `onRemove` hook fires on `world.despawn(child)` and prunes the
|
|
39
41
|
// child from the parent's `Children.entities` (a write-path hook). Despawning
|
|
40
|
-
// the
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
42
|
+
// the parent follows ChildOf's linkedSpawn cascade and retires the linked
|
|
43
|
+
// subtree. A stale ChildOf source is consequently only an explicitly malformed
|
|
44
|
+
// internal fixture (or a generic non-linked relationship); the consumer repairs
|
|
45
|
+
// it with `world.removeComponent(child, ChildOf)` after the structured error.
|
|
44
46
|
//
|
|
45
|
-
// propagateTransforms system (./systems/propagate-transforms.ts) consumes
|
|
46
|
-
//
|
|
47
|
-
//
|
|
47
|
+
// propagateTransforms system (./systems/propagate-transforms.ts) consumes the
|
|
48
|
+
// ECS-maintained Children buffer for parent-first traversal and reads this
|
|
49
|
+
// source column only to validate the mirrored edge. When a parent has been despawned
|
|
48
50
|
// but the child's stale ChildOf is left in place, the live-map lookup surfaces
|
|
49
|
-
// `
|
|
51
|
+
// `SceneError({ code: 'hierarchy-broken' })` - a deliberate per-frame fail-fast
|
|
50
52
|
// (the consumer is expected to despawn the subtree or remove the stale
|
|
51
53
|
// ChildOf), kept as the explicit-failure surface (charter proposition 4).
|
|
52
54
|
//
|
|
53
55
|
// charter mapping: proposition 2 (industry analogy: Bevy ChildOf is the
|
|
54
56
|
// post-0.15 rename of Parent for the same shape as glTF node parent index
|
|
55
57
|
// + Three.js Object3D.parent) + proposition 4 (explicit failure: dangling
|
|
56
|
-
// parent surfaces structured
|
|
58
|
+
// parent surfaces structured SceneError) + proposition 5
|
|
57
59
|
// (consistent abstraction: the schema-vocab 'entity' keyword is the SSOT
|
|
58
60
|
// for entity-typed columns across the engine).
|
|
59
61
|
|
|
@@ -63,25 +65,34 @@ export { ChildOf } from './children';
|
|
|
63
65
|
* Hierarchy back-reference: pointer from child entity to its parent.
|
|
64
66
|
*
|
|
65
67
|
* Store the parent `Entity` handle (returned by `world.spawn(...).unwrap()`)
|
|
66
|
-
* in the `parent` field; `propagateTransforms`
|
|
67
|
-
* compose the child's derived
|
|
68
|
-
* parent.world x child local).
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
68
|
+
* in the `parent` field; `propagateTransforms` follows the ECS-maintained
|
|
69
|
+
* `Children` list each frame to compose the child's derived
|
|
70
|
+
* `GlobalTransform.world` mat4 (child.world = parent.world x child local).
|
|
71
|
+
* `Transform` declares `GlobalTransform` as a
|
|
72
|
+
* generic ECS requirement, so normal spawn/add/command paths materialize the
|
|
73
|
+
* pair without scene-specific repair. Propagation still fails closed when an
|
|
74
|
+
* owner explicitly removes one half. Despawning a child auto-detaches it from
|
|
75
|
+
* its parent's Children list; despawning the parent follows ChildOf's
|
|
76
|
+
* linkedSpawn cascade, while a malformed internal fixture can still leave a
|
|
77
|
+
* stale source for Scene diagnostics.
|
|
72
78
|
*
|
|
73
|
-
* Because ChildOf declares a `relationship` mirror
|
|
74
|
-
* parent's `Children.entities`
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
79
|
+
* Because ChildOf declares a `relationship` mirror and a structural
|
|
80
|
+
* `Transform` requirement, the engine keeps the parent's `Children.entities`
|
|
81
|
+
* reverse list consistent automatically and materializes the local/world pair
|
|
82
|
+
* when the child did not author one: adding ChildOf appends the child to the
|
|
83
|
+
* parent's Children, removing it (or reparenting via the `exclusive` arm)
|
|
84
|
+
* prunes the stale entry. AI users no longer hand-maintain both sides (the
|
|
85
|
+
* prior OOS-10 contract is retired).
|
|
78
86
|
* Reparenting is a plain re-add: `world.addComponent(child, { component:
|
|
79
87
|
* ChildOf, data: { parent: newParent } })` on an entity that already carries
|
|
80
|
-
* ChildOf auto-reparents (exclusive arm)
|
|
81
|
-
*
|
|
88
|
+
* ChildOf auto-reparents (exclusive arm). The generic convenience method has
|
|
89
|
+
* the same owner path and requires the relationship component plus its data:
|
|
90
|
+
* `world.reparent(child, newParent, ChildOf, { parent: newParent })`.
|
|
82
91
|
*
|
|
83
92
|
* @example Spawn a child entity referencing an already-spawned root:
|
|
84
|
-
* const root = world.spawn(
|
|
93
|
+
* const root = world.spawn(
|
|
94
|
+
* { component: Transform, data: {...} },
|
|
95
|
+
* ).unwrap();
|
|
85
96
|
* const child = world.spawn(
|
|
86
97
|
* { component: Transform, data: {...} },
|
|
87
98
|
* { component: ChildOf, data: { parent: root } },
|
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
// @forgeax/engine-runtime - Children (forward-list of child entities).
|
|
2
2
|
//
|
|
3
3
|
// Schema: 1 array<entity> field `entities` (variable-length, ECS-managed via
|
|
4
|
-
// the BufferPool slot column + sidecar count column allocated by the ECS
|
|
5
|
-
//
|
|
4
|
+
// the BufferPool slot column + sidecar count column allocated by the ECS
|
|
5
|
+
// relationship owner).
|
|
6
6
|
//
|
|
7
7
|
// feat-20260515-buffer-array-vocab-collapse M3 / w17:
|
|
8
8
|
// the legacy `VarArrayView<Entity>` value-shape wrapper was retired in
|
|
9
|
-
// favour of a direct `TypedArray` snapshot returned by `world.get
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// world.push(parent, Children, 'entities', child).unwrap();
|
|
13
|
-
// world.pop(parent, Children, 'entities').unwrap();
|
|
14
|
-
// world.capacity(parent, Children, 'entities').unwrap();
|
|
15
|
-
//
|
|
16
|
-
// And read through the read-only `Uint32Array` snapshot:
|
|
9
|
+
// favour of a direct `TypedArray` snapshot returned by `world.get`. AI users
|
|
10
|
+
// read the engine-maintained list through the read-only `Uint32Array` snapshot:
|
|
17
11
|
//
|
|
18
12
|
// const snap = world.get(parent, Children).unwrap().entities;
|
|
19
13
|
// const liveCount = snap.length;
|
|
20
14
|
// for (let i = 0; i < liveCount; i++) { const child = snap[i]; ... }
|
|
21
15
|
//
|
|
22
16
|
// Snapshot length equals the live element count (sidecar count column owned
|
|
23
|
-
// by the ECS layer); the snapshot is rematerialised on
|
|
24
|
-
// (D-4 no-cache)
|
|
25
|
-
//
|
|
17
|
+
// by the ECS layer); the public snapshot is detached and rematerialised on
|
|
18
|
+
// every `world.get` (D-4 no-cache), so `fill` or index writes cannot mutate the
|
|
19
|
+
// target. Internal relationship maintenance and Scene traversal borrow the
|
|
20
|
+
// live array through the package-internal zero-copy seams instead.
|
|
26
21
|
//
|
|
27
22
|
// feat-20260531-ecs-relationship-abstraction-bidirectional-sync M4 / t20:
|
|
28
23
|
// Children is the MIRROR side of the ChildOf relationship. Its schema is
|
|
@@ -33,9 +28,8 @@
|
|
|
33
28
|
// "AI users keep the two sides consistent themselves" contract is retired:
|
|
34
29
|
// `world.addComponent(child, ChildOf{parent})` appends `child` to
|
|
35
30
|
// `parent.Children.entities`, `world.removeComponent` / reparent prunes it.
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// hierarchy the engine owns consistency.
|
|
31
|
+
// For the ChildOf hierarchy the engine owns consistency; no public target
|
|
32
|
+
// write can diverge from the source relationship.
|
|
39
33
|
//
|
|
40
34
|
// feat-20260514-ecs-children-instances-managed-buffer-array M3 / w13 (kept
|
|
41
35
|
// for context): migrated from the legacy `{ count: 'u32' }` advisory marker
|
|
@@ -43,14 +37,12 @@
|
|
|
43
37
|
// - OOS-09 (prior loop): no `addChild` / `removeChild` / `removeChildren`
|
|
44
38
|
// Commands API. Retired this feat: `world.addChild` / `world.removeChild`
|
|
45
39
|
// / `world.reparent` ship in M3, plus the relationship hook above.
|
|
46
|
-
// -
|
|
47
|
-
// child
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// (explicit failure): the engine does not silently drop dangling
|
|
53
|
-
// entries.
|
|
40
|
+
// - Normal ChildOf child despawn invokes the source onRemove hook and
|
|
41
|
+
// removes the child before the row is retired; parent despawn follows the
|
|
42
|
+
// linkedSpawn cascade. A dangling u32 is therefore an explicitly malformed
|
|
43
|
+
// internal fixture or a non-linked generic relationship, not a normal
|
|
44
|
+
// ChildOf lifecycle result. Consumers still probe liveness before using a
|
|
45
|
+
// handle and receive the structured ECS error for malformed state.
|
|
54
46
|
//
|
|
55
47
|
// charter mapping: proposition 2 (Bevy ChildOf+Children pair, holder
|
|
56
48
|
// perspective) + proposition 3 (machine-readable schema:
|
|
@@ -60,30 +52,32 @@
|
|
|
60
52
|
// generic relationship-mirror shape, not a ChildOf special case).
|
|
61
53
|
|
|
62
54
|
import { defineRelationship } from '@forgeax/engine-ecs';
|
|
55
|
+
import { Transform } from './transform';
|
|
63
56
|
|
|
64
57
|
/**
|
|
65
58
|
* Hierarchy forward-list of child entities.
|
|
66
59
|
*
|
|
67
60
|
* `entities` is a variable-length `array<entity>` field; each element is
|
|
68
|
-
* an `Entity` u32 the
|
|
69
|
-
* `world.
|
|
70
|
-
* `world.get(parent, Children).unwrap().entities` is a read-only
|
|
61
|
+
* an `Entity` u32 the ECS relationship owner materialized. The value returned
|
|
62
|
+
* by `world.get(parent, Children).unwrap().entities` is a detached read-only
|
|
71
63
|
* `Uint32Array` snapshot rematerialised fresh on every read (D-4 no-cache);
|
|
72
|
-
* the
|
|
64
|
+
* mutating the returned array cannot change ECS-owned storage, and the
|
|
65
|
+
* snapshot's `length` equals the live element count. Internal Scene/ECS paths
|
|
66
|
+
* use the package-internal zero-copy array seams instead of this snapshot.
|
|
73
67
|
*
|
|
74
68
|
* Invariants:
|
|
75
|
-
* -
|
|
76
|
-
*
|
|
77
|
-
* inspection.
|
|
69
|
+
* - `propagateTransforms` consumes Children from the ECS-owned materialized
|
|
70
|
+
* buffer and expands each root parent-first. The forward list is also
|
|
71
|
+
* available for AI-user traversal / debug / inspection.
|
|
78
72
|
* - Children <-> ChildOf consistency is maintained by the engine via the
|
|
79
73
|
* ChildOf `relationship` mirror hook (see ./child-of.ts): adding /
|
|
80
74
|
* removing / reparenting ChildOf on a child auto-updates the parent's
|
|
81
75
|
* `entities` list. AI users do not hand-sync the two sides for the
|
|
82
76
|
* hierarchy.
|
|
83
|
-
* -
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* through the
|
|
77
|
+
* - ChildOf's linked lifecycle keeps ordinary Children entries aligned: a
|
|
78
|
+
* child despawn prunes its source slot and a parent despawn cascades. A
|
|
79
|
+
* deliberately malformed/non-linked edge remains observable as a dead
|
|
80
|
+
* handle and must be diagnosed through the structured error channel.
|
|
87
81
|
*
|
|
88
82
|
* @example Spawn a parent and two children via ChildOf (engine maintains Children):
|
|
89
83
|
* const parent = world.spawn({ component: Transform, data: identityXf() }).unwrap();
|
|
@@ -99,8 +93,8 @@ import { defineRelationship } from '@forgeax/engine-ecs';
|
|
|
99
93
|
* const snap = world.get(parent, Children).unwrap().entities;
|
|
100
94
|
* for (let i = 0; i < snap.length; i++) {
|
|
101
95
|
* const child = snap[i];
|
|
102
|
-
* // ... consume;
|
|
103
|
-
* //
|
|
96
|
+
* // ... consume; probe the handle before using it when reading a
|
|
97
|
+
* // deliberately malformed/non-linked relationship.
|
|
104
98
|
* }
|
|
105
99
|
*/
|
|
106
100
|
export const { source: ChildOf, target: Children } = defineRelationship({
|
|
@@ -108,6 +102,11 @@ export const { source: ChildOf, target: Children } = defineRelationship({
|
|
|
108
102
|
sourceField: 'parent',
|
|
109
103
|
targetName: 'Children',
|
|
110
104
|
targetField: 'entities',
|
|
105
|
+
// Every scene hierarchy node is spatial. Adding ChildOf therefore
|
|
106
|
+
// materializes the local/derived transform pair at the same structural
|
|
107
|
+
// boundary, so render- and scene-authored children cannot enter a frame
|
|
108
|
+
// with an incomplete hierarchy node.
|
|
109
|
+
sourceRequires: [Transform],
|
|
111
110
|
exclusive: true,
|
|
112
111
|
linkedSpawn: true,
|
|
113
112
|
});
|
|
@@ -1,89 +1,31 @@
|
|
|
1
|
-
// @forgeax/engine-runtime -
|
|
2
|
-
//
|
|
3
|
-
// Schema: three local array<f32, N> columns -- pos (3-vec position), quat
|
|
4
|
-
// (4-quat rotation, component order [x, y, z, w]), scale (3-vec scale) --
|
|
5
|
-
// plus one `world: array<f32, 16>` field carrying the resolved world-space
|
|
6
|
-
// mat4 (column-major 16 floats, written by the propagate kernel each frame).
|
|
7
|
-
// Inline stride-N array columns (feat-20260602) store each row's N floats
|
|
8
|
-
// contiguously, so per-row xyz locality is native to the column layout; the
|
|
9
|
-
// former per-axis scalar decomposition (10 f32 columns) predates
|
|
10
|
-
// inline array columns and was retired in feat-20260709 M2.
|
|
11
|
-
//
|
|
12
|
-
// The world column is the SSOT for the resolved world transform: a root's
|
|
13
|
-
// world equals its local mat4; a child's world equals parent.world x local.
|
|
14
|
-
// AI users author the local TRS arrays and read the derived world mat4 via
|
|
15
|
-
// the ECS column-level array view (`world.get(e, Transform).world` -> live
|
|
16
|
-
// Float32Array of 16 column-major floats). They MUST NOT hand-write the
|
|
17
|
-
// world column -- it is overwritten by propagate.
|
|
18
|
-
//
|
|
19
|
-
// charter mapping: P1 (progressive disclosure via defaults map -- AI users
|
|
20
|
-
// spawn with data: {} and get identity local TRS + identity world mat4),
|
|
21
|
-
// F1 (context-limited: single-import barrel; 3 array keys replace 10 scalar
|
|
22
|
-
// keys at every spawn call-site), P4 (consistent abstraction: pos / quat /
|
|
23
|
-
// scale array columns follow the same access pattern as `world` -- learn
|
|
24
|
-
// the flat column form once, apply it to every array<f32, N> field),
|
|
25
|
-
// P3 (machine-readable schema > prose).
|
|
1
|
+
// @forgeax/engine-runtime - authored local transform and derived world output.
|
|
26
2
|
|
|
27
3
|
import { defineComponent } from '@forgeax/engine-ecs';
|
|
28
4
|
|
|
29
|
-
// Column-major identity mat4 (16 floats) used as the world-column default so
|
|
30
|
-
// `spawn({ component: Transform, data: {} })` lands an identity world view
|
|
31
|
-
// before the first propagate pass writes the resolved transform.
|
|
32
5
|
const IDENTITY_MAT4 = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
33
6
|
|
|
7
|
+
/** Scene-owned derived world transform. Only TransformPropagation writes it. */
|
|
8
|
+
export const GlobalTransform = defineComponent(
|
|
9
|
+
'GlobalTransform',
|
|
10
|
+
{
|
|
11
|
+
world: { type: 'array<f32, 16>', default: IDENTITY_MAT4 },
|
|
12
|
+
},
|
|
13
|
+
{ transient: true },
|
|
14
|
+
);
|
|
15
|
+
|
|
34
16
|
/**
|
|
35
|
-
*
|
|
36
|
-
* component order [x, y, z, w]), scale `scale` (xyz), plus the resolved
|
|
37
|
-
* `world` mat4 (column-major 16 floats).
|
|
38
|
-
*
|
|
39
|
-
* Local TRS is stored as three inline stride-N array<f32, N> columns. The
|
|
40
|
-
* propagate kernel composes each entity's local TRS into a mat4 and writes it
|
|
41
|
-
* into the `world` column (root: world = local; child: world = parent.world x
|
|
42
|
-
* local) using `@forgeax/engine-math` mat4 / vec3 / quat APIs (charter P4: do
|
|
43
|
-
* not reinvent math). MVP recomposes every frame; dirty-flag optimization is
|
|
44
|
-
* owned by feat-future-render-world.
|
|
45
|
-
*
|
|
46
|
-
* The `world` column is a fixed-capacity `array<f32, 16>` (feat-20260602):
|
|
47
|
-
* the 16 contiguous floats live inline in a stride-16 column -- no BufferPool
|
|
48
|
-
* slot. Read it via `world.get(e, Transform).world` which returns a live
|
|
49
|
-
* `Float32Array` aliasing the column buffer. The view is transient: it aliases
|
|
50
|
-
* the archetype column buffer and is valid only until the next structural
|
|
51
|
-
* change (spawn / despawn / addComponent / removeComponent). Re-fetch the view
|
|
52
|
-
* on every access; holding a view across a structural change is undefined
|
|
53
|
-
* behaviour (the backing `ArrayBuffer` is detached on column growth, and
|
|
54
|
-
* swap-remove at the same row index points to the wrong entity). All existing
|
|
55
|
-
* hot paths (propagate / render-extract / pick) already comply -- see
|
|
56
|
-
* `packages/ecs/README.md` Transient view contract section.
|
|
57
|
-
*
|
|
58
|
-
* All three local columns carry explicit layer-2 defaults (identity
|
|
59
|
-
* transform): `pos: [0, 0, 0]`, `quat: [0, 0, 0, 1]` (identity quaternion,
|
|
60
|
-
* [x, y, z, w]), `scale: [1, 1, 1]`. quat and scale MUST stay explicit: the
|
|
61
|
-
* layer-3 fallback for array<f32, N> is all-zero, which would land an invalid
|
|
62
|
-
* zero quaternion / zero scale. The `world` field defaults to the identity
|
|
63
|
-
* mat4. AI users spawn with `data: {}` or with only the fields they need to
|
|
64
|
-
* override.
|
|
65
|
-
*
|
|
66
|
-
* @example Minimal spawn (local defaulted to identity, world = identity mat4):
|
|
67
|
-
* world.spawn({ component: Transform, data: {} });
|
|
68
|
-
*
|
|
69
|
-
* @example Override only the position, leaving rotation/scale at identity:
|
|
70
|
-
* world.spawn({ component: Transform, data: { pos: [0, 6, 0] } });
|
|
17
|
+
* Authored local position, rotation and scale columns.
|
|
71
18
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* pos: [1, 2, 3],
|
|
75
|
-
* quat: [0, 0, 0, 1], // [x, y, z, w]
|
|
76
|
-
* scale: [1, 1, 1],
|
|
77
|
-
* } });
|
|
19
|
+
* The ECS `requires` declaration is the generic structural invariant: callers
|
|
20
|
+
* add `Transform`, while `GlobalTransform` is materialized once at spawn/add.
|
|
78
21
|
*/
|
|
79
|
-
export const Transform = defineComponent(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
22
|
+
export const Transform = defineComponent(
|
|
23
|
+
'Transform',
|
|
24
|
+
{
|
|
25
|
+
pos: { type: 'array<f32, 3>', default: new Float32Array([0, 0, 0]) },
|
|
26
|
+
// Component order [x, y, z, w] is shared with glTF.
|
|
27
|
+
quat: { type: 'array<f32, 4>', default: new Float32Array([0, 0, 0, 1]) },
|
|
28
|
+
scale: { type: 'array<f32, 3>', default: new Float32Array([1, 1, 1]) },
|
|
29
|
+
},
|
|
30
|
+
{ requires: [GlobalTransform] },
|
|
31
|
+
);
|
package/src/errors.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityHandle } from '@forgeax/engine-ecs';
|
|
1
|
+
import type { EcsError, EntityHandle } from '@forgeax/engine-ecs';
|
|
2
2
|
|
|
3
3
|
export type SceneErrorCode = 'hierarchy-broken' | 'hierarchy-cycle';
|
|
4
4
|
|
|
@@ -7,11 +7,35 @@ export type SceneInstanceErrorCode = 'component-not-defined' | 'scene-override-t
|
|
|
7
7
|
|
|
8
8
|
export { ComponentNotDefinedError } from '@forgeax/engine-ecs/projection';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/** The structured ECS failure retained by a Scene derived-write diagnostic. */
|
|
11
|
+
export interface SceneErrorCause {
|
|
12
|
+
readonly code: EcsError['code'];
|
|
13
|
+
readonly expected?: string;
|
|
14
|
+
readonly hint?: string;
|
|
15
|
+
readonly detail?: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Location detail shared by hierarchy diagnostics and derived-write errors. */
|
|
19
|
+
export interface SceneHierarchyErrorDetail {
|
|
20
|
+
readonly kind?: 'hierarchy';
|
|
11
21
|
readonly entity: EntityHandle;
|
|
12
22
|
readonly parent: EntityHandle;
|
|
13
23
|
}
|
|
14
24
|
|
|
25
|
+
/** A flat derived publication failure with its original ECS error intact. */
|
|
26
|
+
export interface SceneDerivedWriteErrorDetail {
|
|
27
|
+
readonly kind: 'derived-write';
|
|
28
|
+
readonly entity: EntityHandle;
|
|
29
|
+
readonly parent: EntityHandle;
|
|
30
|
+
readonly bindingIndex: number;
|
|
31
|
+
readonly base: number;
|
|
32
|
+
readonly start: number;
|
|
33
|
+
readonly count: number;
|
|
34
|
+
readonly cause: SceneErrorCause;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SceneErrorDetail = SceneHierarchyErrorDetail | SceneDerivedWriteErrorDetail;
|
|
38
|
+
|
|
15
39
|
export class SceneError extends Error {
|
|
16
40
|
readonly code: SceneErrorCode;
|
|
17
41
|
readonly expected: string;
|
package/src/index.ts
CHANGED
|
@@ -4,13 +4,21 @@ export { ChildOf } from './components/child-of';
|
|
|
4
4
|
export { Children } from './components/children';
|
|
5
5
|
export { MorphWeights } from './components/morph-weights';
|
|
6
6
|
export { Name } from './components/name';
|
|
7
|
-
export { Transform } from './components/transform';
|
|
7
|
+
export { GlobalTransform, Transform } from './components/transform';
|
|
8
8
|
export {
|
|
9
9
|
ComponentNotDefinedError,
|
|
10
10
|
SceneError,
|
|
11
11
|
type SceneErrorCode,
|
|
12
12
|
type SceneInstanceErrorCode,
|
|
13
13
|
} from './errors';
|
|
14
|
+
export {
|
|
15
|
+
resolveSceneEntity,
|
|
16
|
+
type SceneBindingDeclarationError,
|
|
17
|
+
type SceneBindingError,
|
|
18
|
+
type SceneEntityRef,
|
|
19
|
+
sceneEntity,
|
|
20
|
+
validateSceneBindings,
|
|
21
|
+
} from './instances/binding';
|
|
14
22
|
export { SCENE_COLLECT_PROFILE, type SceneCollectProfile } from './instances/collect-profile';
|
|
15
23
|
export {
|
|
16
24
|
type ExternalizedSceneAsset,
|
|
@@ -44,6 +52,7 @@ export {
|
|
|
44
52
|
worldRemoveSceneOverride,
|
|
45
53
|
worldResolveMountSource,
|
|
46
54
|
worldResolveSceneAsset,
|
|
55
|
+
worldResolveSceneEntity,
|
|
47
56
|
worldResolveSceneInstanceStatePayload,
|
|
48
57
|
worldSetSceneAssetResolver,
|
|
49
58
|
worldSetSceneOverride,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { EntityHandle } from '@forgeax/engine-ecs';
|
|
2
|
+
import type { SceneEntityRef } from '@forgeax/engine-types';
|
|
3
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
4
|
+
|
|
5
|
+
export type { SceneEntityRef } from '@forgeax/engine-types';
|
|
6
|
+
|
|
7
|
+
export type SceneBindingError = {
|
|
8
|
+
readonly code: 'scene-binding-missing' | 'scene-binding-wrong-instance';
|
|
9
|
+
readonly expected: string;
|
|
10
|
+
readonly hint: string;
|
|
11
|
+
readonly detail: { readonly sceneSourceKey: string; readonly bindingKey: string };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type SceneBindingDeclarationError = {
|
|
15
|
+
readonly code: 'scene-binding-duplicate' | 'scene-binding-source-missing';
|
|
16
|
+
readonly expected: string;
|
|
17
|
+
readonly hint: string;
|
|
18
|
+
readonly detail: { readonly sceneSourceKey?: string; readonly bindingKey?: string };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function validateSceneBindings(
|
|
22
|
+
sceneSourceKey: string,
|
|
23
|
+
bindingKeys: readonly string[],
|
|
24
|
+
): Result<readonly string[], SceneBindingDeclarationError> {
|
|
25
|
+
if (sceneSourceKey.length === 0) {
|
|
26
|
+
return err({
|
|
27
|
+
code: 'scene-binding-source-missing',
|
|
28
|
+
expected: 'a non-empty scene sourceKey',
|
|
29
|
+
hint: 'declare the scene sourceKey in the author inventory',
|
|
30
|
+
detail: {},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const seen = new Set<string>();
|
|
34
|
+
for (const bindingKey of bindingKeys) {
|
|
35
|
+
if (bindingKey.length === 0 || seen.has(bindingKey)) {
|
|
36
|
+
return err({
|
|
37
|
+
code: 'scene-binding-duplicate',
|
|
38
|
+
expected: 'unique non-empty bindingKey values within one scene',
|
|
39
|
+
hint: 'rename the duplicate bindingKey in the scene producer',
|
|
40
|
+
detail: { sceneSourceKey, bindingKey },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
seen.add(bindingKey);
|
|
44
|
+
}
|
|
45
|
+
return ok([...bindingKeys]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function sceneEntity(sceneSourceKey: string, bindingKey: string): SceneEntityRef {
|
|
49
|
+
return { sceneSourceKey, bindingKey };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveSceneEntity(
|
|
53
|
+
ref: SceneEntityRef,
|
|
54
|
+
instance: {
|
|
55
|
+
readonly sceneSourceKey: string;
|
|
56
|
+
readonly bindings: ReadonlyMap<string, EntityHandle | number>;
|
|
57
|
+
},
|
|
58
|
+
): Result<EntityHandle | number, SceneBindingError> {
|
|
59
|
+
if (ref.sceneSourceKey !== instance.sceneSourceKey) {
|
|
60
|
+
return err({
|
|
61
|
+
code: 'scene-binding-wrong-instance',
|
|
62
|
+
expected: `scene instance ${ref.sceneSourceKey}`,
|
|
63
|
+
hint: 'resolve the SceneEntityRef against its owning SceneInstance',
|
|
64
|
+
detail: { sceneSourceKey: ref.sceneSourceKey, bindingKey: ref.bindingKey },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const value = instance.bindings.get(ref.bindingKey);
|
|
68
|
+
if (value === undefined) {
|
|
69
|
+
return err({
|
|
70
|
+
code: 'scene-binding-missing',
|
|
71
|
+
expected: 'bindingKey declared by the scene producer',
|
|
72
|
+
hint: 'declare the bindingKey in the scene producer before consuming it',
|
|
73
|
+
detail: { sceneSourceKey: ref.sceneSourceKey, bindingKey: ref.bindingKey },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return ok(value);
|
|
77
|
+
}
|
|
@@ -91,7 +91,11 @@ export function externalizeSceneAsset(
|
|
|
91
91
|
components[componentName] = fields;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
localId: entity.localId as number,
|
|
96
|
+
...(entity.bindingKey === undefined ? {} : { bindingKey: entity.bindingKey }),
|
|
97
|
+
components,
|
|
98
|
+
};
|
|
95
99
|
});
|
|
96
100
|
|
|
97
101
|
const mounts = scene.mounts?.map((mount) => {
|
|
@@ -126,6 +130,8 @@ export function externalizeSceneAsset(
|
|
|
126
130
|
}
|
|
127
131
|
return ok({
|
|
128
132
|
payload: {
|
|
133
|
+
kind: 'scene',
|
|
134
|
+
...(scene.sourceKey === undefined ? {} : { sourceKey: scene.sourceKey }),
|
|
129
135
|
entities,
|
|
130
136
|
...(mounts === undefined || mounts.length === 0 ? {} : { mounts }),
|
|
131
137
|
...(scene.skinGuids === undefined
|