@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
|
@@ -1,313 +1,1214 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defineSystem,
|
|
3
3
|
defineSystemSet,
|
|
4
|
+
type EcsError,
|
|
5
|
+
ENTITY_NULL_RAW,
|
|
4
6
|
type EntityHandle,
|
|
5
7
|
FixedUpdate,
|
|
8
|
+
type Query,
|
|
6
9
|
type SystemHandle,
|
|
7
10
|
Update,
|
|
8
11
|
type World,
|
|
9
12
|
} from '@forgeax/engine-ecs';
|
|
10
13
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
type
|
|
14
|
-
|
|
14
|
+
type DerivedColumnBinding,
|
|
15
|
+
type DerivedRangeCursor,
|
|
16
|
+
type DerivedRangeWriter,
|
|
17
|
+
getDerivedWriter,
|
|
18
|
+
worldInternal,
|
|
19
|
+
} from '@forgeax/engine-ecs/internal';
|
|
15
20
|
import { type Mat4, mat4 } from '@forgeax/engine-math';
|
|
16
21
|
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
17
22
|
import { ChildOf } from '../components/child-of';
|
|
18
|
-
import {
|
|
23
|
+
import { Children } from '../components/children';
|
|
24
|
+
import { GlobalTransform, Transform } from '../components/transform';
|
|
19
25
|
import { SceneError } from '../errors';
|
|
20
|
-
import { projectHierarchy, type SceneHierarchySnapshot } from './hierarchy-projection';
|
|
21
26
|
|
|
22
27
|
export const PROPAGATE_TRANSFORMS_SYSTEM = 'propagateTransforms' as const;
|
|
23
28
|
export const PROPAGATE_TRANSFORMS_FIXED_SYSTEM = 'propagateTransformsFixed' as const;
|
|
24
29
|
export const TransformSet = defineSystemSet({ name: 'transform' });
|
|
25
30
|
export const TransformFixedSet = defineSystemSet({ name: 'transform-fixed' });
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Optional, test-owned counters for the parent-first executor. The counters
|
|
34
|
+
* are disabled unless a caller brackets a run with begin/end; production
|
|
35
|
+
* propagation therefore pays only one predictable branch per instrumented
|
|
36
|
+
* event. They are deliberately not a second execution state or a public
|
|
37
|
+
* dirty/cache contract.
|
|
38
|
+
*/
|
|
39
|
+
export interface TransformPropagationTrace {
|
|
40
|
+
hierarchyRootInvocations: number;
|
|
41
|
+
hierarchyRootCursorReuses: number;
|
|
42
|
+
hierarchyRootCursorAllocations: number;
|
|
43
|
+
hierarchyEntityLookups: number;
|
|
44
|
+
hierarchyRowsEvaluated: number;
|
|
45
|
+
hierarchyEdgesVisited: number;
|
|
46
|
+
hierarchyPublishedRows: number;
|
|
47
|
+
hierarchyPublishedRuns: number;
|
|
48
|
+
hierarchyResidualParentProbes: number;
|
|
49
|
+
flatStructuralRootRows: number;
|
|
31
50
|
}
|
|
32
51
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
readonly parentOf: ReadonlyMap<EntityHandle, EntityHandle>;
|
|
37
|
-
readonly childrenOf: ReadonlyMap<EntityHandle, readonly EntityHandle[]>;
|
|
38
|
-
readonly entities: ReadonlySet<EntityHandle>;
|
|
39
|
-
readonly locals: Map<EntityHandle, LocalState>;
|
|
40
|
-
readonly derived: Map<EntityHandle, Float32Array>;
|
|
41
|
-
result: Result<void, SceneError>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const CACHE = new WeakMap<World, PropagationCache>();
|
|
52
|
+
let propagationTrace: TransformPropagationTrace | undefined;
|
|
53
|
+
let hierarchyRootCursorAllocationCount = 0;
|
|
54
|
+
let propagationTraceRootCursorAllocationStart = 0;
|
|
45
55
|
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
function createHierarchyRootCursor(): DerivedRangeCursor {
|
|
57
|
+
hierarchyRootCursorAllocationCount += 1;
|
|
58
|
+
return { bindingIndex: -1, row: -1 };
|
|
48
59
|
}
|
|
49
60
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
pos: new Float32Array(value.pos),
|
|
64
|
-
quat: new Float32Array(value.quat),
|
|
65
|
-
scale: new Float32Array(value.scale),
|
|
61
|
+
export function beginTransformPropagationTrace(): void {
|
|
62
|
+
propagationTraceRootCursorAllocationStart = hierarchyRootCursorAllocationCount;
|
|
63
|
+
propagationTrace = {
|
|
64
|
+
hierarchyRootInvocations: 0,
|
|
65
|
+
hierarchyRootCursorReuses: 0,
|
|
66
|
+
hierarchyRootCursorAllocations: 0,
|
|
67
|
+
hierarchyEntityLookups: 0,
|
|
68
|
+
hierarchyRowsEvaluated: 0,
|
|
69
|
+
hierarchyEdgesVisited: 0,
|
|
70
|
+
hierarchyPublishedRows: 0,
|
|
71
|
+
hierarchyPublishedRuns: 0,
|
|
72
|
+
hierarchyResidualParentProbes: 0,
|
|
73
|
+
flatStructuralRootRows: 0,
|
|
66
74
|
};
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
export function endTransformPropagationTrace(): TransformPropagationTrace {
|
|
78
|
+
const trace = propagationTrace;
|
|
79
|
+
propagationTrace = undefined;
|
|
80
|
+
const rootCursorAllocations =
|
|
81
|
+
hierarchyRootCursorAllocationCount - propagationTraceRootCursorAllocationStart;
|
|
82
|
+
propagationTraceRootCursorAllocationStart = hierarchyRootCursorAllocationCount;
|
|
83
|
+
if (trace !== undefined) {
|
|
84
|
+
trace.hierarchyRootCursorAllocations = rootCursorAllocations;
|
|
85
|
+
return trace;
|
|
73
86
|
}
|
|
74
|
-
return
|
|
87
|
+
return {
|
|
88
|
+
hierarchyRootInvocations: 0,
|
|
89
|
+
hierarchyRootCursorReuses: 0,
|
|
90
|
+
hierarchyRootCursorAllocations: 0,
|
|
91
|
+
hierarchyEntityLookups: 0,
|
|
92
|
+
hierarchyRowsEvaluated: 0,
|
|
93
|
+
hierarchyEdgesVisited: 0,
|
|
94
|
+
hierarchyPublishedRows: 0,
|
|
95
|
+
hierarchyPublishedRuns: 0,
|
|
96
|
+
hierarchyResidualParentProbes: 0,
|
|
97
|
+
flatStructuralRootRows: 0,
|
|
98
|
+
};
|
|
75
99
|
}
|
|
76
100
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
sameArray(left.pos, right.pos) &&
|
|
81
|
-
sameArray(left.quat, right.quat) &&
|
|
82
|
-
sameArray(left.scale, right.scale)
|
|
83
|
-
);
|
|
101
|
+
function countPropagation(name: keyof TransformPropagationTrace): void {
|
|
102
|
+
const trace = propagationTrace;
|
|
103
|
+
if (trace !== undefined) trace[name] += 1;
|
|
84
104
|
}
|
|
85
105
|
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
interface Scratch {
|
|
107
|
+
position: Float32Array;
|
|
108
|
+
rotation: Float32Array;
|
|
109
|
+
scale: Float32Array;
|
|
110
|
+
local: Mat4;
|
|
111
|
+
parent: Mat4;
|
|
112
|
+
candidate: Mat4;
|
|
113
|
+
hierarchyStackEntities: EntityHandle[];
|
|
114
|
+
hierarchyStackChildren: number[];
|
|
115
|
+
hierarchyProbeEntities: EntityHandle[];
|
|
116
|
+
hierarchyCurrentCursor: DerivedRangeCursor;
|
|
117
|
+
hierarchyParentCursor: DerivedRangeCursor;
|
|
118
|
+
hierarchyChildCursor: DerivedRangeCursor;
|
|
119
|
+
hierarchyResidualCursor: DerivedRangeCursor;
|
|
120
|
+
hierarchyResidualParentCursor: DerivedRangeCursor;
|
|
121
|
+
hierarchyRootCursor: DerivedRangeCursor;
|
|
122
|
+
hierarchyStates: Uint8Array[];
|
|
123
|
+
hierarchyChanged: Uint8Array[];
|
|
124
|
+
hierarchyBindingTables: number[];
|
|
125
|
+
hierarchyBindingRows: number[];
|
|
126
|
+
flatChanged: Uint8Array[];
|
|
127
|
+
flatBindingTables: number[];
|
|
128
|
+
flatBindingRows: number[];
|
|
129
|
+
flatStructureEpoch: number;
|
|
130
|
+
flatQuery?: FlatQuery;
|
|
131
|
+
hierarchyQuery?: HierarchyQuery;
|
|
132
|
+
transformQuery?: TransformQuery;
|
|
133
|
+
hierarchyWriter?: HierarchyWriter;
|
|
134
|
+
transformWriter?: TransformWriter;
|
|
135
|
+
missingGlobalQuery?: MissingGlobalQuery;
|
|
136
|
+
missingTransformQuery?: MissingTransformQuery;
|
|
88
137
|
}
|
|
89
138
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
139
|
+
type FlatQuery = Query<readonly [typeof Transform], readonly [typeof GlobalTransform]>;
|
|
140
|
+
type HierarchyQuery = Query<
|
|
141
|
+
readonly [typeof Transform, typeof ChildOf],
|
|
142
|
+
readonly [typeof GlobalTransform]
|
|
143
|
+
>;
|
|
144
|
+
type TransformQuery = Query<readonly [typeof Transform], readonly [typeof GlobalTransform]>;
|
|
145
|
+
type HierarchyWriter = DerivedRangeWriter<
|
|
146
|
+
typeof Transform | typeof ChildOf,
|
|
147
|
+
typeof GlobalTransform
|
|
148
|
+
>;
|
|
149
|
+
type TransformWriter = DerivedRangeWriter<typeof Transform, typeof GlobalTransform>;
|
|
150
|
+
type HierarchyBinding = DerivedColumnBinding<
|
|
151
|
+
typeof Transform | typeof ChildOf,
|
|
152
|
+
typeof GlobalTransform
|
|
153
|
+
>;
|
|
154
|
+
type TransformBinding = DerivedColumnBinding<typeof Transform, typeof GlobalTransform>;
|
|
155
|
+
type MissingGlobalQuery = Query<readonly [], readonly [], readonly []>;
|
|
156
|
+
type MissingTransformQuery = Query<readonly [], readonly [], readonly []>;
|
|
101
157
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
roots: ReadonlySet<EntityHandle>,
|
|
105
|
-
): Set<EntityHandle> {
|
|
106
|
-
const affected = new Set(roots);
|
|
107
|
-
const pending = [...roots];
|
|
108
|
-
for (let index = 0; index < pending.length; index += 1) {
|
|
109
|
-
const parent = pending[index];
|
|
110
|
-
if (parent === undefined) continue;
|
|
111
|
-
for (const child of childrenOf.get(parent) ?? []) {
|
|
112
|
-
if (affected.has(child)) continue;
|
|
113
|
-
affected.add(child);
|
|
114
|
-
pending.push(child);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return affected;
|
|
158
|
+
interface RegistrationLease {
|
|
159
|
+
refs: number;
|
|
118
160
|
}
|
|
119
161
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
162
|
+
const SCRATCH = new WeakMap<World, Scratch>();
|
|
163
|
+
const REGISTRATION_LEASES = new WeakMap<World, RegistrationLease>();
|
|
164
|
+
|
|
165
|
+
function pairError(entity: EntityHandle, expected: string): Result<void, SceneError> {
|
|
123
166
|
return err(
|
|
124
167
|
new SceneError({
|
|
125
|
-
code:
|
|
126
|
-
expected
|
|
127
|
-
hint:
|
|
128
|
-
detail:
|
|
168
|
+
code: 'hierarchy-broken',
|
|
169
|
+
expected,
|
|
170
|
+
hint: 'attach both Transform and GlobalTransform at scene authoring or import time, then retry propagation',
|
|
171
|
+
detail: { entity, parent: entity },
|
|
129
172
|
}),
|
|
130
173
|
);
|
|
131
174
|
}
|
|
132
175
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
176
|
+
function ensureQueries(world: World, scratch: Scratch): Result<void, SceneError> {
|
|
177
|
+
if (
|
|
178
|
+
scratch.flatQuery !== undefined &&
|
|
179
|
+
scratch.hierarchyQuery !== undefined &&
|
|
180
|
+
scratch.transformQuery !== undefined &&
|
|
181
|
+
scratch.hierarchyWriter !== undefined &&
|
|
182
|
+
scratch.transformWriter !== undefined &&
|
|
183
|
+
scratch.missingGlobalQuery !== undefined &&
|
|
184
|
+
scratch.missingTransformQuery !== undefined
|
|
185
|
+
) {
|
|
186
|
+
return ok(undefined);
|
|
145
187
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
188
|
+
const flatOutput = world.query({
|
|
189
|
+
read: [Transform],
|
|
190
|
+
write: [GlobalTransform],
|
|
191
|
+
without: [ChildOf],
|
|
192
|
+
changed: [Transform],
|
|
193
|
+
});
|
|
194
|
+
const hierarchy = world.query({ read: [Transform, ChildOf], write: [GlobalTransform] });
|
|
195
|
+
const transform = world.query({ read: [Transform], write: [GlobalTransform] });
|
|
196
|
+
const missingGlobal = world.query({ with: [Transform], without: [GlobalTransform] });
|
|
197
|
+
const missingTransform = world.query({ with: [GlobalTransform], without: [Transform] });
|
|
198
|
+
if (
|
|
199
|
+
!flatOutput.ok ||
|
|
200
|
+
!hierarchy.ok ||
|
|
201
|
+
!transform.ok ||
|
|
202
|
+
!missingGlobal.ok ||
|
|
203
|
+
!missingTransform.ok
|
|
204
|
+
) {
|
|
205
|
+
return pairError(0 as EntityHandle, 'valid Transform and GlobalTransform pair queries');
|
|
206
|
+
}
|
|
207
|
+
const hierarchyWriter = getDerivedWriter(hierarchy.value, GlobalTransform);
|
|
208
|
+
const transformWriter = getDerivedWriter(transform.value, GlobalTransform);
|
|
209
|
+
if (!hierarchyWriter.ok || !transformWriter.ok) {
|
|
210
|
+
return pairError(0 as EntityHandle, 'dense Transform and ChildOf derived bindings');
|
|
211
|
+
}
|
|
212
|
+
scratch.flatQuery = flatOutput.value as FlatQuery;
|
|
213
|
+
scratch.hierarchyQuery = hierarchy.value as HierarchyQuery;
|
|
214
|
+
scratch.transformQuery = transform.value as TransformQuery;
|
|
215
|
+
scratch.hierarchyWriter = hierarchyWriter.value as HierarchyWriter;
|
|
216
|
+
scratch.transformWriter = transformWriter.value as TransformWriter;
|
|
217
|
+
scratch.missingGlobalQuery = missingGlobal.value as MissingGlobalQuery;
|
|
218
|
+
scratch.missingTransformQuery = missingTransform.value as MissingTransformQuery;
|
|
219
|
+
return ok(undefined);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function validateTransformPairs(world: World, scratch: Scratch): Result<void, SceneError> {
|
|
223
|
+
const queryResult = ensureQueries(world, scratch);
|
|
224
|
+
if (!queryResult.ok) return queryResult;
|
|
225
|
+
const missingGlobal = scratch.missingGlobalQuery;
|
|
226
|
+
const missingTransform = scratch.missingTransformQuery;
|
|
227
|
+
if (missingGlobal === undefined || missingTransform === undefined) {
|
|
228
|
+
return pairError(0 as EntityHandle, 'valid Transform and GlobalTransform pair queries');
|
|
229
|
+
}
|
|
230
|
+
for (const row of missingGlobal) {
|
|
231
|
+
return pairError(row.entity, 'each Transform entity to carry a GlobalTransform pair');
|
|
232
|
+
}
|
|
233
|
+
for (const row of missingTransform) {
|
|
234
|
+
return pairError(row.entity, 'each GlobalTransform entity to carry a Transform pair');
|
|
235
|
+
}
|
|
236
|
+
return ok(undefined);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function scratchFor(world: World): Scratch {
|
|
240
|
+
const existing = SCRATCH.get(world);
|
|
241
|
+
if (existing !== undefined) return existing;
|
|
242
|
+
const created = {
|
|
243
|
+
position: new Float32Array(3),
|
|
244
|
+
rotation: new Float32Array(4),
|
|
245
|
+
scale: new Float32Array(3),
|
|
246
|
+
local: mat4.create(),
|
|
247
|
+
parent: mat4.create(),
|
|
248
|
+
candidate: mat4.create(),
|
|
249
|
+
hierarchyStackEntities: [] as EntityHandle[],
|
|
250
|
+
hierarchyStackChildren: [] as number[],
|
|
251
|
+
hierarchyProbeEntities: [] as EntityHandle[],
|
|
252
|
+
hierarchyCurrentCursor: { bindingIndex: -1, row: -1 },
|
|
253
|
+
hierarchyParentCursor: { bindingIndex: -1, row: -1 },
|
|
254
|
+
hierarchyChildCursor: { bindingIndex: -1, row: -1 },
|
|
255
|
+
hierarchyResidualCursor: { bindingIndex: -1, row: -1 },
|
|
256
|
+
hierarchyResidualParentCursor: { bindingIndex: -1, row: -1 },
|
|
257
|
+
hierarchyRootCursor: createHierarchyRootCursor(),
|
|
258
|
+
hierarchyStates: [],
|
|
259
|
+
hierarchyChanged: [],
|
|
260
|
+
hierarchyBindingTables: [],
|
|
261
|
+
hierarchyBindingRows: [],
|
|
262
|
+
flatChanged: [],
|
|
263
|
+
flatBindingTables: [],
|
|
264
|
+
flatBindingRows: [],
|
|
265
|
+
flatStructureEpoch: -1,
|
|
155
266
|
};
|
|
267
|
+
SCRATCH.set(world, created);
|
|
268
|
+
return created;
|
|
156
269
|
}
|
|
157
270
|
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
271
|
+
function composeColumns(
|
|
272
|
+
position: ArrayLike<number>,
|
|
273
|
+
rotation: ArrayLike<number>,
|
|
274
|
+
scale: ArrayLike<number>,
|
|
275
|
+
out: Mat4,
|
|
276
|
+
scratch: Scratch,
|
|
277
|
+
positionStart = 0,
|
|
278
|
+
rotationStart = 0,
|
|
279
|
+
): void {
|
|
280
|
+
scratch.position[0] = position[positionStart] ?? 0;
|
|
281
|
+
scratch.position[1] = position[positionStart + 1] ?? 0;
|
|
282
|
+
scratch.position[2] = position[positionStart + 2] ?? 0;
|
|
283
|
+
scratch.rotation[0] = rotation[rotationStart] ?? 0;
|
|
284
|
+
scratch.rotation[1] = rotation[rotationStart + 1] ?? 0;
|
|
285
|
+
scratch.rotation[2] = rotation[rotationStart + 2] ?? 0;
|
|
286
|
+
scratch.rotation[3] = rotation[rotationStart + 3] ?? 1;
|
|
287
|
+
scratch.scale[0] = scale[positionStart] ?? 1;
|
|
288
|
+
scratch.scale[1] = scale[positionStart + 1] ?? 1;
|
|
289
|
+
scratch.scale[2] = scale[positionStart + 2] ?? 1;
|
|
290
|
+
mat4.compose(out, scratch.position, scratch.rotation, scratch.scale);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function composeFlatColumns(
|
|
294
|
+
positions: ArrayLike<number>,
|
|
295
|
+
rotations: ArrayLike<number>,
|
|
296
|
+
scales: ArrayLike<number>,
|
|
297
|
+
worlds: Float32Array,
|
|
298
|
+
count: number,
|
|
299
|
+
): void {
|
|
300
|
+
for (let row = 0; row < count; row += 1) {
|
|
301
|
+
const position = row * 3;
|
|
302
|
+
const rotation = row * 4;
|
|
303
|
+
const world = row * 16;
|
|
304
|
+
const x = rotations[rotation] ?? 0;
|
|
305
|
+
const y = rotations[rotation + 1] ?? 0;
|
|
306
|
+
const z = rotations[rotation + 2] ?? 0;
|
|
307
|
+
const w = rotations[rotation + 3] ?? 1;
|
|
308
|
+
const x2 = x + x;
|
|
309
|
+
const y2 = y + y;
|
|
310
|
+
const z2 = z + z;
|
|
311
|
+
const xx = x * x2;
|
|
312
|
+
const xy = x * y2;
|
|
313
|
+
const xz = x * z2;
|
|
314
|
+
const yy = y * y2;
|
|
315
|
+
const yz = y * z2;
|
|
316
|
+
const zz = z * z2;
|
|
317
|
+
const wx = w * x2;
|
|
318
|
+
const wy = w * y2;
|
|
319
|
+
const wz = w * z2;
|
|
320
|
+
const sx = scales[position] ?? 1;
|
|
321
|
+
const sy = scales[position + 1] ?? 1;
|
|
322
|
+
const sz = scales[position + 2] ?? 1;
|
|
323
|
+
|
|
324
|
+
worlds[world] = (1 - (yy + zz)) * sx;
|
|
325
|
+
worlds[world + 1] = (xy + wz) * sx;
|
|
326
|
+
worlds[world + 2] = (xz - wy) * sx;
|
|
327
|
+
worlds[world + 3] = 0;
|
|
328
|
+
worlds[world + 4] = (xy - wz) * sy;
|
|
329
|
+
worlds[world + 5] = (1 - (xx + zz)) * sy;
|
|
330
|
+
worlds[world + 6] = (yz + wx) * sy;
|
|
331
|
+
worlds[world + 7] = 0;
|
|
332
|
+
worlds[world + 8] = (xz + wy) * sz;
|
|
333
|
+
worlds[world + 9] = (yz - wx) * sz;
|
|
334
|
+
worlds[world + 10] = (1 - (xx + yy)) * sz;
|
|
335
|
+
worlds[world + 11] = 0;
|
|
336
|
+
worlds[world + 12] = positions[position] ?? 0;
|
|
337
|
+
worlds[world + 13] = positions[position + 1] ?? 0;
|
|
338
|
+
worlds[world + 14] = positions[position + 2] ?? 0;
|
|
339
|
+
worlds[world + 15] = 1;
|
|
173
340
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const localWorld = mat4.create();
|
|
180
|
-
compose(local, localWorld);
|
|
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);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function propagateFlat(world: World, scratch: Scratch): Result<void, SceneError> {
|
|
344
|
+
const query = scratch.flatQuery;
|
|
345
|
+
if (query === undefined)
|
|
188
346
|
return err(
|
|
189
347
|
new SceneError({
|
|
190
348
|
code: 'hierarchy-broken',
|
|
191
|
-
expected: '
|
|
192
|
-
hint: '
|
|
193
|
-
detail: { entity, parent: entity },
|
|
349
|
+
expected: 'a valid changed Transform write query',
|
|
350
|
+
hint: 'register the scene components before running TransformPropagation',
|
|
194
351
|
}),
|
|
195
352
|
);
|
|
353
|
+
const spans = query.spans();
|
|
354
|
+
if (!spans.ok) return pairError(0 as EntityHandle, 'dense numeric Transform spans');
|
|
355
|
+
let bindingIndex = 0;
|
|
356
|
+
try {
|
|
357
|
+
for (const span of spans.value) {
|
|
358
|
+
const local = span.get(Transform);
|
|
359
|
+
const world = span.mut(GlobalTransform).world;
|
|
360
|
+
composeFlatColumns(local.pos, local.quat, local.scale, world, span.length);
|
|
361
|
+
bindingIndex += 1;
|
|
362
|
+
}
|
|
363
|
+
} catch (cause) {
|
|
364
|
+
const error = cause as EcsError;
|
|
365
|
+
return err(derivedWriteError(error, bindingIndex));
|
|
196
366
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
367
|
+
|
|
368
|
+
// A structural relation change can turn an unchanged Transform row into a
|
|
369
|
+
// flat root (most importantly ChildOf removal). The changed Transform query
|
|
370
|
+
// above cannot observe that transition, so once per structure epoch scan the
|
|
371
|
+
// already-bound numeric Transform rows and publish only differing roots.
|
|
372
|
+
const transformWriter = scratch.transformWriter;
|
|
373
|
+
if (transformWriter === undefined) {
|
|
374
|
+
return pairError(0 as EntityHandle, 'dense Transform and GlobalTransform derived bindings');
|
|
375
|
+
}
|
|
376
|
+
const transformBindings = transformWriter.bindings as readonly TransformBinding[];
|
|
377
|
+
const structureEpoch = world[worldInternal].getStructureEpoch();
|
|
378
|
+
if (scratch.flatStructureEpoch === structureEpoch) return ok(undefined);
|
|
379
|
+
|
|
380
|
+
ensureFlatBuffers(scratch, transformBindings);
|
|
381
|
+
resetFlatBuffers(scratch);
|
|
382
|
+
for (let bindingIndex = 0; bindingIndex < transformBindings.length; bindingIndex += 1) {
|
|
383
|
+
const binding = transformBindings[bindingIndex];
|
|
384
|
+
const changed = scratch.flatChanged[bindingIndex];
|
|
385
|
+
if (binding === undefined || changed === undefined) continue;
|
|
386
|
+
for (let row = 0; row < binding.rowCapacity; row += 1) {
|
|
387
|
+
const entity = (binding.entities[row] ?? 0) as EntityHandle;
|
|
388
|
+
const parentRaw = world[worldInternal].getFieldValue(entity, ChildOf, 'parent');
|
|
389
|
+
if (parentRaw !== undefined && parentRaw !== ENTITY_NULL_RAW) continue;
|
|
390
|
+
countPropagation('flatStructuralRootRows');
|
|
391
|
+
composeBindingRow(binding, row, undefined, 0, scratch, changed);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (let bindingIndex = 0; bindingIndex < transformBindings.length; bindingIndex += 1) {
|
|
395
|
+
const changed = scratch.flatChanged[bindingIndex];
|
|
396
|
+
if (changed === undefined) continue;
|
|
397
|
+
const published = transformWriter.publishChangedRows(bindingIndex, changed);
|
|
398
|
+
if (!published.ok) return err(derivedWriteError(published.error, bindingIndex));
|
|
399
|
+
}
|
|
400
|
+
scratch.flatStructureEpoch = structureEpoch;
|
|
200
401
|
return ok(undefined);
|
|
201
402
|
}
|
|
202
403
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
404
|
+
interface TransformColumnShape {
|
|
405
|
+
readonly pos: ArrayLike<number>;
|
|
406
|
+
readonly quat: ArrayLike<number>;
|
|
407
|
+
readonly scale: ArrayLike<number>;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
interface OutputColumnShape {
|
|
411
|
+
readonly world: Float32Array;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
interface HierarchyColumnShape extends TransformColumnShape {
|
|
415
|
+
readonly parent: ArrayLike<number>;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function transformColumns(binding: TransformBinding | HierarchyBinding): TransformColumnShape {
|
|
419
|
+
return binding.read as unknown as TransformColumnShape;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function hierarchyColumns(binding: HierarchyBinding): HierarchyColumnShape {
|
|
423
|
+
return binding.read as unknown as HierarchyColumnShape;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function worldColumn(binding: TransformBinding | HierarchyBinding): Float32Array {
|
|
427
|
+
return (binding.write as unknown as OutputColumnShape).world;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function hierarchyError(
|
|
431
|
+
code: 'hierarchy-broken' | 'hierarchy-cycle',
|
|
432
|
+
entity: EntityHandle,
|
|
433
|
+
parent: EntityHandle,
|
|
434
|
+
expected: string,
|
|
435
|
+
hint: string,
|
|
436
|
+
): SceneError {
|
|
437
|
+
return new SceneError({ code, expected, hint, detail: { entity, parent } });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function writeCandidate(
|
|
441
|
+
binding: TransformBinding | HierarchyBinding,
|
|
442
|
+
row: number,
|
|
443
|
+
candidate: Mat4,
|
|
444
|
+
changed: Uint8Array,
|
|
445
|
+
): void {
|
|
446
|
+
const worlds = worldColumn(binding);
|
|
447
|
+
const base = row * 16;
|
|
448
|
+
for (let index = 0; index < 16; index += 1) {
|
|
449
|
+
if (worlds[base + index] !== candidate[index]) {
|
|
450
|
+
worlds.set(candidate, base);
|
|
451
|
+
changed[row] = 1;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function composeBindingRow(
|
|
458
|
+
binding: TransformBinding | HierarchyBinding,
|
|
459
|
+
row: number,
|
|
460
|
+
parentBinding: TransformBinding | undefined,
|
|
461
|
+
parentRow: number,
|
|
462
|
+
scratch: Scratch,
|
|
463
|
+
changed: Uint8Array,
|
|
464
|
+
): void {
|
|
465
|
+
// Hierarchy bindings carry the parent column; flat structural-root repair
|
|
466
|
+
// uses the same numeric kernel but is intentionally counted separately.
|
|
467
|
+
if ('parent' in (binding.read as object)) countPropagation('hierarchyRowsEvaluated');
|
|
468
|
+
const local = transformColumns(binding);
|
|
469
|
+
const offset = row * 3;
|
|
470
|
+
composeColumns(local.pos, local.quat, local.scale, scratch.local, scratch, offset, row * 4);
|
|
471
|
+
if (parentBinding === undefined) {
|
|
472
|
+
scratch.candidate.set(scratch.local);
|
|
473
|
+
} else {
|
|
474
|
+
const parentWorld = worldColumn(parentBinding);
|
|
475
|
+
const parentOffset = parentRow * 16;
|
|
476
|
+
for (let index = 0; index < 16; index += 1) {
|
|
477
|
+
scratch.parent[index] = parentWorld[parentOffset + index] ?? 0;
|
|
478
|
+
}
|
|
479
|
+
// Column-major composition is intentionally identical to the previous
|
|
480
|
+
// matrix path: Global = Parent * Local, with no TRS decomposition.
|
|
481
|
+
mat4.multiply(scratch.candidate, scratch.parent, scratch.local);
|
|
482
|
+
}
|
|
483
|
+
writeCandidate(binding, row, scratch.candidate, changed);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function ensureHierarchyBuffers(scratch: Scratch, bindings: readonly HierarchyBinding[]): void {
|
|
487
|
+
let same = scratch.hierarchyBindingTables.length === bindings.length;
|
|
488
|
+
if (same) {
|
|
489
|
+
for (let index = 0; index < bindings.length; index += 1) {
|
|
490
|
+
const binding = bindings[index];
|
|
491
|
+
if (
|
|
492
|
+
binding === undefined ||
|
|
493
|
+
scratch.hierarchyBindingTables[index] !== binding.tableId ||
|
|
494
|
+
scratch.hierarchyBindingRows[index] !== binding.rowCapacity
|
|
495
|
+
) {
|
|
496
|
+
same = false;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (same) return;
|
|
502
|
+
scratch.hierarchyBindingTables = bindings.map((binding) => binding.tableId);
|
|
503
|
+
scratch.hierarchyBindingRows = bindings.map((binding) => binding.rowCapacity);
|
|
504
|
+
scratch.hierarchyStates = bindings.map((binding) => new Uint8Array(binding.rowCapacity));
|
|
505
|
+
scratch.hierarchyChanged = bindings.map((binding) => new Uint8Array(binding.rowCapacity));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function resetHierarchyBuffers(scratch: Scratch): void {
|
|
509
|
+
for (let index = 0; index < scratch.hierarchyStates.length; index += 1) {
|
|
510
|
+
scratch.hierarchyStates[index]?.fill(0);
|
|
511
|
+
scratch.hierarchyChanged[index]?.fill(0);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function ensureFlatBuffers(scratch: Scratch, bindings: readonly TransformBinding[]): void {
|
|
516
|
+
let same = scratch.flatBindingTables.length === bindings.length;
|
|
517
|
+
if (same) {
|
|
518
|
+
for (let index = 0; index < bindings.length; index += 1) {
|
|
519
|
+
const binding = bindings[index];
|
|
520
|
+
if (
|
|
521
|
+
binding === undefined ||
|
|
522
|
+
scratch.flatBindingTables[index] !== binding.tableId ||
|
|
523
|
+
scratch.flatBindingRows[index] !== binding.rowCapacity
|
|
524
|
+
) {
|
|
525
|
+
same = false;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (same) return;
|
|
531
|
+
scratch.flatBindingTables = bindings.map((binding) => binding.tableId);
|
|
532
|
+
scratch.flatBindingRows = bindings.map((binding) => binding.rowCapacity);
|
|
533
|
+
scratch.flatChanged = bindings.map((binding) => new Uint8Array(binding.rowCapacity));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function resetFlatBuffers(scratch: Scratch): void {
|
|
537
|
+
for (const changed of scratch.flatChanged) changed.fill(0);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function derivedWriteError(cause: EcsError, bindingIndex: number): SceneError {
|
|
541
|
+
return new SceneError({
|
|
542
|
+
code: 'hierarchy-broken',
|
|
543
|
+
expected: 'derived GlobalTransform range publication to succeed',
|
|
544
|
+
hint: cause.hint ?? 'retry propagation on a healthy World',
|
|
545
|
+
detail: {
|
|
546
|
+
kind: 'derived-write',
|
|
547
|
+
entity: 0 as EntityHandle,
|
|
548
|
+
parent: 0 as EntityHandle,
|
|
549
|
+
bindingIndex,
|
|
550
|
+
base: 0,
|
|
551
|
+
start: 0,
|
|
552
|
+
count: 0,
|
|
553
|
+
cause,
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function findHierarchyLocation(
|
|
559
|
+
writer: HierarchyWriter,
|
|
560
|
+
bindings: readonly HierarchyBinding[],
|
|
561
|
+
entity: EntityHandle,
|
|
562
|
+
cursor: DerivedRangeCursor,
|
|
563
|
+
): HierarchyBinding | undefined {
|
|
564
|
+
countPropagation('hierarchyEntityLookups');
|
|
565
|
+
if (!writer.locateEntity(entity, cursor)) return undefined;
|
|
566
|
+
return bindings[cursor.bindingIndex];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function findTransformLocation(
|
|
570
|
+
writer: TransformWriter,
|
|
571
|
+
bindings: readonly TransformBinding[],
|
|
572
|
+
entity: EntityHandle,
|
|
573
|
+
cursor: DerivedRangeCursor,
|
|
574
|
+
): TransformBinding | undefined {
|
|
575
|
+
countPropagation('hierarchyEntityLookups');
|
|
576
|
+
if (!writer.locateEntity(entity, cursor)) return undefined;
|
|
577
|
+
return bindings[cursor.bindingIndex];
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function countPublishedRows(changed: Uint8Array): void {
|
|
581
|
+
const trace = propagationTrace;
|
|
582
|
+
if (trace === undefined) return;
|
|
583
|
+
let runOpen = false;
|
|
584
|
+
for (let row = 0; row < changed.length; row += 1) {
|
|
585
|
+
if ((changed[row] ?? 0) !== 0) {
|
|
586
|
+
trace.hierarchyPublishedRows += 1;
|
|
587
|
+
if (!runOpen) {
|
|
588
|
+
trace.hierarchyPublishedRuns += 1;
|
|
589
|
+
runOpen = true;
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
runOpen = false;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function noteHierarchyError(current: SceneError | undefined, next: SceneError): SceneError {
|
|
598
|
+
if (current === undefined) return next;
|
|
599
|
+
const currentEntity = Number(current.detail?.entity ?? Number.MAX_SAFE_INTEGER);
|
|
600
|
+
const nextEntity = Number(next.detail?.entity ?? Number.MAX_SAFE_INTEGER);
|
|
601
|
+
return nextEntity < currentEntity ||
|
|
602
|
+
(nextEntity === currentEntity && next.code.localeCompare(current.code) < 0)
|
|
603
|
+
? next
|
|
604
|
+
: current;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function composeLocalHierarchyEntity(
|
|
608
|
+
writer: HierarchyWriter,
|
|
609
|
+
bindings: readonly HierarchyBinding[],
|
|
610
|
+
entity: EntityHandle,
|
|
611
|
+
scratch: Scratch,
|
|
612
|
+
changed: Uint8Array[],
|
|
206
613
|
): boolean {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
return
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
614
|
+
const cursor = scratch.hierarchyCurrentCursor;
|
|
615
|
+
const binding = findHierarchyLocation(writer, bindings, entity, cursor);
|
|
616
|
+
if (binding === undefined) return false;
|
|
617
|
+
composeBindingRow(
|
|
618
|
+
binding,
|
|
619
|
+
cursor.row,
|
|
620
|
+
undefined,
|
|
621
|
+
0,
|
|
622
|
+
scratch,
|
|
623
|
+
changed[cursor.bindingIndex] as Uint8Array,
|
|
624
|
+
);
|
|
625
|
+
const states = scratch.hierarchyStates[cursor.bindingIndex];
|
|
626
|
+
if (states !== undefined) states[cursor.row] = 3;
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function handleActiveCycle(
|
|
631
|
+
repeated: EntityHandle,
|
|
632
|
+
stackEntities: EntityHandle[],
|
|
633
|
+
stackChildren: number[],
|
|
634
|
+
writer: HierarchyWriter,
|
|
635
|
+
bindings: readonly HierarchyBinding[],
|
|
636
|
+
scratch: Scratch,
|
|
637
|
+
changed: Uint8Array[],
|
|
638
|
+
report: (error: SceneError) => void,
|
|
639
|
+
): void {
|
|
640
|
+
let cycleStart = -1;
|
|
641
|
+
for (let index = 0; index < stackEntities.length; index += 1) {
|
|
642
|
+
if (stackEntities[index] === repeated) {
|
|
643
|
+
cycleStart = index;
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (cycleStart < 0) return;
|
|
648
|
+
const repeatedCursor: DerivedRangeCursor = { bindingIndex: -1, row: -1 };
|
|
649
|
+
const repeatedBinding = findHierarchyLocation(writer, bindings, repeated, repeatedCursor);
|
|
650
|
+
const repeatedParent =
|
|
651
|
+
repeatedBinding === undefined
|
|
652
|
+
? repeated
|
|
653
|
+
: ((hierarchyColumns(repeatedBinding).parent[repeatedCursor.row] ??
|
|
654
|
+
ENTITY_NULL_RAW) as number as EntityHandle);
|
|
655
|
+
report(
|
|
656
|
+
hierarchyError(
|
|
657
|
+
'hierarchy-cycle',
|
|
658
|
+
repeated,
|
|
659
|
+
repeatedParent,
|
|
660
|
+
'a parent-before-child Transform hierarchy',
|
|
661
|
+
'repair the ChildOf cycle and retry TransformPropagation',
|
|
662
|
+
),
|
|
214
663
|
);
|
|
664
|
+
for (let index = cycleStart; index < stackEntities.length; index += 1) {
|
|
665
|
+
const cycleEntity = stackEntities[index];
|
|
666
|
+
if (cycleEntity === undefined) continue;
|
|
667
|
+
composeLocalHierarchyEntity(writer, bindings, cycleEntity, scratch, changed);
|
|
668
|
+
stackChildren[index] = 0;
|
|
669
|
+
}
|
|
215
670
|
}
|
|
216
671
|
|
|
217
|
-
|
|
672
|
+
function walkChildren(
|
|
218
673
|
world: World,
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
674
|
+
root: EntityHandle,
|
|
675
|
+
writer: HierarchyWriter,
|
|
676
|
+
bindings: readonly HierarchyBinding[],
|
|
677
|
+
transformWriter: TransformWriter,
|
|
678
|
+
transformBindings: readonly TransformBinding[],
|
|
679
|
+
scratch: Scratch,
|
|
680
|
+
report: (error: SceneError) => void,
|
|
681
|
+
allowCompletedRoot: boolean,
|
|
682
|
+
): void {
|
|
683
|
+
countPropagation('hierarchyRootInvocations');
|
|
684
|
+
const stackEntities = scratch.hierarchyStackEntities;
|
|
685
|
+
const stackChildren = scratch.hierarchyStackChildren;
|
|
686
|
+
stackEntities.length = 0;
|
|
687
|
+
stackChildren.length = 0;
|
|
688
|
+
|
|
689
|
+
if (allowCompletedRoot) {
|
|
690
|
+
// Residual recovery may be entered once for each member of a malformed
|
|
691
|
+
// parent path. State 4 means a previous fallback walk already expanded
|
|
692
|
+
// this root and all reachable Children edges, so do not repeat that
|
|
693
|
+
// subtree for every cycle member.
|
|
694
|
+
const rootCursor = scratch.hierarchyRootCursor;
|
|
695
|
+
const rootBinding = findHierarchyLocation(writer, bindings, root, rootCursor);
|
|
696
|
+
if (rootBinding !== undefined) {
|
|
697
|
+
const state = scratch.hierarchyStates[rootCursor.bindingIndex]?.[rootCursor.row] ?? 0;
|
|
698
|
+
if (state === 4) return;
|
|
230
699
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
700
|
+
} else {
|
|
701
|
+
// This cursor is scratch-owned and reused for every root. A root walk is
|
|
702
|
+
// a hot invocation; allocating a cursor here would scale object churn with
|
|
703
|
+
// the number of roots even after all bindings have warmed.
|
|
704
|
+
const rootCursor = scratch.hierarchyRootCursor;
|
|
705
|
+
countPropagation('hierarchyRootCursorReuses');
|
|
706
|
+
const rootBinding = findHierarchyLocation(writer, bindings, root, rootCursor);
|
|
707
|
+
if (rootBinding !== undefined) {
|
|
708
|
+
const state = scratch.hierarchyStates[rootCursor.bindingIndex]?.[rootCursor.row] ?? 0;
|
|
709
|
+
if (state !== 0) return;
|
|
234
710
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
711
|
+
}
|
|
712
|
+
stackEntities.push(root);
|
|
713
|
+
stackChildren.push(-1);
|
|
714
|
+
|
|
715
|
+
const currentCursor = scratch.hierarchyCurrentCursor;
|
|
716
|
+
const parentCursor = scratch.hierarchyParentCursor;
|
|
717
|
+
const childCursor = scratch.hierarchyChildCursor;
|
|
718
|
+
while (stackEntities.length > 0) {
|
|
719
|
+
const top = stackEntities.length - 1;
|
|
720
|
+
const current = stackEntities[top];
|
|
721
|
+
if (current === undefined) {
|
|
722
|
+
stackEntities.pop();
|
|
723
|
+
stackChildren.pop();
|
|
724
|
+
continue;
|
|
248
725
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
726
|
+
const hierarchyBinding = findHierarchyLocation(writer, bindings, current, currentCursor);
|
|
727
|
+
const nextChild = stackChildren[top] ?? -1;
|
|
728
|
+
if (nextChild < 0) {
|
|
729
|
+
if (hierarchyBinding !== undefined) {
|
|
730
|
+
const state = scratch.hierarchyStates[currentCursor.bindingIndex]?.[currentCursor.row] ?? 0;
|
|
731
|
+
if (state === 0) {
|
|
732
|
+
const states = scratch.hierarchyStates[currentCursor.bindingIndex];
|
|
733
|
+
if (states !== undefined) states[currentCursor.row] = 1;
|
|
734
|
+
const parentRaw = (hierarchyColumns(hierarchyBinding).parent[currentCursor.row] ??
|
|
735
|
+
ENTITY_NULL_RAW) as number;
|
|
736
|
+
let completed = false;
|
|
737
|
+
if (parentRaw === ENTITY_NULL_RAW) {
|
|
738
|
+
composeBindingRow(
|
|
739
|
+
hierarchyBinding,
|
|
740
|
+
currentCursor.row,
|
|
741
|
+
undefined,
|
|
742
|
+
0,
|
|
743
|
+
scratch,
|
|
744
|
+
scratch.hierarchyChanged[currentCursor.bindingIndex] as Uint8Array,
|
|
745
|
+
);
|
|
746
|
+
completed = true;
|
|
747
|
+
} else {
|
|
748
|
+
const parent = parentRaw as EntityHandle;
|
|
749
|
+
const parentBinding = findTransformLocation(
|
|
750
|
+
transformWriter,
|
|
751
|
+
transformBindings,
|
|
752
|
+
parent,
|
|
753
|
+
parentCursor,
|
|
754
|
+
);
|
|
755
|
+
const parentHierarchy = findHierarchyLocation(writer, bindings, parent, childCursor);
|
|
756
|
+
if (parentBinding === undefined) {
|
|
757
|
+
report(
|
|
758
|
+
hierarchyError(
|
|
759
|
+
'hierarchy-broken',
|
|
760
|
+
current,
|
|
761
|
+
parent,
|
|
762
|
+
'each ChildOf parent to carry Transform and GlobalTransform',
|
|
763
|
+
'repair the missing parent pair before retrying propagation',
|
|
764
|
+
),
|
|
765
|
+
);
|
|
766
|
+
composeBindingRow(
|
|
767
|
+
hierarchyBinding,
|
|
768
|
+
currentCursor.row,
|
|
769
|
+
undefined,
|
|
770
|
+
0,
|
|
771
|
+
scratch,
|
|
772
|
+
scratch.hierarchyChanged[currentCursor.bindingIndex] as Uint8Array,
|
|
773
|
+
);
|
|
774
|
+
completed = true;
|
|
775
|
+
} else if (parentHierarchy !== undefined) {
|
|
776
|
+
const parentState =
|
|
777
|
+
scratch.hierarchyStates[childCursor.bindingIndex]?.[childCursor.row] ?? 0;
|
|
778
|
+
if (parentState === 1) {
|
|
779
|
+
handleActiveCycle(
|
|
780
|
+
parent,
|
|
781
|
+
stackEntities,
|
|
782
|
+
stackChildren,
|
|
783
|
+
writer,
|
|
784
|
+
bindings,
|
|
785
|
+
scratch,
|
|
786
|
+
scratch.hierarchyChanged,
|
|
787
|
+
report,
|
|
788
|
+
);
|
|
789
|
+
completed = true;
|
|
790
|
+
} else if (parentState === 0) {
|
|
791
|
+
report(
|
|
792
|
+
hierarchyError(
|
|
793
|
+
'hierarchy-broken',
|
|
794
|
+
current,
|
|
795
|
+
parent,
|
|
796
|
+
'Children to enumerate every parent-before-child edge',
|
|
797
|
+
'repair the Children mirror and retry TransformPropagation',
|
|
798
|
+
),
|
|
799
|
+
);
|
|
800
|
+
composeBindingRow(
|
|
801
|
+
hierarchyBinding,
|
|
802
|
+
currentCursor.row,
|
|
803
|
+
undefined,
|
|
804
|
+
0,
|
|
805
|
+
scratch,
|
|
806
|
+
scratch.hierarchyChanged[currentCursor.bindingIndex] as Uint8Array,
|
|
807
|
+
);
|
|
808
|
+
completed = true;
|
|
809
|
+
} else {
|
|
810
|
+
composeBindingRow(
|
|
811
|
+
hierarchyBinding,
|
|
812
|
+
currentCursor.row,
|
|
813
|
+
parentBinding,
|
|
814
|
+
parentCursor.row,
|
|
815
|
+
scratch,
|
|
816
|
+
scratch.hierarchyChanged[currentCursor.bindingIndex] as Uint8Array,
|
|
817
|
+
);
|
|
818
|
+
completed = true;
|
|
819
|
+
}
|
|
820
|
+
} else {
|
|
821
|
+
composeBindingRow(
|
|
822
|
+
hierarchyBinding,
|
|
823
|
+
currentCursor.row,
|
|
824
|
+
parentBinding,
|
|
825
|
+
parentCursor.row,
|
|
826
|
+
scratch,
|
|
827
|
+
scratch.hierarchyChanged[currentCursor.bindingIndex] as Uint8Array,
|
|
828
|
+
);
|
|
829
|
+
completed = true;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (completed && states !== undefined && states[currentCursor.row] === 1) {
|
|
833
|
+
states[currentCursor.row] = 2;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
stackChildren[top] = 0;
|
|
838
|
+
continue;
|
|
252
839
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
840
|
+
|
|
841
|
+
const childrenLength = world[worldInternal].getArrayLength(current, Children, 'entities') ?? 0;
|
|
842
|
+
if (nextChild >= childrenLength) {
|
|
843
|
+
stackEntities.pop();
|
|
844
|
+
stackChildren.pop();
|
|
845
|
+
if (allowCompletedRoot) {
|
|
846
|
+
const completedCursor = scratch.hierarchyResidualCursor;
|
|
847
|
+
const completedBinding = findHierarchyLocation(writer, bindings, current, completedCursor);
|
|
848
|
+
const completedStates =
|
|
849
|
+
completedBinding === undefined
|
|
850
|
+
? undefined
|
|
851
|
+
: scratch.hierarchyStates[completedCursor.bindingIndex];
|
|
852
|
+
if (completedStates !== undefined && completedStates[completedCursor.row] === 3) {
|
|
853
|
+
completedStates[completedCursor.row] = 4;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
continue;
|
|
268
857
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
858
|
+
stackChildren[top] = nextChild + 1;
|
|
859
|
+
countPropagation('hierarchyEdgesVisited');
|
|
860
|
+
const childRaw = world[worldInternal].getArrayElement(current, Children, 'entities', nextChild);
|
|
861
|
+
if (childRaw === undefined || childRaw === ENTITY_NULL_RAW) {
|
|
862
|
+
report(
|
|
863
|
+
hierarchyError(
|
|
864
|
+
'hierarchy-broken',
|
|
865
|
+
current,
|
|
866
|
+
current,
|
|
867
|
+
'Children.entities to contain live child handles',
|
|
868
|
+
'repair the Children mirror and retry TransformPropagation',
|
|
869
|
+
),
|
|
870
|
+
);
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const child = childRaw as EntityHandle;
|
|
874
|
+
const childBinding = findHierarchyLocation(writer, bindings, child, childCursor);
|
|
875
|
+
if (childBinding === undefined) {
|
|
876
|
+
report(
|
|
877
|
+
hierarchyError(
|
|
878
|
+
'hierarchy-broken',
|
|
879
|
+
child,
|
|
880
|
+
current,
|
|
881
|
+
'each Children entry to carry Transform, GlobalTransform, and ChildOf',
|
|
882
|
+
'repair the child component pair before retrying propagation',
|
|
883
|
+
),
|
|
884
|
+
);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const childParent = (hierarchyColumns(childBinding).parent[childCursor.row] ??
|
|
888
|
+
ENTITY_NULL_RAW) as number;
|
|
889
|
+
const childState = scratch.hierarchyStates[childCursor.bindingIndex]?.[childCursor.row] ?? 0;
|
|
890
|
+
if (childParent !== (current as number)) {
|
|
891
|
+
// Leave an unvisited child for the residual row-state pass. That pass
|
|
892
|
+
// can classify the complete path (including a rootless cycle) without
|
|
893
|
+
// emitting a premature mirror error that would hide the cycle cause.
|
|
894
|
+
if (childState === 0) continue;
|
|
895
|
+
report(
|
|
896
|
+
hierarchyError(
|
|
897
|
+
'hierarchy-broken',
|
|
898
|
+
child,
|
|
899
|
+
current,
|
|
900
|
+
'Children and ChildOf to describe the same parent',
|
|
901
|
+
'repair the relationship mirror and retry TransformPropagation',
|
|
902
|
+
),
|
|
903
|
+
);
|
|
904
|
+
continue;
|
|
273
905
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (!current.ok) {
|
|
289
|
-
CACHE.delete(world);
|
|
290
|
-
return propagateTransforms(world);
|
|
906
|
+
if (childState === 1) {
|
|
907
|
+
handleActiveCycle(
|
|
908
|
+
child,
|
|
909
|
+
stackEntities,
|
|
910
|
+
stackChildren,
|
|
911
|
+
writer,
|
|
912
|
+
bindings,
|
|
913
|
+
scratch,
|
|
914
|
+
scratch.hierarchyChanged,
|
|
915
|
+
report,
|
|
916
|
+
);
|
|
917
|
+
} else if (childState === 0) {
|
|
918
|
+
stackEntities.push(child);
|
|
919
|
+
stackChildren.push(-1);
|
|
291
920
|
}
|
|
292
|
-
const next = copyState(current.value);
|
|
293
|
-
if (!sameState(cache.locals.get(entity), next)) changed.add(entity);
|
|
294
|
-
cache.locals.set(entity, next);
|
|
295
921
|
}
|
|
296
|
-
|
|
922
|
+
}
|
|
297
923
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
924
|
+
function fallbackResidualPath(
|
|
925
|
+
world: World,
|
|
926
|
+
path: readonly EntityHandle[],
|
|
927
|
+
writer: HierarchyWriter,
|
|
928
|
+
bindings: readonly HierarchyBinding[],
|
|
929
|
+
transformWriter: TransformWriter,
|
|
930
|
+
transformBindings: readonly TransformBinding[],
|
|
931
|
+
scratch: Scratch,
|
|
932
|
+
report: (error: SceneError) => void,
|
|
933
|
+
): void {
|
|
934
|
+
// Root traversal owns the normal path. A residual path is necessarily a
|
|
935
|
+
// malformed Children projection (or a rootless cycle); cut the complete
|
|
936
|
+
// path to local roots in one pass so no stale GlobalTransform survives the
|
|
937
|
+
// diagnostic. Keeping the path as an explicit work list also makes this
|
|
938
|
+
// recovery O(path length), rather than repeatedly searching parent chains.
|
|
939
|
+
for (const entity of path) {
|
|
940
|
+
composeLocalHierarchyEntity(writer, bindings, entity, scratch, scratch.hierarchyChanged);
|
|
304
941
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
942
|
+
for (const entity of path) {
|
|
943
|
+
walkChildren(
|
|
944
|
+
world,
|
|
945
|
+
entity,
|
|
946
|
+
writer,
|
|
947
|
+
bindings,
|
|
948
|
+
transformWriter,
|
|
949
|
+
transformBindings,
|
|
950
|
+
scratch,
|
|
951
|
+
report,
|
|
952
|
+
true,
|
|
953
|
+
);
|
|
308
954
|
}
|
|
309
|
-
|
|
310
|
-
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function resolveResidualPath(
|
|
958
|
+
world: World,
|
|
959
|
+
entity: EntityHandle,
|
|
960
|
+
writer: HierarchyWriter,
|
|
961
|
+
bindings: readonly HierarchyBinding[],
|
|
962
|
+
transformWriter: TransformWriter,
|
|
963
|
+
transformBindings: readonly TransformBinding[],
|
|
964
|
+
scratch: Scratch,
|
|
965
|
+
report: (error: SceneError) => void,
|
|
966
|
+
): void {
|
|
967
|
+
const path = scratch.hierarchyProbeEntities;
|
|
968
|
+
path.length = 0;
|
|
969
|
+
const cursor = scratch.hierarchyResidualCursor;
|
|
970
|
+
const parentTransformCursor = scratch.hierarchyParentCursor;
|
|
971
|
+
const parentHierarchyCursor = scratch.hierarchyResidualParentCursor;
|
|
972
|
+
let current = entity;
|
|
973
|
+
while (true) {
|
|
974
|
+
countPropagation('hierarchyResidualParentProbes');
|
|
975
|
+
const binding = findHierarchyLocation(writer, bindings, current, cursor);
|
|
976
|
+
if (binding === undefined) break;
|
|
977
|
+
const states = scratch.hierarchyStates[cursor.bindingIndex];
|
|
978
|
+
const state = states?.[cursor.row] ?? 0;
|
|
979
|
+
if (state !== 0) {
|
|
980
|
+
const columns = hierarchyColumns(binding);
|
|
981
|
+
const parentRaw = (columns.parent[cursor.row] ?? ENTITY_NULL_RAW) as number;
|
|
982
|
+
report(
|
|
983
|
+
hierarchyError(
|
|
984
|
+
'hierarchy-cycle',
|
|
985
|
+
current,
|
|
986
|
+
parentRaw === ENTITY_NULL_RAW ? current : (parentRaw as EntityHandle),
|
|
987
|
+
'a parent-before-child Transform hierarchy',
|
|
988
|
+
'repair the ChildOf cycle and retry TransformPropagation',
|
|
989
|
+
),
|
|
990
|
+
);
|
|
991
|
+
fallbackResidualPath(
|
|
992
|
+
world,
|
|
993
|
+
path,
|
|
994
|
+
writer,
|
|
995
|
+
bindings,
|
|
996
|
+
transformWriter,
|
|
997
|
+
transformBindings,
|
|
998
|
+
scratch,
|
|
999
|
+
report,
|
|
1000
|
+
);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (states !== undefined) states[cursor.row] = 1;
|
|
1005
|
+
path.push(current);
|
|
1006
|
+
const parentRaw = (hierarchyColumns(binding).parent[cursor.row] ?? ENTITY_NULL_RAW) as number;
|
|
1007
|
+
if (parentRaw === ENTITY_NULL_RAW) {
|
|
1008
|
+
// Every null-parent row is enumerated as a root above. Reaching one
|
|
1009
|
+
// here means the materialized Children graph failed to expose the
|
|
1010
|
+
// parent-first edge; preserve the explicit malformed-graph error.
|
|
1011
|
+
report(
|
|
1012
|
+
hierarchyError(
|
|
1013
|
+
'hierarchy-broken',
|
|
1014
|
+
current,
|
|
1015
|
+
current,
|
|
1016
|
+
'Children to enumerate every parent-before-child edge',
|
|
1017
|
+
'repair the Children mirror and retry TransformPropagation',
|
|
1018
|
+
),
|
|
1019
|
+
);
|
|
1020
|
+
fallbackResidualPath(
|
|
1021
|
+
world,
|
|
1022
|
+
path,
|
|
1023
|
+
writer,
|
|
1024
|
+
bindings,
|
|
1025
|
+
transformWriter,
|
|
1026
|
+
transformBindings,
|
|
1027
|
+
scratch,
|
|
1028
|
+
report,
|
|
1029
|
+
);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const parent = parentRaw as EntityHandle;
|
|
1034
|
+
const parentTransform = findTransformLocation(
|
|
1035
|
+
transformWriter,
|
|
1036
|
+
transformBindings,
|
|
1037
|
+
parent,
|
|
1038
|
+
parentTransformCursor,
|
|
1039
|
+
);
|
|
1040
|
+
const parentHierarchy = findHierarchyLocation(writer, bindings, parent, parentHierarchyCursor);
|
|
1041
|
+
if (parentHierarchy === undefined) {
|
|
1042
|
+
report(
|
|
1043
|
+
hierarchyError(
|
|
1044
|
+
'hierarchy-broken',
|
|
1045
|
+
current,
|
|
1046
|
+
parent,
|
|
1047
|
+
parentTransform === undefined
|
|
1048
|
+
? 'each ChildOf parent to carry Transform and GlobalTransform'
|
|
1049
|
+
: 'Children to enumerate every parent-before-child edge',
|
|
1050
|
+
parentTransform === undefined
|
|
1051
|
+
? 'repair the missing parent pair before retrying propagation'
|
|
1052
|
+
: 'repair the Children mirror and retry TransformPropagation',
|
|
1053
|
+
),
|
|
1054
|
+
);
|
|
1055
|
+
fallbackResidualPath(
|
|
1056
|
+
world,
|
|
1057
|
+
path,
|
|
1058
|
+
writer,
|
|
1059
|
+
bindings,
|
|
1060
|
+
transformWriter,
|
|
1061
|
+
transformBindings,
|
|
1062
|
+
scratch,
|
|
1063
|
+
report,
|
|
1064
|
+
);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const parentState =
|
|
1069
|
+
scratch.hierarchyStates[parentHierarchyCursor.bindingIndex]?.[parentHierarchyCursor.row] ?? 0;
|
|
1070
|
+
if (parentState === 0) {
|
|
1071
|
+
current = parent;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
if (parentState === 1) {
|
|
1075
|
+
report(
|
|
1076
|
+
hierarchyError(
|
|
1077
|
+
'hierarchy-cycle',
|
|
1078
|
+
parent,
|
|
1079
|
+
current,
|
|
1080
|
+
'a parent-before-child Transform hierarchy',
|
|
1081
|
+
'repair the ChildOf cycle and retry TransformPropagation',
|
|
1082
|
+
),
|
|
1083
|
+
);
|
|
1084
|
+
} else {
|
|
1085
|
+
report(
|
|
1086
|
+
hierarchyError(
|
|
1087
|
+
'hierarchy-broken',
|
|
1088
|
+
current,
|
|
1089
|
+
parent,
|
|
1090
|
+
'Children to enumerate every parent-before-child edge',
|
|
1091
|
+
'repair the Children mirror and retry TransformPropagation',
|
|
1092
|
+
),
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
fallbackResidualPath(
|
|
1096
|
+
world,
|
|
1097
|
+
path,
|
|
1098
|
+
writer,
|
|
1099
|
+
bindings,
|
|
1100
|
+
transformWriter,
|
|
1101
|
+
transformBindings,
|
|
1102
|
+
scratch,
|
|
1103
|
+
report,
|
|
1104
|
+
);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function propagateHierarchy(world: World, scratch: Scratch): Result<void, SceneError> {
|
|
1110
|
+
const hierarchyWriter = scratch.hierarchyWriter;
|
|
1111
|
+
const transformWriter = scratch.transformWriter;
|
|
1112
|
+
if (hierarchyWriter === undefined || transformWriter === undefined) {
|
|
1113
|
+
return err(
|
|
1114
|
+
new SceneError({
|
|
1115
|
+
code: 'hierarchy-broken',
|
|
1116
|
+
expected: 'paired Transform and GlobalTransform derived bindings',
|
|
1117
|
+
hint: 'register the scene components before running TransformPropagation',
|
|
1118
|
+
}),
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
const hierarchyBindings = hierarchyWriter.bindings as readonly HierarchyBinding[];
|
|
1122
|
+
const transformBindings = transformWriter.bindings as readonly TransformBinding[];
|
|
1123
|
+
if (hierarchyBindings.length === 0) {
|
|
1124
|
+
// Keep the flat-only lane independent: it must not allocate hierarchy
|
|
1125
|
+
// markers or scan every Transform row merely to prove that no ChildOf
|
|
1126
|
+
// archetype exists.
|
|
1127
|
+
scratch.hierarchyBindingTables.length = 0;
|
|
1128
|
+
scratch.hierarchyBindingRows.length = 0;
|
|
1129
|
+
scratch.hierarchyStates.length = 0;
|
|
1130
|
+
scratch.hierarchyChanged.length = 0;
|
|
1131
|
+
return ok(undefined);
|
|
1132
|
+
}
|
|
1133
|
+
ensureHierarchyBuffers(scratch, hierarchyBindings);
|
|
1134
|
+
resetHierarchyBuffers(scratch);
|
|
1135
|
+
let firstError: SceneError | undefined;
|
|
1136
|
+
const report = (error: SceneError): void => {
|
|
1137
|
+
firstError = noteHierarchyError(firstError, error);
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
// Start only at actual roots (flat Transform rows and null-parent
|
|
1141
|
+
// ChildOf rows). Descendants are discovered exclusively through the ECS
|
|
1142
|
+
// materialized Children lists, so every normal frame is parent-first and
|
|
1143
|
+
// linear in rows plus relationship edges.
|
|
1144
|
+
for (const binding of transformBindings) {
|
|
1145
|
+
const entities = binding.entities;
|
|
1146
|
+
for (let row = 0; row < binding.rowCapacity; row += 1) {
|
|
1147
|
+
const entity = (entities[row] ?? 0) as EntityHandle;
|
|
1148
|
+
const parentRaw = world[worldInternal].getFieldValue(entity, ChildOf, 'parent');
|
|
1149
|
+
if (parentRaw === undefined || parentRaw === ENTITY_NULL_RAW) {
|
|
1150
|
+
walkChildren(
|
|
1151
|
+
world,
|
|
1152
|
+
entity,
|
|
1153
|
+
hierarchyWriter,
|
|
1154
|
+
hierarchyBindings,
|
|
1155
|
+
transformWriter,
|
|
1156
|
+
transformBindings,
|
|
1157
|
+
scratch,
|
|
1158
|
+
report,
|
|
1159
|
+
false,
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Any remaining hierarchy row is either behind a malformed/missing mirror
|
|
1166
|
+
// edge or belongs to a rootless cycle. Follow each residual parent chain
|
|
1167
|
+
// once with the same row-state machine (not a per-node parent search), then
|
|
1168
|
+
// cut that complete residual path to local space. This keeps malformed
|
|
1169
|
+
// coverage linear in residual rows and edges while normal frames remain
|
|
1170
|
+
// exclusively Children-driven.
|
|
1171
|
+
for (let bindingIndex = 0; bindingIndex < hierarchyBindings.length; bindingIndex += 1) {
|
|
1172
|
+
const binding = hierarchyBindings[bindingIndex];
|
|
1173
|
+
if (binding === undefined) continue;
|
|
1174
|
+
const entities = binding.entities;
|
|
1175
|
+
const states = scratch.hierarchyStates[bindingIndex];
|
|
1176
|
+
for (let row = 0; row < binding.rowCapacity; row += 1) {
|
|
1177
|
+
if ((states?.[row] ?? 0) !== 0) continue;
|
|
1178
|
+
const entity = (entities[row] ?? 0) as EntityHandle;
|
|
1179
|
+
resolveResidualPath(
|
|
1180
|
+
world,
|
|
1181
|
+
entity,
|
|
1182
|
+
hierarchyWriter,
|
|
1183
|
+
hierarchyBindings,
|
|
1184
|
+
transformWriter,
|
|
1185
|
+
transformBindings,
|
|
1186
|
+
scratch,
|
|
1187
|
+
report,
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
for (let bindingIndex = 0; bindingIndex < hierarchyBindings.length; bindingIndex += 1) {
|
|
1193
|
+
const changed = scratch.hierarchyChanged[bindingIndex];
|
|
1194
|
+
if (changed === undefined) continue;
|
|
1195
|
+
countPublishedRows(changed);
|
|
1196
|
+
const published = hierarchyWriter.publishChangedRows(bindingIndex, changed);
|
|
1197
|
+
if (!published.ok) {
|
|
1198
|
+
const cause = published.error as EcsError;
|
|
1199
|
+
report(derivedWriteError(cause, bindingIndex));
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return firstError === undefined ? ok(undefined) : err(firstError);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
export function propagateTransforms(world: World): Result<void, SceneError> {
|
|
1206
|
+
const scratch = scratchFor(world);
|
|
1207
|
+
const pairs = validateTransformPairs(world, scratch);
|
|
1208
|
+
if (!pairs.ok) return pairs;
|
|
1209
|
+
const flat = propagateFlat(world, scratch);
|
|
1210
|
+
if (!flat.ok) return flat;
|
|
1211
|
+
return propagateHierarchy(world, scratch);
|
|
311
1212
|
}
|
|
312
1213
|
|
|
313
1214
|
export const PropagateTransforms: SystemHandle<readonly []> = defineSystem({
|
|
@@ -341,7 +1242,7 @@ export function registerPropagateTransforms(
|
|
|
341
1242
|
world.removeSystem(FixedUpdate, PROPAGATE_TRANSFORMS_FIXED_SYSTEM);
|
|
342
1243
|
world.removeSystem(Update, PROPAGATE_TRANSFORMS_SYSTEM);
|
|
343
1244
|
REGISTRATION_LEASES.delete(world);
|
|
344
|
-
|
|
1245
|
+
SCRATCH.delete(world);
|
|
345
1246
|
}
|
|
346
1247
|
};
|
|
347
1248
|
}
|
|
@@ -360,17 +1261,18 @@ export function registerPropagateTransforms(
|
|
|
360
1261
|
.unwrap();
|
|
361
1262
|
}
|
|
362
1263
|
world.addSystems(FixedUpdate, TransformFixedSet, [PropagateTransformsFixed]).unwrap();
|
|
363
|
-
|
|
364
|
-
REGISTRATION_LEASES.set(world, lease);
|
|
1264
|
+
REGISTRATION_LEASES.set(world, { refs: 1 });
|
|
365
1265
|
let active = true;
|
|
366
1266
|
return () => {
|
|
367
1267
|
if (!active) return;
|
|
368
1268
|
active = false;
|
|
1269
|
+
const lease = REGISTRATION_LEASES.get(world);
|
|
1270
|
+
if (lease === undefined) return;
|
|
369
1271
|
lease.refs -= 1;
|
|
370
1272
|
if (lease.refs !== 0) return;
|
|
371
1273
|
world.removeSystem(FixedUpdate, PROPAGATE_TRANSFORMS_FIXED_SYSTEM);
|
|
372
1274
|
world.removeSystem(Update, PROPAGATE_TRANSFORMS_SYSTEM);
|
|
373
1275
|
REGISTRATION_LEASES.delete(world);
|
|
374
|
-
|
|
1276
|
+
SCRATCH.delete(world);
|
|
375
1277
|
};
|
|
376
1278
|
}
|