@forgeax/engine-scene 0.1.26 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/fixtures/malformed-hierarchy-edge.d.ts.map +1 -1
- package/dist/index.mjs +7 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/fixtures/malformed-hierarchy-edge.ts +53 -4
- package/src/__tests__/propagation.unit.test.ts +13 -2
- package/src/systems/propagate-transforms.ts +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-scene",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
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.27",
|
|
26
|
+
"@forgeax/engine-math": "0.1.27",
|
|
27
|
+
"@forgeax/engine-plugin": "0.1.27",
|
|
28
|
+
"@forgeax/engine-types": "0.1.27"
|
|
29
29
|
},
|
|
30
30
|
"forgeax": {
|
|
31
31
|
"metrics": {
|
|
@@ -1,9 +1,57 @@
|
|
|
1
1
|
import type { EntityHandle, World } from '@forgeax/engine-ecs';
|
|
2
2
|
import { componentId } from '../../../../ecs/src/component';
|
|
3
3
|
import { entityIndex } from '../../../../ecs/src/entity-handle';
|
|
4
|
-
import { worldInternal } from '../../../../ecs/src/world-internal';
|
|
5
4
|
import { ChildOf } from '../../index';
|
|
6
5
|
|
|
6
|
+
const WORLD_INTERNAL_KEY: unique symbol = Symbol.for(
|
|
7
|
+
'forgeax.ecs.worldInternal',
|
|
8
|
+
) as unknown as typeof WORLD_INTERNAL_KEY;
|
|
9
|
+
|
|
10
|
+
interface MalformedRecord {
|
|
11
|
+
readonly archetypeId: number;
|
|
12
|
+
readonly archetypeRow: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface MalformedColumn {
|
|
16
|
+
readonly view: { [index: number]: number };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface MalformedComponentStorage {
|
|
20
|
+
readonly fields: Map<string, MalformedColumn>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface MalformedTable {
|
|
24
|
+
readonly storage: Map<number, MalformedComponentStorage>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface MalformedArchetype {
|
|
28
|
+
readonly tableId: number;
|
|
29
|
+
readonly rows: readonly number[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface MalformedGraph {
|
|
33
|
+
readonly archetypes: readonly (MalformedArchetype | undefined)[];
|
|
34
|
+
readonly tables: readonly (MalformedTable | undefined)[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface MalformedWorldInternal {
|
|
38
|
+
readonly getRecords: () => readonly (MalformedRecord | undefined)[];
|
|
39
|
+
readonly getGraph: () => MalformedGraph;
|
|
40
|
+
readonly markComponentChanged: (entity: EntityHandle, componentId: number) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface WorldWithMalformedInjection {
|
|
44
|
+
readonly [WORLD_INTERNAL_KEY]: MalformedWorldInternal;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function malformedInjection(world: World): MalformedWorldInternal {
|
|
48
|
+
const internal = (world as unknown as WorldWithMalformedInjection)[WORLD_INTERNAL_KEY];
|
|
49
|
+
if (internal === undefined) {
|
|
50
|
+
throw new Error('Malformed hierarchy fixture requires the ECS test injection seam.');
|
|
51
|
+
}
|
|
52
|
+
return internal;
|
|
53
|
+
}
|
|
54
|
+
|
|
7
55
|
/**
|
|
8
56
|
* Scene-owned malformed-edge fixture. Production relationship writes reject
|
|
9
57
|
* stale targets and cycles before touching either side of the relationship.
|
|
@@ -16,11 +64,12 @@ export function setMalformedParentEdge(
|
|
|
16
64
|
child: EntityHandle,
|
|
17
65
|
parent: EntityHandle,
|
|
18
66
|
): void {
|
|
19
|
-
const
|
|
67
|
+
const internal = malformedInjection(world);
|
|
68
|
+
const record = internal.getRecords()[entityIndex(child)];
|
|
20
69
|
if (record === undefined || record.archetypeId < 0) {
|
|
21
70
|
throw new Error(`Malformed hierarchy fixture child ${child} is not live.`);
|
|
22
71
|
}
|
|
23
|
-
const graph =
|
|
72
|
+
const graph = internal.getGraph();
|
|
24
73
|
const archetype = graph.archetypes[record.archetypeId];
|
|
25
74
|
if (archetype === undefined) throw new Error(`Missing archetype ${record.archetypeId}.`);
|
|
26
75
|
const table = graph.tables[archetype.tableId];
|
|
@@ -33,5 +82,5 @@ export function setMalformedParentEdge(
|
|
|
33
82
|
// Keep the corruption internal, but make the source mutation observable to
|
|
34
83
|
// the normal change-token consumers so an already-built projection cannot
|
|
35
84
|
// mask the deliberately malformed value.
|
|
36
|
-
|
|
85
|
+
internal.markComponentChanged(child, componentId(ChildOf));
|
|
37
86
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type EntityHandle, FixedUpdate, Update, World } from '@forgeax/engine-ecs';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
3
|
import { ChangeEpochExhaustedError } from '../../../ecs/src/errors';
|
|
4
|
-
import { worldInternal } from '../../../ecs/src/world-internal';
|
|
5
4
|
import { ChildOf, GlobalTransform, propagateTransforms, Transform } from '../index';
|
|
6
5
|
import { registerPropagateTransforms } from '../systems';
|
|
7
6
|
import {
|
|
@@ -10,6 +9,18 @@ import {
|
|
|
10
9
|
} from '../systems/propagate-transforms';
|
|
11
10
|
import { setMalformedParentEdge } from './fixtures/malformed-hierarchy-edge';
|
|
12
11
|
|
|
12
|
+
const WORLD_INTERNAL_KEY: unique symbol = Symbol.for(
|
|
13
|
+
'forgeax.ecs.worldInternal',
|
|
14
|
+
) as unknown as typeof WORLD_INTERNAL_KEY;
|
|
15
|
+
|
|
16
|
+
interface RangePublicationInjection {
|
|
17
|
+
readonly markComponentRangeChanged: (...args: never[]) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface WorldWithRangePublicationInjection {
|
|
21
|
+
readonly [WORLD_INTERNAL_KEY]: RangePublicationInjection;
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
function spawnTransform(world: World, pos: number[], parent?: EntityHandle) {
|
|
14
25
|
const components = [{ component: Transform, data: { pos } }];
|
|
15
26
|
if (parent === undefined) return world.spawn(...components).unwrap();
|
|
@@ -67,7 +78,7 @@ describe('scene propagation', () => {
|
|
|
67
78
|
it('preserves the ECS range-publication error in flat propagation diagnostics', () => {
|
|
68
79
|
const world = new World();
|
|
69
80
|
spawnTransform(world, [2, 0, 0]);
|
|
70
|
-
const internal = world[
|
|
81
|
+
const internal = (world as unknown as WorldWithRangePublicationInjection)[WORLD_INTERNAL_KEY];
|
|
71
82
|
const originalMarkRange = internal.markComponentRangeChanged;
|
|
72
83
|
Object.defineProperty(internal, 'markComponentRangeChanged', {
|
|
73
84
|
configurable: true,
|
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
type DerivedRangeCursor,
|
|
16
16
|
type DerivedRangeWriter,
|
|
17
17
|
getDerivedWriter,
|
|
18
|
-
worldInternal,
|
|
19
18
|
} from '@forgeax/engine-ecs/internal';
|
|
19
|
+
import { worldRead } from '@forgeax/engine-ecs/world-read';
|
|
20
20
|
import { type Mat4, mat4 } from '@forgeax/engine-math';
|
|
21
21
|
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
22
22
|
import { ChildOf } from '../components/child-of';
|
|
@@ -374,7 +374,7 @@ function propagateFlat(world: World, scratch: Scratch): Result<void, SceneError>
|
|
|
374
374
|
return pairError(0 as EntityHandle, 'dense Transform and GlobalTransform derived bindings');
|
|
375
375
|
}
|
|
376
376
|
const transformBindings = transformWriter.bindings as readonly TransformBinding[];
|
|
377
|
-
const structureEpoch = world
|
|
377
|
+
const structureEpoch = world.getStructureEpoch();
|
|
378
378
|
if (scratch.flatStructureEpoch === structureEpoch) return ok(undefined);
|
|
379
379
|
|
|
380
380
|
ensureFlatBuffers(scratch, transformBindings);
|
|
@@ -385,7 +385,7 @@ function propagateFlat(world: World, scratch: Scratch): Result<void, SceneError>
|
|
|
385
385
|
if (binding === undefined || changed === undefined) continue;
|
|
386
386
|
for (let row = 0; row < binding.rowCapacity; row += 1) {
|
|
387
387
|
const entity = (binding.entities[row] ?? 0) as EntityHandle;
|
|
388
|
-
const parentRaw = world[
|
|
388
|
+
const parentRaw = world[worldRead].getFieldValue(entity, ChildOf, 'parent');
|
|
389
389
|
if (parentRaw !== undefined && parentRaw !== ENTITY_NULL_RAW) continue;
|
|
390
390
|
countPropagation('flatStructuralRootRows');
|
|
391
391
|
composeBindingRow(binding, row, undefined, 0, scratch, changed);
|
|
@@ -838,7 +838,7 @@ function walkChildren(
|
|
|
838
838
|
continue;
|
|
839
839
|
}
|
|
840
840
|
|
|
841
|
-
const childrenLength = world[
|
|
841
|
+
const childrenLength = world[worldRead].getArrayLength(current, Children, 'entities') ?? 0;
|
|
842
842
|
if (nextChild >= childrenLength) {
|
|
843
843
|
stackEntities.pop();
|
|
844
844
|
stackChildren.pop();
|
|
@@ -857,7 +857,7 @@ function walkChildren(
|
|
|
857
857
|
}
|
|
858
858
|
stackChildren[top] = nextChild + 1;
|
|
859
859
|
countPropagation('hierarchyEdgesVisited');
|
|
860
|
-
const childRaw = world[
|
|
860
|
+
const childRaw = world[worldRead].getArrayElement(current, Children, 'entities', nextChild);
|
|
861
861
|
if (childRaw === undefined || childRaw === ENTITY_NULL_RAW) {
|
|
862
862
|
report(
|
|
863
863
|
hierarchyError(
|
|
@@ -1145,7 +1145,7 @@ function propagateHierarchy(world: World, scratch: Scratch): Result<void, SceneE
|
|
|
1145
1145
|
const entities = binding.entities;
|
|
1146
1146
|
for (let row = 0; row < binding.rowCapacity; row += 1) {
|
|
1147
1147
|
const entity = (entities[row] ?? 0) as EntityHandle;
|
|
1148
|
-
const parentRaw = world[
|
|
1148
|
+
const parentRaw = world[worldRead].getFieldValue(entity, ChildOf, 'parent');
|
|
1149
1149
|
if (parentRaw === undefined || parentRaw === ENTITY_NULL_RAW) {
|
|
1150
1150
|
walkChildren(
|
|
1151
1151
|
world,
|