@forgeax/engine-scene 0.1.20 → 0.1.23
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 +37 -9
- 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__/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 +15 -8
- package/dist/components/child-of.d.ts.map +1 -1
- 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 +616 -372
- 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 +5 -17
- 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 +1 -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__/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__/plugin.integration.test.ts +2 -2
- package/src/__tests__/propagation.unit.test.ts +207 -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 +15 -8
- package/src/components/children.ts +6 -0
- 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 +47 -76
- 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 +376 -245
- 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
|
@@ -1,313 +1,443 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defineSystem,
|
|
3
3
|
defineSystemSet,
|
|
4
|
+
type EcsError,
|
|
4
5
|
type EntityHandle,
|
|
5
6
|
FixedUpdate,
|
|
7
|
+
type Query,
|
|
6
8
|
type SystemHandle,
|
|
7
9
|
Update,
|
|
8
10
|
type World,
|
|
9
11
|
} from '@forgeax/engine-ecs';
|
|
10
|
-
import {
|
|
11
|
-
createWorldProjection,
|
|
12
|
-
setDerivedComponent,
|
|
13
|
-
type WorldProjection,
|
|
14
|
-
} from '@forgeax/engine-ecs/projection';
|
|
15
12
|
import { type Mat4, mat4 } from '@forgeax/engine-math';
|
|
16
13
|
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
17
14
|
import { ChildOf } from '../components/child-of';
|
|
18
|
-
import { Transform } from '../components/transform';
|
|
15
|
+
import { GlobalTransform, Transform } from '../components/transform';
|
|
19
16
|
import { SceneError } from '../errors';
|
|
20
|
-
import { projectHierarchy, type SceneHierarchySnapshot } from './hierarchy-projection';
|
|
21
17
|
|
|
22
18
|
export const PROPAGATE_TRANSFORMS_SYSTEM = 'propagateTransforms' as const;
|
|
23
19
|
export const PROPAGATE_TRANSFORMS_FIXED_SYSTEM = 'propagateTransformsFixed' as const;
|
|
24
20
|
export const TransformSet = defineSystemSet({ name: 'transform' });
|
|
25
21
|
export const TransformFixedSet = defineSystemSet({ name: 'transform-fixed' });
|
|
26
22
|
|
|
27
|
-
interface
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
readonly entities: ReadonlySet<EntityHandle>;
|
|
39
|
-
readonly locals: Map<EntityHandle, LocalState>;
|
|
40
|
-
readonly derived: Map<EntityHandle, Float32Array>;
|
|
41
|
-
result: Result<void, SceneError>;
|
|
23
|
+
interface Scratch {
|
|
24
|
+
position: Float32Array;
|
|
25
|
+
rotation: Float32Array;
|
|
26
|
+
scale: Float32Array;
|
|
27
|
+
local: Mat4;
|
|
28
|
+
hierarchyPath: number[];
|
|
29
|
+
flatQuery?: FlatQuery;
|
|
30
|
+
outputQuery?: OutputQuery;
|
|
31
|
+
hierarchyQuery?: HierarchyQuery;
|
|
32
|
+
missingGlobalQuery?: MissingGlobalQuery;
|
|
33
|
+
missingTransformQuery?: MissingTransformQuery;
|
|
42
34
|
}
|
|
43
35
|
|
|
44
|
-
|
|
36
|
+
type OutputQuery = Query<readonly [], readonly [typeof GlobalTransform]>;
|
|
37
|
+
type FlatQuery = Query<readonly [typeof Transform], readonly [typeof GlobalTransform]>;
|
|
38
|
+
type HierarchyQuery = Query<readonly [typeof Transform, typeof ChildOf]>;
|
|
39
|
+
type MissingGlobalQuery = Query<readonly [], readonly [], readonly []>;
|
|
40
|
+
type MissingTransformQuery = Query<readonly [], readonly [], readonly []>;
|
|
45
41
|
|
|
46
|
-
interface
|
|
42
|
+
interface RegistrationLease {
|
|
47
43
|
refs: number;
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// shared transform system from another owner. This is lifecycle bookkeeping,
|
|
53
|
-
// not a second component/system authority; the World schedule remains the
|
|
54
|
-
// source of truth.
|
|
55
|
-
const REGISTRATION_LEASES = new WeakMap<World, TransformRegistrationLease>();
|
|
56
|
-
|
|
57
|
-
function copyState(value: {
|
|
58
|
-
readonly pos: ArrayLike<number>;
|
|
59
|
-
readonly quat: ArrayLike<number>;
|
|
60
|
-
readonly scale: ArrayLike<number>;
|
|
61
|
-
}): LocalState {
|
|
62
|
-
return {
|
|
63
|
-
pos: new Float32Array(value.pos),
|
|
64
|
-
quat: new Float32Array(value.quat),
|
|
65
|
-
scale: new Float32Array(value.scale),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function sameArray(left: ArrayLike<number>, right: ArrayLike<number>): boolean {
|
|
70
|
-
if (left.length !== right.length) return false;
|
|
71
|
-
for (let index = 0; index < left.length; index += 1) {
|
|
72
|
-
if (left[index] !== right[index]) return false;
|
|
73
|
-
}
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
46
|
+
const SCRATCH = new WeakMap<World, Scratch>();
|
|
47
|
+
const REGISTRATION_LEASES = new WeakMap<World, RegistrationLease>();
|
|
76
48
|
|
|
77
|
-
function
|
|
78
|
-
return (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
49
|
+
function pairError(entity: EntityHandle, expected: string): Result<void, SceneError> {
|
|
50
|
+
return err(
|
|
51
|
+
new SceneError({
|
|
52
|
+
code: 'hierarchy-broken',
|
|
53
|
+
expected,
|
|
54
|
+
hint: 'attach both Transform and GlobalTransform at scene authoring or import time, then retry propagation',
|
|
55
|
+
detail: { entity, parent: entity },
|
|
56
|
+
}),
|
|
83
57
|
);
|
|
84
58
|
}
|
|
85
59
|
|
|
86
|
-
function
|
|
87
|
-
|
|
60
|
+
function ensureQueries(world: World, scratch: Scratch): Result<void, SceneError> {
|
|
61
|
+
if (
|
|
62
|
+
scratch.flatQuery !== undefined &&
|
|
63
|
+
scratch.outputQuery !== undefined &&
|
|
64
|
+
scratch.hierarchyQuery !== undefined &&
|
|
65
|
+
scratch.missingGlobalQuery !== undefined &&
|
|
66
|
+
scratch.missingTransformQuery !== undefined
|
|
67
|
+
) {
|
|
68
|
+
return ok(undefined);
|
|
69
|
+
}
|
|
70
|
+
const flatOutput = world.query({
|
|
71
|
+
read: [Transform],
|
|
72
|
+
write: [GlobalTransform],
|
|
73
|
+
without: [ChildOf],
|
|
74
|
+
changed: [Transform],
|
|
75
|
+
});
|
|
76
|
+
const output = world.query({ write: [GlobalTransform] });
|
|
77
|
+
const hierarchy = world.query({ read: [Transform, ChildOf] });
|
|
78
|
+
const missingGlobal = world.query({ with: [Transform], without: [GlobalTransform] });
|
|
79
|
+
const missingTransform = world.query({ with: [GlobalTransform], without: [Transform] });
|
|
80
|
+
if (!flatOutput.ok || !output.ok || !hierarchy.ok || !missingGlobal.ok || !missingTransform.ok) {
|
|
81
|
+
return pairError(0 as EntityHandle, 'valid Transform and GlobalTransform pair queries');
|
|
82
|
+
}
|
|
83
|
+
scratch.flatQuery = flatOutput.value as FlatQuery;
|
|
84
|
+
scratch.outputQuery = output.value as OutputQuery;
|
|
85
|
+
scratch.hierarchyQuery = hierarchy.value as HierarchyQuery;
|
|
86
|
+
scratch.missingGlobalQuery = missingGlobal.value as MissingGlobalQuery;
|
|
87
|
+
scratch.missingTransformQuery = missingTransform.value as MissingTransformQuery;
|
|
88
|
+
return ok(undefined);
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
)
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
function validateTransformPairs(world: World, scratch: Scratch): Result<void, SceneError> {
|
|
92
|
+
const queryResult = ensureQueries(world, scratch);
|
|
93
|
+
if (!queryResult.ok) return queryResult;
|
|
94
|
+
const missingGlobal = scratch.missingGlobalQuery;
|
|
95
|
+
const missingTransform = scratch.missingTransformQuery;
|
|
96
|
+
if (missingGlobal === undefined || missingTransform === undefined) {
|
|
97
|
+
return pairError(0 as EntityHandle, 'valid Transform and GlobalTransform pair queries');
|
|
98
|
+
}
|
|
99
|
+
for (const row of missingGlobal) {
|
|
100
|
+
return pairError(row.entity, 'each Transform entity to carry a GlobalTransform pair');
|
|
98
101
|
}
|
|
99
|
-
|
|
102
|
+
for (const row of missingTransform) {
|
|
103
|
+
return pairError(row.entity, 'each GlobalTransform entity to carry a Transform pair');
|
|
104
|
+
}
|
|
105
|
+
return ok(undefined);
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
pending.push(child);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return affected;
|
|
108
|
+
function scratchFor(world: World): Scratch {
|
|
109
|
+
const existing = SCRATCH.get(world);
|
|
110
|
+
if (existing !== undefined) return existing;
|
|
111
|
+
const created = {
|
|
112
|
+
position: new Float32Array(3),
|
|
113
|
+
rotation: new Float32Array(4),
|
|
114
|
+
scale: new Float32Array(3),
|
|
115
|
+
local: mat4.create(),
|
|
116
|
+
hierarchyPath: [] as number[],
|
|
117
|
+
};
|
|
118
|
+
SCRATCH.set(world, created);
|
|
119
|
+
return created;
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
function composeColumns(
|
|
123
|
+
position: ArrayLike<number>,
|
|
124
|
+
rotation: ArrayLike<number>,
|
|
125
|
+
scale: ArrayLike<number>,
|
|
126
|
+
out: Mat4,
|
|
127
|
+
scratch: Scratch,
|
|
128
|
+
positionStart = 0,
|
|
129
|
+
rotationStart = 0,
|
|
130
|
+
): void {
|
|
131
|
+
scratch.position[0] = position[positionStart] ?? 0;
|
|
132
|
+
scratch.position[1] = position[positionStart + 1] ?? 0;
|
|
133
|
+
scratch.position[2] = position[positionStart + 2] ?? 0;
|
|
134
|
+
scratch.rotation[0] = rotation[rotationStart] ?? 0;
|
|
135
|
+
scratch.rotation[1] = rotation[rotationStart + 1] ?? 0;
|
|
136
|
+
scratch.rotation[2] = rotation[rotationStart + 2] ?? 0;
|
|
137
|
+
scratch.rotation[3] = rotation[rotationStart + 3] ?? 1;
|
|
138
|
+
scratch.scale[0] = scale[positionStart] ?? 1;
|
|
139
|
+
scratch.scale[1] = scale[positionStart + 1] ?? 1;
|
|
140
|
+
scratch.scale[2] = scale[positionStart + 2] ?? 1;
|
|
141
|
+
mat4.compose(out, scratch.position, scratch.rotation, scratch.scale);
|
|
131
142
|
}
|
|
132
143
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
function composeFlatColumns(
|
|
145
|
+
positions: ArrayLike<number>,
|
|
146
|
+
rotations: ArrayLike<number>,
|
|
147
|
+
scales: ArrayLike<number>,
|
|
148
|
+
worlds: Float32Array,
|
|
149
|
+
count: number,
|
|
150
|
+
): void {
|
|
151
|
+
for (let row = 0; row < count; row += 1) {
|
|
152
|
+
const position = row * 3;
|
|
153
|
+
const rotation = row * 4;
|
|
154
|
+
const world = row * 16;
|
|
155
|
+
const x = rotations[rotation] ?? 0;
|
|
156
|
+
const y = rotations[rotation + 1] ?? 0;
|
|
157
|
+
const z = rotations[rotation + 2] ?? 0;
|
|
158
|
+
const w = rotations[rotation + 3] ?? 1;
|
|
159
|
+
const x2 = x + x;
|
|
160
|
+
const y2 = y + y;
|
|
161
|
+
const z2 = z + z;
|
|
162
|
+
const xx = x * x2;
|
|
163
|
+
const xy = x * y2;
|
|
164
|
+
const xz = x * z2;
|
|
165
|
+
const yy = y * y2;
|
|
166
|
+
const yz = y * z2;
|
|
167
|
+
const zz = z * z2;
|
|
168
|
+
const wx = w * x2;
|
|
169
|
+
const wy = w * y2;
|
|
170
|
+
const wz = w * z2;
|
|
171
|
+
const sx = scales[position] ?? 1;
|
|
172
|
+
const sy = scales[position + 1] ?? 1;
|
|
173
|
+
const sz = scales[position + 2] ?? 1;
|
|
174
|
+
|
|
175
|
+
worlds[world] = (1 - (yy + zz)) * sx;
|
|
176
|
+
worlds[world + 1] = (xy + wz) * sx;
|
|
177
|
+
worlds[world + 2] = (xz - wy) * sx;
|
|
178
|
+
worlds[world + 3] = 0;
|
|
179
|
+
worlds[world + 4] = (xy - wz) * sy;
|
|
180
|
+
worlds[world + 5] = (1 - (xx + zz)) * sy;
|
|
181
|
+
worlds[world + 6] = (yz + wx) * sy;
|
|
182
|
+
worlds[world + 7] = 0;
|
|
183
|
+
worlds[world + 8] = (xz + wy) * sz;
|
|
184
|
+
worlds[world + 9] = (yz - wx) * sz;
|
|
185
|
+
worlds[world + 10] = (1 - (xx + yy)) * sz;
|
|
186
|
+
worlds[world + 11] = 0;
|
|
187
|
+
worlds[world + 12] = positions[position] ?? 0;
|
|
188
|
+
worlds[world + 13] = positions[position + 1] ?? 0;
|
|
189
|
+
worlds[world + 14] = positions[position + 2] ?? 0;
|
|
190
|
+
worlds[world + 15] = 1;
|
|
145
191
|
}
|
|
146
|
-
return {
|
|
147
|
-
projection,
|
|
148
|
-
hierarchy,
|
|
149
|
-
parentOf: hierarchy.parentOf,
|
|
150
|
-
childrenOf: indexChildren(hierarchy),
|
|
151
|
-
entities,
|
|
152
|
-
locals,
|
|
153
|
-
derived: new Map(),
|
|
154
|
-
result: ok(undefined),
|
|
155
|
-
};
|
|
156
192
|
}
|
|
157
193
|
|
|
158
|
-
function
|
|
194
|
+
function writeWorld(
|
|
159
195
|
world: World,
|
|
196
|
+
output: ReturnType<World['query']> extends Result<infer Q, unknown> ? Q : never,
|
|
160
197
|
entity: EntityHandle,
|
|
161
|
-
|
|
162
|
-
affected: ReadonlySet<EntityHandle>,
|
|
163
|
-
visiting: Set<EntityHandle>,
|
|
164
|
-
published: Set<EntityHandle>,
|
|
198
|
+
value: Mat4,
|
|
165
199
|
): Result<void, SceneError> {
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const parentResult = deriveEntity(world, parent, cache, affected, visiting, published);
|
|
177
|
-
if (!parentResult.ok) return parentResult;
|
|
200
|
+
const row = output.at(entity);
|
|
201
|
+
if (row === undefined) {
|
|
202
|
+
return err(
|
|
203
|
+
new SceneError({
|
|
204
|
+
code: 'hierarchy-broken',
|
|
205
|
+
expected: 'each Transform entity to carry a GlobalTransform pair',
|
|
206
|
+
hint: 'attach GlobalTransform at scene authoring or import time, then retry propagation',
|
|
207
|
+
detail: { entity, parent: entity },
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
178
210
|
}
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
const parentWorld = parent === undefined ? undefined : cache.derived.get(parent);
|
|
182
|
-
const resolved = mat4.create();
|
|
183
|
-
if (parentWorld === undefined) resolved.set(localWorld);
|
|
184
|
-
else mat4.multiply(resolved, parentWorld, localWorld);
|
|
185
|
-
const write = setDerivedComponent(world, entity, Transform, { world: resolved });
|
|
186
|
-
if (!write.ok) {
|
|
187
|
-
visiting.delete(entity);
|
|
211
|
+
const current = world.get(entity, GlobalTransform);
|
|
212
|
+
if (!current.ok)
|
|
188
213
|
return err(
|
|
189
214
|
new SceneError({
|
|
190
215
|
code: 'hierarchy-broken',
|
|
191
|
-
expected: '
|
|
192
|
-
hint: '
|
|
216
|
+
expected: 'each Transform entity to carry a GlobalTransform pair',
|
|
217
|
+
hint: 'attach GlobalTransform at scene authoring or import time, then retry propagation',
|
|
193
218
|
detail: { entity, parent: entity },
|
|
194
219
|
}),
|
|
195
220
|
);
|
|
221
|
+
let changed = false;
|
|
222
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
223
|
+
if (current.value.world[index] !== value[index]) {
|
|
224
|
+
changed = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
196
227
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
visiting.delete(entity);
|
|
228
|
+
if (!changed) return ok(undefined);
|
|
229
|
+
row.mut(GlobalTransform).world.set(value);
|
|
200
230
|
return ok(undefined);
|
|
201
231
|
}
|
|
202
232
|
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
);
|
|
233
|
+
function propagateFlat(scratch: Scratch): Result<void, SceneError> {
|
|
234
|
+
const query = scratch.flatQuery;
|
|
235
|
+
if (query === undefined)
|
|
236
|
+
return err(
|
|
237
|
+
new SceneError({
|
|
238
|
+
code: 'hierarchy-broken',
|
|
239
|
+
expected: 'a valid changed Transform write query',
|
|
240
|
+
hint: 'register the scene components before running TransformPropagation',
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
const spans = query.spans();
|
|
244
|
+
if (!spans.ok) return pairError(0 as EntityHandle, 'dense numeric Transform spans');
|
|
245
|
+
let bindingIndex = 0;
|
|
246
|
+
try {
|
|
247
|
+
for (const span of spans.value) {
|
|
248
|
+
const local = span.get(Transform);
|
|
249
|
+
const world = span.mut(GlobalTransform).world;
|
|
250
|
+
composeFlatColumns(local.pos, local.quat, local.scale, world, span.length);
|
|
251
|
+
bindingIndex += 1;
|
|
252
|
+
}
|
|
253
|
+
} catch (cause) {
|
|
254
|
+
const error = cause as EcsError;
|
|
255
|
+
return err(
|
|
256
|
+
new SceneError({
|
|
257
|
+
code: 'hierarchy-broken',
|
|
258
|
+
expected: 'derived GlobalTransform range publication to succeed',
|
|
259
|
+
hint: error.hint,
|
|
260
|
+
detail: {
|
|
261
|
+
kind: 'derived-write',
|
|
262
|
+
entity: 0 as EntityHandle,
|
|
263
|
+
parent: 0 as EntityHandle,
|
|
264
|
+
bindingIndex,
|
|
265
|
+
base: 0,
|
|
266
|
+
start: 0,
|
|
267
|
+
count: 0,
|
|
268
|
+
cause: error,
|
|
269
|
+
},
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return ok(undefined);
|
|
215
274
|
}
|
|
216
275
|
|
|
217
|
-
|
|
276
|
+
interface HierarchyEntry {
|
|
277
|
+
readonly entity: EntityHandle;
|
|
278
|
+
parent: EntityHandle | undefined;
|
|
279
|
+
readonly position: Float32Array;
|
|
280
|
+
readonly rotation: Float32Array;
|
|
281
|
+
readonly scale: Float32Array;
|
|
282
|
+
readonly world: Mat4;
|
|
283
|
+
state: 0 | 1 | 2;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function findEntry(entries: readonly HierarchyEntry[], entity: EntityHandle): number {
|
|
287
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
288
|
+
if (entries[index]?.entity === entity) return index;
|
|
289
|
+
}
|
|
290
|
+
return -1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function resolveEntry(
|
|
294
|
+
index: number,
|
|
295
|
+
entries: HierarchyEntry[],
|
|
218
296
|
world: World,
|
|
219
|
-
|
|
297
|
+
output: ReturnType<World['query']> extends Result<infer Q, unknown> ? Q : never,
|
|
298
|
+
scratch: Scratch,
|
|
220
299
|
): Result<void, SceneError> {
|
|
221
|
-
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
300
|
+
const entry = entries[index];
|
|
301
|
+
if (entry === undefined || entry.state === 2) return ok(undefined);
|
|
302
|
+
if (entry.state === 1) {
|
|
303
|
+
// A cycle is an input error, but leaving every member on its previous
|
|
304
|
+
// GlobalTransform would make the renderer consume stale derived facts.
|
|
305
|
+
// Cut the complete active cycle segment to roots for this failed frame;
|
|
306
|
+
// the authored ChildOf values remain untouched and the next repaired
|
|
307
|
+
// propagation recomputes the normal hierarchy.
|
|
308
|
+
const cycleParent = entry.parent ?? entry.entity;
|
|
309
|
+
const cycleStart = scratch.hierarchyPath.indexOf(index);
|
|
310
|
+
const start = cycleStart < 0 ? 0 : cycleStart;
|
|
311
|
+
for (let pathIndex = start; pathIndex < scratch.hierarchyPath.length; pathIndex += 1) {
|
|
312
|
+
const cycleEntry = entries[scratch.hierarchyPath[pathIndex] ?? -1];
|
|
313
|
+
if (cycleEntry !== undefined) cycleEntry.parent = undefined;
|
|
234
314
|
}
|
|
235
|
-
|
|
236
|
-
|
|
315
|
+
return err(
|
|
316
|
+
new SceneError({
|
|
317
|
+
code: 'hierarchy-cycle',
|
|
318
|
+
expected: 'a parent-before-child Transform hierarchy',
|
|
319
|
+
hint: 'repair the ChildOf cycle and retry TransformPropagation',
|
|
320
|
+
detail: { entity: entry.entity, parent: cycleParent },
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
237
323
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
324
|
+
entry.state = 1;
|
|
325
|
+
scratch.hierarchyPath.push(index);
|
|
326
|
+
let parentIndex = entry.parent === undefined ? -1 : findEntry(entries, entry.parent);
|
|
327
|
+
let failure: SceneError | undefined;
|
|
328
|
+
if (entry.parent !== undefined && parentIndex < 0) {
|
|
329
|
+
const local = world.get(entry.parent, Transform);
|
|
330
|
+
const global = world.get(entry.parent, GlobalTransform);
|
|
331
|
+
if (!local.ok || !global.ok) {
|
|
332
|
+
// The edge is invalid for this frame. Treat the child as a local root
|
|
333
|
+
// so the output column reflects current authored input instead of the
|
|
334
|
+
// previous frame's world matrix, while still returning the structured
|
|
335
|
+
// error to the caller.
|
|
336
|
+
failure = new SceneError({
|
|
337
|
+
code: 'hierarchy-broken',
|
|
338
|
+
expected: 'each ChildOf parent to carry Transform and GlobalTransform',
|
|
339
|
+
hint: 'repair the missing parent pair before retrying propagation',
|
|
340
|
+
detail: { entity: entry.entity, parent: entry.parent },
|
|
341
|
+
});
|
|
342
|
+
entry.parent = undefined;
|
|
343
|
+
parentIndex = -1;
|
|
344
|
+
} else {
|
|
345
|
+
const relation = world.get(entry.parent, ChildOf);
|
|
346
|
+
entries.push({
|
|
347
|
+
entity: entry.parent,
|
|
348
|
+
parent: relation.ok && relation.value.parent !== null ? relation.value.parent : undefined,
|
|
349
|
+
position: new Float32Array(local.value.pos),
|
|
350
|
+
rotation: new Float32Array(local.value.quat),
|
|
351
|
+
scale: new Float32Array(local.value.scale),
|
|
352
|
+
world: mat4.create(),
|
|
353
|
+
state: 0,
|
|
354
|
+
});
|
|
355
|
+
parentIndex = entries.length - 1;
|
|
252
356
|
}
|
|
253
|
-
cache.result = hierarchyError(cache.hierarchy);
|
|
254
|
-
return cache.result;
|
|
255
357
|
}
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
change.kind === 'component-removed'
|
|
265
|
-
) {
|
|
266
|
-
rebuild = true;
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
269
|
-
if (change.kind !== 'component-changed') continue;
|
|
270
|
-
if (change.component === ChildOf) {
|
|
271
|
-
rebuild = true;
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
// Derived Transform publications are deliberately not dirty seeds. Only
|
|
275
|
-
// authored component-changed records can invalidate local TRS state.
|
|
276
|
-
if (change.component === Transform) transformSeeds.add(change.entity);
|
|
358
|
+
if (parentIndex >= 0) {
|
|
359
|
+
const parentResult = resolveEntry(parentIndex, entries, world, output, scratch);
|
|
360
|
+
if (!parentResult.ok) failure = parentResult.error;
|
|
361
|
+
// A cycle fallback can cut this entry's authored edge while resolving its
|
|
362
|
+
// parent. Re-read the edge before composing so no cycle member consumes an
|
|
363
|
+
// uninitialized parent matrix.
|
|
364
|
+
parentIndex = entry.parent === undefined ? -1 : findEntry(entries, entry.parent);
|
|
365
|
+
if (parentIndex >= 0 && entries[parentIndex]?.state !== 2) parentIndex = -1;
|
|
277
366
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
367
|
+
composeColumns(entry.position, entry.rotation, entry.scale, scratch.local, scratch);
|
|
368
|
+
if (parentIndex >= 0) {
|
|
369
|
+
const parent = entries[parentIndex];
|
|
370
|
+
if (parent !== undefined) mat4.multiply(entry.world, parent.world, scratch.local);
|
|
371
|
+
} else {
|
|
372
|
+
entry.world.set(scratch.local);
|
|
281
373
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
for (const entity of transformSeeds) {
|
|
287
|
-
const current = world.get(entity, Transform);
|
|
288
|
-
if (!current.ok) {
|
|
289
|
-
CACHE.delete(world);
|
|
290
|
-
return propagateTransforms(world);
|
|
291
|
-
}
|
|
292
|
-
const next = copyState(current.value);
|
|
293
|
-
if (!sameState(cache.locals.get(entity), next)) changed.add(entity);
|
|
294
|
-
cache.locals.set(entity, next);
|
|
374
|
+
const write = writeWorld(world, output, entry.entity, entry.world);
|
|
375
|
+
if (!write.ok) {
|
|
376
|
+
scratch.hierarchyPath.pop();
|
|
377
|
+
return write;
|
|
295
378
|
}
|
|
296
|
-
|
|
379
|
+
entry.state = 2;
|
|
380
|
+
scratch.hierarchyPath.pop();
|
|
381
|
+
return failure === undefined ? ok(undefined) : err(failure);
|
|
382
|
+
}
|
|
297
383
|
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
384
|
+
function propagateHierarchy(world: World, scratch: Scratch): Result<void, SceneError> {
|
|
385
|
+
const query = scratch.hierarchyQuery;
|
|
386
|
+
const output = scratch.outputQuery;
|
|
387
|
+
if (query === undefined || output === undefined) {
|
|
388
|
+
return err(
|
|
389
|
+
new SceneError({
|
|
390
|
+
code: 'hierarchy-broken',
|
|
391
|
+
expected: 'paired Transform and GlobalTransform query access',
|
|
392
|
+
hint: 'register the scene components before running TransformPropagation',
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
304
395
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
396
|
+
const entries: HierarchyEntry[] = [];
|
|
397
|
+
for (const row of query) {
|
|
398
|
+
if (findEntry(entries, row.entity) >= 0) continue;
|
|
399
|
+
const local = row.get(Transform);
|
|
400
|
+
const parentValue = row.get(ChildOf).parent;
|
|
401
|
+
const parent = parentValue === null ? undefined : parentValue;
|
|
402
|
+
entries.push({
|
|
403
|
+
entity: row.entity,
|
|
404
|
+
parent,
|
|
405
|
+
position: new Float32Array(local.pos),
|
|
406
|
+
rotation: new Float32Array(local.quat),
|
|
407
|
+
scale: new Float32Array(local.scale),
|
|
408
|
+
world: mat4.create(),
|
|
409
|
+
state: 0,
|
|
410
|
+
});
|
|
308
411
|
}
|
|
309
|
-
|
|
310
|
-
|
|
412
|
+
scratch.hierarchyPath.length = 0;
|
|
413
|
+
let firstError: SceneError | undefined;
|
|
414
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
415
|
+
const result = resolveEntry(index, entries, world, output, scratch);
|
|
416
|
+
if (!result.ok) {
|
|
417
|
+
const currentEntity = Number(result.error.detail?.entity ?? Number.MAX_SAFE_INTEGER);
|
|
418
|
+
if (firstError === undefined) {
|
|
419
|
+
firstError = result.error;
|
|
420
|
+
} else {
|
|
421
|
+
const firstEntity = Number(firstError.detail?.entity ?? Number.MAX_SAFE_INTEGER);
|
|
422
|
+
if (
|
|
423
|
+
currentEntity < firstEntity ||
|
|
424
|
+
(currentEntity === firstEntity && result.error.code.localeCompare(firstError.code) < 0)
|
|
425
|
+
) {
|
|
426
|
+
firstError = result.error;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return firstError === undefined ? ok(undefined) : err(firstError);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function propagateTransforms(world: World): Result<void, SceneError> {
|
|
435
|
+
const scratch = scratchFor(world);
|
|
436
|
+
const pairs = validateTransformPairs(world, scratch);
|
|
437
|
+
if (!pairs.ok) return pairs;
|
|
438
|
+
const flat = propagateFlat(scratch);
|
|
439
|
+
if (!flat.ok) return flat;
|
|
440
|
+
return propagateHierarchy(world, scratch);
|
|
311
441
|
}
|
|
312
442
|
|
|
313
443
|
export const PropagateTransforms: SystemHandle<readonly []> = defineSystem({
|
|
@@ -341,7 +471,7 @@ export function registerPropagateTransforms(
|
|
|
341
471
|
world.removeSystem(FixedUpdate, PROPAGATE_TRANSFORMS_FIXED_SYSTEM);
|
|
342
472
|
world.removeSystem(Update, PROPAGATE_TRANSFORMS_SYSTEM);
|
|
343
473
|
REGISTRATION_LEASES.delete(world);
|
|
344
|
-
|
|
474
|
+
SCRATCH.delete(world);
|
|
345
475
|
}
|
|
346
476
|
};
|
|
347
477
|
}
|
|
@@ -360,17 +490,18 @@ export function registerPropagateTransforms(
|
|
|
360
490
|
.unwrap();
|
|
361
491
|
}
|
|
362
492
|
world.addSystems(FixedUpdate, TransformFixedSet, [PropagateTransformsFixed]).unwrap();
|
|
363
|
-
|
|
364
|
-
REGISTRATION_LEASES.set(world, lease);
|
|
493
|
+
REGISTRATION_LEASES.set(world, { refs: 1 });
|
|
365
494
|
let active = true;
|
|
366
495
|
return () => {
|
|
367
496
|
if (!active) return;
|
|
368
497
|
active = false;
|
|
498
|
+
const lease = REGISTRATION_LEASES.get(world);
|
|
499
|
+
if (lease === undefined) return;
|
|
369
500
|
lease.refs -= 1;
|
|
370
501
|
if (lease.refs !== 0) return;
|
|
371
502
|
world.removeSystem(FixedUpdate, PROPAGATE_TRANSFORMS_FIXED_SYSTEM);
|
|
372
503
|
world.removeSystem(Update, PROPAGATE_TRANSFORMS_SYSTEM);
|
|
373
504
|
REGISTRATION_LEASES.delete(world);
|
|
374
|
-
|
|
505
|
+
SCRATCH.delete(world);
|
|
375
506
|
};
|
|
376
507
|
}
|