@forgeax/engine-scene 0.1.33 → 0.1.34
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 +8 -6
- package/dist/__tests__/hierarchy-dirty.unit.test.d.ts +2 -0
- package/dist/__tests__/hierarchy-dirty.unit.test.d.ts.map +1 -0
- package/dist/index.mjs +82 -33
- package/dist/index.mjs.map +1 -1
- package/dist/systems/propagate-transforms.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/flat-propagation.contract.test.ts +3 -1
- package/src/__tests__/flat-propagation.perf.test.ts +3 -1
- package/src/__tests__/hierarchy-dirty.unit.test.ts +68 -0
- package/src/__tests__/propagation.unit.test.ts +4 -0
- package/src/__tests__/transform-carrier-prototype.bench.ts +2 -2
- package/src/__tests__/transform-carrier-prototype.test.ts +1 -1
- package/src/systems/propagate-transforms.ts +104 -40
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"propagate-transforms.d.ts","sourceRoot":"","sources":["../../src/systems/propagate-transforms.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,YAAY,EAEjB,KAAK,KAAK,EACX,MAAM,qBAAqB,CAAC;AAS7B,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAI7D,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,eAAO,MAAM,2BAA2B,EAAG,qBAA8B,CAAC;AAC1E,eAAO,MAAM,iCAAiC,EAAG,0BAAmC,CAAC;AACrF,eAAO,MAAM,YAAY,yCAAyC,CAAC;AACnE,eAAO,MAAM,iBAAiB,yCAA+C,CAAC;AAE9E;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC,wBAAwB,EAAE,MAAM,CAAC;IACjC,yBAAyB,EAAE,MAAM,CAAC;IAClC,8BAA8B,EAAE,MAAM,CAAC;IACvC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,6BAA6B,EAAE,MAAM,CAAC;IACtC,sBAAsB,EAAE,MAAM,CAAC;CAChC;AAWD,wBAAgB,8BAA8B,IAAI,IAAI,CAcrD;AAED,wBAAgB,4BAA4B,IAAI,yBAAyB,CAsBxE;
|
|
1
|
+
{"version":3,"file":"propagate-transforms.d.ts","sourceRoot":"","sources":["../../src/systems/propagate-transforms.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,YAAY,EAEjB,KAAK,KAAK,EACX,MAAM,qBAAqB,CAAC;AAS7B,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAI7D,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,eAAO,MAAM,2BAA2B,EAAG,qBAA8B,CAAC;AAC1E,eAAO,MAAM,iCAAiC,EAAG,0BAAmC,CAAC;AACrF,eAAO,MAAM,YAAY,yCAAyC,CAAC;AACnE,eAAO,MAAM,iBAAiB,yCAA+C,CAAC;AAE9E;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC,wBAAwB,EAAE,MAAM,CAAC;IACjC,yBAAyB,EAAE,MAAM,CAAC;IAClC,8BAA8B,EAAE,MAAM,CAAC;IACvC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,6BAA6B,EAAE,MAAM,CAAC;IACtC,sBAAsB,EAAE,MAAM,CAAC;CAChC;AAWD,wBAAgB,8BAA8B,IAAI,IAAI,CAcrD;AAED,wBAAgB,4BAA4B,IAAI,yBAAyB,CAsBxE;AA2oCD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAc1E;AAED,eAAO,MAAM,mBAAmB,EAAE,YAAY,CAAC,SAAS,EAAE,CAOxD,CAAC;AAEH,eAAO,MAAM,wBAAwB,EAAE,YAAY,CAAC,SAAS,EAAE,CAI7D,CAAC;AAEH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,KAAK,EACZ,OAAO,GAAE;IAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1C,MAAM,IAAI,CA8CZ"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-scene",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@forgeax/engine-ecs": "0.1.
|
|
26
|
-
"@forgeax/engine-math": "0.1.
|
|
27
|
-
"@forgeax/engine-plugin": "0.1.
|
|
28
|
-
"@forgeax/engine-types": "0.1.
|
|
25
|
+
"@forgeax/engine-ecs": "0.1.34",
|
|
26
|
+
"@forgeax/engine-math": "0.1.34",
|
|
27
|
+
"@forgeax/engine-plugin": "0.1.34",
|
|
28
|
+
"@forgeax/engine-types": "0.1.34"
|
|
29
29
|
},
|
|
30
30
|
"forgeax": {
|
|
31
31
|
"metrics": {
|
|
@@ -100,7 +100,9 @@ describe('flat Transform propagation contract', () => {
|
|
|
100
100
|
|
|
101
101
|
it('does not retain an entity collection or a second lane in the flat owner', async () => {
|
|
102
102
|
const source = readFileSync('packages/scene/src/systems/propagate-transforms.ts', 'utf8');
|
|
103
|
-
expect(source
|
|
103
|
+
expect(source.slice(0, source.indexOf('function walkChildren('))).not.toMatch(
|
|
104
|
+
/new (Map|Set)|\b(Map|Set)</,
|
|
105
|
+
);
|
|
104
106
|
expect(source).not.toMatch(/PropagationCache/);
|
|
105
107
|
expect(source).toContain('changed: [Transform]');
|
|
106
108
|
expect(source).toContain('composeFlatColumns');
|
|
@@ -332,6 +332,8 @@ describe('real ECS flat Transform propagation benchmark', () => {
|
|
|
332
332
|
).toBe(true);
|
|
333
333
|
|
|
334
334
|
const source = readFileSync('packages/scene/src/systems/propagate-transforms.ts', 'utf8');
|
|
335
|
-
expect(source
|
|
335
|
+
expect(source.slice(0, source.indexOf('function walkChildren('))).not.toMatch(
|
|
336
|
+
/new (Map|Set)|\\b(Map|Set)</,
|
|
337
|
+
);
|
|
336
338
|
}, 120_000);
|
|
337
339
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { FixedUpdate, World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { expect, it } from 'vitest';
|
|
3
|
+
import { ChildOf } from '../components/child-of';
|
|
4
|
+
import { GlobalTransform, Transform } from '../components/transform';
|
|
5
|
+
import {
|
|
6
|
+
beginTransformPropagationTrace,
|
|
7
|
+
endTransformPropagationTrace,
|
|
8
|
+
propagateTransforms,
|
|
9
|
+
registerPropagateTransforms,
|
|
10
|
+
} from '../systems/propagate-transforms';
|
|
11
|
+
|
|
12
|
+
it('skips clean hierarchies and evaluates only the highest dirty subtrees', () => {
|
|
13
|
+
const world = new World();
|
|
14
|
+
const root = world.spawn({ component: Transform, data: { pos: [1, 0, 0] } }).unwrap();
|
|
15
|
+
const child = world
|
|
16
|
+
.spawn(
|
|
17
|
+
{ component: Transform, data: { pos: [2, 0, 0] } },
|
|
18
|
+
{ component: ChildOf, data: { parent: root } },
|
|
19
|
+
)
|
|
20
|
+
.unwrap();
|
|
21
|
+
const leaf = world
|
|
22
|
+
.spawn(
|
|
23
|
+
{ component: Transform, data: { pos: [3, 0, 0] } },
|
|
24
|
+
{ component: ChildOf, data: { parent: child } },
|
|
25
|
+
)
|
|
26
|
+
.unwrap();
|
|
27
|
+
const sibling = world
|
|
28
|
+
.spawn({ component: Transform, data: {} }, { component: ChildOf, data: { parent: root } })
|
|
29
|
+
.unwrap();
|
|
30
|
+
propagateTransforms(world).unwrap();
|
|
31
|
+
beginTransformPropagationTrace();
|
|
32
|
+
propagateTransforms(world).unwrap();
|
|
33
|
+
expect(endTransformPropagationTrace().hierarchyEntityLookups).toBe(0);
|
|
34
|
+
world.set(child, Transform, { pos: [4, 0, 0] }).unwrap();
|
|
35
|
+
world.set(leaf, Transform, { pos: [5, 0, 0] }).unwrap();
|
|
36
|
+
beginTransformPropagationTrace();
|
|
37
|
+
propagateTransforms(world).unwrap();
|
|
38
|
+
expect(endTransformPropagationTrace().hierarchyRowsEvaluated).toBe(2);
|
|
39
|
+
expect(world.get(leaf, GlobalTransform).unwrap().world[12]).toBe(10);
|
|
40
|
+
world.set(child, ChildOf, { parent: sibling }).unwrap();
|
|
41
|
+
propagateTransforms(world).unwrap();
|
|
42
|
+
expect(world.get(leaf, GlobalTransform).unwrap().world[12]).toBe(10);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('does not repeat FixedUpdate propagation in Update without another local write', () => {
|
|
46
|
+
const world = new World({ time: { fixedDeltaSeconds: 1 / 60 } });
|
|
47
|
+
const root = world.spawn({ component: Transform, data: {} }).unwrap();
|
|
48
|
+
const child = world
|
|
49
|
+
.spawn({ component: Transform, data: {} }, { component: ChildOf, data: { parent: root } })
|
|
50
|
+
.unwrap();
|
|
51
|
+
registerPropagateTransforms(world);
|
|
52
|
+
world.update(0).unwrap();
|
|
53
|
+
world
|
|
54
|
+
.addSystem(FixedUpdate, {
|
|
55
|
+
name: 'write-local',
|
|
56
|
+
queries: [],
|
|
57
|
+
before: ['propagateTransformsFixed'],
|
|
58
|
+
fn: () => {
|
|
59
|
+
world.set(root, Transform, { pos: [3, 0, 0] }).unwrap();
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
.unwrap();
|
|
63
|
+
beginTransformPropagationTrace();
|
|
64
|
+
world.update(1 / 60).unwrap();
|
|
65
|
+
const trace = endTransformPropagationTrace();
|
|
66
|
+
expect(trace.hierarchyRowsEvaluated).toBe(1);
|
|
67
|
+
expect(world.get(child, GlobalTransform).unwrap().world[12]).toBe(3);
|
|
68
|
+
});
|
|
@@ -230,6 +230,10 @@ describe('scene propagation', () => {
|
|
|
230
230
|
}
|
|
231
231
|
propagateTransforms(world).unwrap();
|
|
232
232
|
|
|
233
|
+
for (const row of world.query({ write: [Transform] }).unwrap()) {
|
|
234
|
+
const pos = row.mut(Transform).pos;
|
|
235
|
+
pos[0] = (pos[0] ?? 0) + 1;
|
|
236
|
+
}
|
|
233
237
|
beginTransformPropagationTrace();
|
|
234
238
|
propagateTransforms(world).unwrap();
|
|
235
239
|
const trace = endTransformPropagationTrace();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
3
|
const SEED = 20260901;
|
|
4
|
-
const FRAME_COUNT =
|
|
4
|
+
const FRAME_COUNT = 60;
|
|
5
5
|
const processLike = (
|
|
6
6
|
globalThis as typeof globalThis & {
|
|
7
7
|
process?: { memoryUsage(): { heapUsed: number }; env?: Record<string, string | undefined> };
|
|
@@ -86,7 +86,7 @@ function runCandidate(candidateId: CandidateId, workload: Workload): CandidateRe
|
|
|
86
86
|
checksum = (checksum + Math.round(frameChecksum * 1000) + SEED + frame) >>> 0;
|
|
87
87
|
queryMs += performance.now() - queryStart;
|
|
88
88
|
|
|
89
|
-
if (workload.workloadId === 'structural-churn' && frame %
|
|
89
|
+
if (workload.workloadId === 'structural-churn' && frame % (FRAME_COUNT / 10) === 0) {
|
|
90
90
|
const recycled = (frame * 97) % workload.rows;
|
|
91
91
|
local[recycled] = frame + 1;
|
|
92
92
|
world[recycled] = local[recycled] * 2;
|
|
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
3
3
|
import { ChildOf } from '../index';
|
|
4
4
|
|
|
5
5
|
const SEED = 20260901;
|
|
6
|
-
const FRAME_COUNT =
|
|
6
|
+
const FRAME_COUNT = 60;
|
|
7
7
|
const PROTOTYPE_ROWS = 64;
|
|
8
8
|
|
|
9
9
|
const BCarrier = defineComponent('PrototypeBCarrier', { local: 'f32', world: 'f32' });
|
|
@@ -127,6 +127,10 @@ interface Scratch {
|
|
|
127
127
|
flatBindingTables: number[];
|
|
128
128
|
flatBindingRows: number[];
|
|
129
129
|
flatStructureEpoch: number;
|
|
130
|
+
hierarchyStructureEpoch: number;
|
|
131
|
+
dirtyTransforms?: Query<readonly [typeof Transform]>;
|
|
132
|
+
dirtyParents?: Query<readonly [typeof ChildOf]>;
|
|
133
|
+
dirtyGlobals?: Query<readonly [typeof GlobalTransform]>;
|
|
130
134
|
flatQuery?: FlatQuery;
|
|
131
135
|
hierarchyQuery?: HierarchyQuery;
|
|
132
136
|
transformQuery?: TransformQuery;
|
|
@@ -191,6 +195,11 @@ function ensureQueries(world: World, scratch: Scratch): Result<void, SceneError>
|
|
|
191
195
|
without: [ChildOf],
|
|
192
196
|
changed: [Transform],
|
|
193
197
|
});
|
|
198
|
+
scratch.dirtyTransforms ??= world.query({ read: [Transform], changed: [Transform] }).unwrap();
|
|
199
|
+
scratch.dirtyParents ??= world.query({ read: [ChildOf], changed: [ChildOf] }).unwrap();
|
|
200
|
+
scratch.dirtyGlobals ??= world
|
|
201
|
+
.query({ read: [GlobalTransform], changed: [GlobalTransform] })
|
|
202
|
+
.unwrap();
|
|
194
203
|
const hierarchy = world.query({ read: [Transform, ChildOf], write: [GlobalTransform] });
|
|
195
204
|
const transform = world.query({ read: [Transform], write: [GlobalTransform] });
|
|
196
205
|
const missingGlobal = world.query({ with: [Transform], without: [GlobalTransform] });
|
|
@@ -263,6 +272,7 @@ function scratchFor(world: World): Scratch {
|
|
|
263
272
|
flatBindingTables: [],
|
|
264
273
|
flatBindingRows: [],
|
|
265
274
|
flatStructureEpoch: -1,
|
|
275
|
+
hierarchyStructureEpoch: -1,
|
|
266
276
|
};
|
|
267
277
|
SCRATCH.set(world, created);
|
|
268
278
|
return created;
|
|
@@ -679,6 +689,7 @@ function walkChildren(
|
|
|
679
689
|
scratch: Scratch,
|
|
680
690
|
report: (error: SceneError) => void,
|
|
681
691
|
allowCompletedRoot: boolean,
|
|
692
|
+
refresh = false,
|
|
682
693
|
): void {
|
|
683
694
|
countPropagation('hierarchyRootInvocations');
|
|
684
695
|
const stackEntities = scratch.hierarchyStackEntities;
|
|
@@ -706,7 +717,7 @@ function walkChildren(
|
|
|
706
717
|
const rootBinding = findHierarchyLocation(writer, bindings, root, rootCursor);
|
|
707
718
|
if (rootBinding !== undefined) {
|
|
708
719
|
const state = scratch.hierarchyStates[rootCursor.bindingIndex]?.[rootCursor.row] ?? 0;
|
|
709
|
-
if (state !== 0) return;
|
|
720
|
+
if (state !== 0 && !refresh) return;
|
|
710
721
|
}
|
|
711
722
|
}
|
|
712
723
|
stackEntities.push(root);
|
|
@@ -727,6 +738,10 @@ function walkChildren(
|
|
|
727
738
|
const nextChild = stackChildren[top] ?? -1;
|
|
728
739
|
if (nextChild < 0) {
|
|
729
740
|
if (hierarchyBinding !== undefined) {
|
|
741
|
+
if (refresh) {
|
|
742
|
+
const states = scratch.hierarchyStates[currentCursor.bindingIndex];
|
|
743
|
+
if (states !== undefined) states[currentCursor.row] = 0;
|
|
744
|
+
}
|
|
730
745
|
const state = scratch.hierarchyStates[currentCursor.bindingIndex]?.[currentCursor.row] ?? 0;
|
|
731
746
|
if (state === 0) {
|
|
732
747
|
const states = scratch.hierarchyStates[currentCursor.bindingIndex];
|
|
@@ -914,7 +929,7 @@ function walkChildren(
|
|
|
914
929
|
scratch.hierarchyChanged,
|
|
915
930
|
report,
|
|
916
931
|
);
|
|
917
|
-
} else if (childState === 0) {
|
|
932
|
+
} else if (childState === 0 || refresh) {
|
|
918
933
|
stackEntities.push(child);
|
|
919
934
|
stackChildren.push(-1);
|
|
920
935
|
}
|
|
@@ -1106,7 +1121,11 @@ function resolveResidualPath(
|
|
|
1106
1121
|
}
|
|
1107
1122
|
}
|
|
1108
1123
|
|
|
1109
|
-
function propagateHierarchy(
|
|
1124
|
+
function propagateHierarchy(
|
|
1125
|
+
world: World,
|
|
1126
|
+
scratch: Scratch,
|
|
1127
|
+
globalEdited: boolean,
|
|
1128
|
+
): Result<void, SceneError> {
|
|
1110
1129
|
const hierarchyWriter = scratch.hierarchyWriter;
|
|
1111
1130
|
const transformWriter = scratch.transformWriter;
|
|
1112
1131
|
if (hierarchyWriter === undefined || transformWriter === undefined) {
|
|
@@ -1128,25 +1147,37 @@ function propagateHierarchy(world: World, scratch: Scratch): Result<void, SceneE
|
|
|
1128
1147
|
scratch.hierarchyBindingRows.length = 0;
|
|
1129
1148
|
scratch.hierarchyStates.length = 0;
|
|
1130
1149
|
scratch.hierarchyChanged.length = 0;
|
|
1150
|
+
scratch.hierarchyStructureEpoch = world.getStructureEpoch();
|
|
1131
1151
|
return ok(undefined);
|
|
1132
1152
|
}
|
|
1153
|
+
const dirty = new Set<EntityHandle>();
|
|
1154
|
+
for (const row of scratch.dirtyTransforms ?? []) dirty.add(row.entity);
|
|
1155
|
+
let rebuild = scratch.hierarchyStructureEpoch !== world.getStructureEpoch();
|
|
1156
|
+
for (const _row of scratch.dirtyParents ?? []) rebuild = true;
|
|
1157
|
+
rebuild ||= globalEdited;
|
|
1158
|
+
if (!rebuild && dirty.size === 0) return ok(undefined);
|
|
1133
1159
|
ensureHierarchyBuffers(scratch, hierarchyBindings);
|
|
1134
|
-
resetHierarchyBuffers(scratch);
|
|
1160
|
+
if (rebuild) resetHierarchyBuffers(scratch);
|
|
1161
|
+
else for (const changed of scratch.hierarchyChanged) changed.fill(0);
|
|
1135
1162
|
let firstError: SceneError | undefined;
|
|
1136
1163
|
const report = (error: SceneError): void => {
|
|
1137
1164
|
firstError = noteHierarchyError(firstError, error);
|
|
1138
1165
|
};
|
|
1139
1166
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1167
|
+
if (!rebuild) {
|
|
1168
|
+
// The previous successful structural pass proved the parent graph. Only
|
|
1169
|
+
// highest dirty ancestors need execution; clean parents retain valid worlds.
|
|
1170
|
+
for (const entity of dirty) {
|
|
1171
|
+
let parent = world[worldRead].getFieldValue(entity, ChildOf, 'parent');
|
|
1172
|
+
let covered = false;
|
|
1173
|
+
while (parent !== undefined && parent !== ENTITY_NULL_RAW) {
|
|
1174
|
+
if (dirty.has(parent as EntityHandle)) {
|
|
1175
|
+
covered = true;
|
|
1176
|
+
break;
|
|
1177
|
+
}
|
|
1178
|
+
parent = world[worldRead].getFieldValue(parent as EntityHandle, ChildOf, 'parent');
|
|
1179
|
+
}
|
|
1180
|
+
if (!covered)
|
|
1150
1181
|
walkChildren(
|
|
1151
1182
|
world,
|
|
1152
1183
|
entity,
|
|
@@ -1157,35 +1188,60 @@ function propagateHierarchy(world: World, scratch: Scratch): Result<void, SceneE
|
|
|
1157
1188
|
scratch,
|
|
1158
1189
|
report,
|
|
1159
1190
|
false,
|
|
1191
|
+
true,
|
|
1160
1192
|
);
|
|
1193
|
+
}
|
|
1194
|
+
} else {
|
|
1195
|
+
// Start only at actual roots (flat Transform rows and null-parent
|
|
1196
|
+
// ChildOf rows). Descendants are discovered exclusively through the ECS
|
|
1197
|
+
// materialized Children lists, so every normal frame is parent-first and
|
|
1198
|
+
// linear in rows plus relationship edges.
|
|
1199
|
+
for (const binding of transformBindings) {
|
|
1200
|
+
const entities = binding.entities;
|
|
1201
|
+
for (let row = 0; row < binding.rowCapacity; row += 1) {
|
|
1202
|
+
const entity = (entities[row] ?? 0) as EntityHandle;
|
|
1203
|
+
const parentRaw = world[worldRead].getFieldValue(entity, ChildOf, 'parent');
|
|
1204
|
+
if (parentRaw === undefined || parentRaw === ENTITY_NULL_RAW) {
|
|
1205
|
+
walkChildren(
|
|
1206
|
+
world,
|
|
1207
|
+
entity,
|
|
1208
|
+
hierarchyWriter,
|
|
1209
|
+
hierarchyBindings,
|
|
1210
|
+
transformWriter,
|
|
1211
|
+
transformBindings,
|
|
1212
|
+
scratch,
|
|
1213
|
+
report,
|
|
1214
|
+
false,
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1161
1217
|
}
|
|
1162
1218
|
}
|
|
1163
|
-
}
|
|
1164
1219
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1220
|
+
// Any remaining hierarchy row is either behind a malformed/missing mirror
|
|
1221
|
+
// edge or belongs to a rootless cycle. Follow each residual parent chain
|
|
1222
|
+
// once with the same row-state machine (not a per-node parent search), then
|
|
1223
|
+
// cut that complete residual path to local space. This keeps malformed
|
|
1224
|
+
// coverage linear in residual rows and edges while normal frames remain
|
|
1225
|
+
// exclusively Children-driven.
|
|
1226
|
+
for (let bindingIndex = 0; bindingIndex < hierarchyBindings.length; bindingIndex += 1) {
|
|
1227
|
+
const binding = hierarchyBindings[bindingIndex];
|
|
1228
|
+
if (binding === undefined) continue;
|
|
1229
|
+
const entities = binding.entities;
|
|
1230
|
+
const states = scratch.hierarchyStates[bindingIndex];
|
|
1231
|
+
for (let row = 0; row < binding.rowCapacity; row += 1) {
|
|
1232
|
+
if ((states?.[row] ?? 0) !== 0) continue;
|
|
1233
|
+
const entity = (entities[row] ?? 0) as EntityHandle;
|
|
1234
|
+
resolveResidualPath(
|
|
1235
|
+
world,
|
|
1236
|
+
entity,
|
|
1237
|
+
hierarchyWriter,
|
|
1238
|
+
hierarchyBindings,
|
|
1239
|
+
transformWriter,
|
|
1240
|
+
transformBindings,
|
|
1241
|
+
scratch,
|
|
1242
|
+
report,
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1189
1245
|
}
|
|
1190
1246
|
}
|
|
1191
1247
|
|
|
@@ -1199,6 +1255,7 @@ function propagateHierarchy(world: World, scratch: Scratch): Result<void, SceneE
|
|
|
1199
1255
|
report(derivedWriteError(cause, bindingIndex));
|
|
1200
1256
|
}
|
|
1201
1257
|
}
|
|
1258
|
+
scratch.hierarchyStructureEpoch = firstError === undefined ? world.getStructureEpoch() : -1;
|
|
1202
1259
|
return firstError === undefined ? ok(undefined) : err(firstError);
|
|
1203
1260
|
}
|
|
1204
1261
|
|
|
@@ -1206,9 +1263,16 @@ export function propagateTransforms(world: World): Result<void, SceneError> {
|
|
|
1206
1263
|
const scratch = scratchFor(world);
|
|
1207
1264
|
const pairs = validateTransformPairs(world, scratch);
|
|
1208
1265
|
if (!pairs.ok) return pairs;
|
|
1266
|
+
let globalEdited = false;
|
|
1267
|
+
for (const _row of scratch.dirtyGlobals ?? []) globalEdited = true;
|
|
1209
1268
|
const flat = propagateFlat(world, scratch);
|
|
1210
1269
|
if (!flat.ok) return flat;
|
|
1211
|
-
|
|
1270
|
+
const hierarchy = propagateHierarchy(world, scratch, globalEdited);
|
|
1271
|
+
// Consume our own derived publications, while preserving later external edits.
|
|
1272
|
+
for (const _row of scratch.dirtyGlobals ?? []) {
|
|
1273
|
+
/* observation only */
|
|
1274
|
+
}
|
|
1275
|
+
return hierarchy;
|
|
1212
1276
|
}
|
|
1213
1277
|
|
|
1214
1278
|
export const PropagateTransforms: SystemHandle<readonly []> = defineSystem({
|