@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
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
PackErrorCode,
|
|
22
22
|
PackErrorDetail,
|
|
23
23
|
SceneAsset,
|
|
24
|
+
SceneEntityRef,
|
|
24
25
|
SceneInstanceMount,
|
|
25
26
|
} from '@forgeax/engine-types';
|
|
26
27
|
import {
|
|
@@ -32,6 +33,16 @@ import {
|
|
|
32
33
|
unwrapHandle,
|
|
33
34
|
} from '@forgeax/engine-types';
|
|
34
35
|
import { ComponentNotDefinedError } from '../errors';
|
|
36
|
+
import { resolveSceneEntity, validateSceneBindings } from './binding.js';
|
|
37
|
+
import {
|
|
38
|
+
isPrimitiveScalarFieldType,
|
|
39
|
+
mountOverrideStateKey,
|
|
40
|
+
primitiveJsType,
|
|
41
|
+
type SceneInstanceStatePayload,
|
|
42
|
+
sceneWorldState,
|
|
43
|
+
} from './state.js';
|
|
44
|
+
|
|
45
|
+
export type { SceneInstanceStatePayload } from './state.js';
|
|
35
46
|
|
|
36
47
|
const entityIndex = (entity: EntityHandle): number => (entity as number) & 0x00ffffff;
|
|
37
48
|
const entityGeneration = (entity: EntityHandle): number => ((entity as number) >>> 24) & 0xff;
|
|
@@ -136,21 +147,6 @@ export type SceneAssetResolver = (
|
|
|
136
147
|
parentHandle: Handle<'SceneAsset', 'shared'>,
|
|
137
148
|
) => Result<Handle<'SceneAsset', 'shared'>, unknown>;
|
|
138
149
|
|
|
139
|
-
interface SceneWorldState {
|
|
140
|
-
resolver: SceneAssetResolver | null;
|
|
141
|
-
readonly statePayloads: Map<number, unknown>;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const sceneWorldStates = new WeakMap<World, SceneWorldState>();
|
|
145
|
-
|
|
146
|
-
function sceneWorldState(world: World): SceneWorldState {
|
|
147
|
-
const current = sceneWorldStates.get(world);
|
|
148
|
-
if (current !== undefined) return current;
|
|
149
|
-
const created = { resolver: null, statePayloads: new Map<number, unknown>() };
|
|
150
|
-
sceneWorldStates.set(world, created);
|
|
151
|
-
return created;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
150
|
/** @internal */
|
|
155
151
|
export function worldSetSceneAssetResolver(world: World, resolver: SceneAssetResolver): void {
|
|
156
152
|
sceneWorldState(world).resolver = resolver;
|
|
@@ -158,7 +154,7 @@ export function worldSetSceneAssetResolver(world: World, resolver: SceneAssetRes
|
|
|
158
154
|
|
|
159
155
|
/** @internal */
|
|
160
156
|
export function worldGetSceneAssetResolver(world: World): SceneAssetResolver | null {
|
|
161
|
-
return sceneWorldState(world).resolver;
|
|
157
|
+
return sceneWorldState(world).resolver as SceneAssetResolver | null;
|
|
162
158
|
}
|
|
163
159
|
|
|
164
160
|
/**
|
|
@@ -339,6 +335,15 @@ export function worldSpawnSceneMembers(
|
|
|
339
335
|
|
|
340
336
|
const ownEntities = asset.entities;
|
|
341
337
|
const ownMounts = asset.mounts ?? [];
|
|
338
|
+
const bindingKeys = ownEntities.flatMap((entity) =>
|
|
339
|
+
entity.bindingKey === undefined ? [] : [entity.bindingKey],
|
|
340
|
+
);
|
|
341
|
+
if (bindingKeys.length > 0) {
|
|
342
|
+
const bindingCheck = validateSceneBindings(asset.sourceKey ?? '', bindingKeys);
|
|
343
|
+
if (!bindingCheck.ok) {
|
|
344
|
+
return err(bindingCheck.error as unknown as EcsError);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
342
347
|
const memberSum = ownMounts.reduce((s, m) => s + m.memberCount, 0);
|
|
343
348
|
const countBaseline = ownEntities.length + ownMounts.length + memberSum;
|
|
344
349
|
// C-R1 (studio-issues #6): mapping table must be sized to maxLocalId+1,
|
|
@@ -641,8 +646,8 @@ export function worldInstantiateSceneAsset(
|
|
|
641
646
|
const mappingPlain: number[] = Array.from(mapping);
|
|
642
647
|
// The synthetic root is the ChildOf parent of every owned root entity
|
|
643
648
|
// (step 5 below) and may itself become a ChildOf parent of a caller-
|
|
644
|
-
// supplied `parent` chain. propagateTransforms
|
|
645
|
-
//
|
|
649
|
+
// supplied `parent` chain. propagateTransforms expands the ECS-maintained
|
|
650
|
+
// Children lists parent-first and treats a parent missing Transform
|
|
646
651
|
// as `hierarchy-broken`, so the synthetic root must carry Transform
|
|
647
652
|
// (identity TRS via layer-2 defaults) when Transform is defined.
|
|
648
653
|
const rootComponents: ComponentData[] = [
|
|
@@ -701,8 +706,18 @@ export function worldInstantiateSceneAsset(
|
|
|
701
706
|
}
|
|
702
707
|
|
|
703
708
|
const detached = new Set<LocalEntityId>();
|
|
709
|
+
const bindings = new Map<string, EntityHandle>();
|
|
710
|
+
for (const entity of asset.entities) {
|
|
711
|
+
if (entity.bindingKey === undefined) continue;
|
|
712
|
+
const live = mapping[entity.localId as unknown as number];
|
|
713
|
+
if (live !== undefined && live !== ENTITY_NULL_RAW) {
|
|
714
|
+
bindings.set(entity.bindingKey, live as unknown as EntityHandle);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
704
717
|
const state: Record<string, unknown> = {
|
|
705
718
|
source: handle,
|
|
719
|
+
sceneSourceKey: asset.sourceKey,
|
|
720
|
+
bindings,
|
|
706
721
|
entityToLocalId,
|
|
707
722
|
detachedLocalIds: detached,
|
|
708
723
|
// Convert overrides Map<LocalEntityId, Map<string, MountOverride>>
|
|
@@ -1033,7 +1048,7 @@ export function worldValidateMountOverrides(
|
|
|
1033
1048
|
* chain `cube -> innerSyntheticRoot -> mountEntity -> outerSyntheticRoot`,
|
|
1034
1049
|
* so it MUST carry Transform whenever Transform is registered (mirrors
|
|
1035
1050
|
* the D-V-0 synthetic-root invariant). Otherwise propagateTransforms
|
|
1036
|
-
*
|
|
1051
|
+
* expanding the chain hits a Transform-less parent and emits per-frame
|
|
1037
1052
|
* `RhiError(hierarchy-broken)` (verify R1 root cause of the
|
|
1038
1053
|
* hello-scene-nesting demo black frames).
|
|
1039
1054
|
*/
|
|
@@ -1049,7 +1064,7 @@ export function worldSpawnMountEntity(
|
|
|
1049
1064
|
};
|
|
1050
1065
|
const cdRes = worldBuildSceneEntityComponentDatas(world, fakeNode, mapping, diagnostics);
|
|
1051
1066
|
if (!cdRes.ok) return cdRes;
|
|
1052
|
-
// R2/B-1: ensure Transform is attached so propagateTransforms can
|
|
1067
|
+
// R2/B-1: ensure Transform is attached so propagateTransforms can expand
|
|
1053
1068
|
// through this entity. Layer-2 defaults supply identity TRS; the
|
|
1054
1069
|
// mount.components overlay (when present and including Transform) takes
|
|
1055
1070
|
// precedence and is already in cdRes.value.
|
|
@@ -1179,6 +1194,22 @@ export function worldGetSceneInstanceState(
|
|
|
1179
1194
|
): Result<SceneInstanceStatePayload, EcsError> {
|
|
1180
1195
|
return worldResolveSceneInstanceStatePayload(world, root);
|
|
1181
1196
|
}
|
|
1197
|
+
|
|
1198
|
+
/** Resolve a generated SceneEntityRef against one concrete SceneInstance. */
|
|
1199
|
+
export function worldResolveSceneEntity(
|
|
1200
|
+
world: World,
|
|
1201
|
+
root: EntityHandle,
|
|
1202
|
+
ref: SceneEntityRef,
|
|
1203
|
+
): Result<EntityHandle, EcsError> {
|
|
1204
|
+
const state = worldResolveSceneInstanceStatePayload(world, root);
|
|
1205
|
+
if (!state.ok) return state;
|
|
1206
|
+
const resolved = resolveSceneEntity(ref, {
|
|
1207
|
+
sceneSourceKey: state.value.sceneSourceKey ?? ref.sceneSourceKey,
|
|
1208
|
+
bindings: state.value.bindings,
|
|
1209
|
+
});
|
|
1210
|
+
if (!resolved.ok) return err(resolved.error as unknown as EcsError);
|
|
1211
|
+
return ok(resolved.value as EntityHandle);
|
|
1212
|
+
}
|
|
1182
1213
|
/**
|
|
1183
1214
|
* Despawn a SceneInstance root + all its members. `opts.keepDetached`
|
|
1184
1215
|
* preserves members marked via `worldDetachSceneMember` (plan-strategy
|
|
@@ -1452,27 +1483,6 @@ export function worldGetSceneAssetForInstance(
|
|
|
1452
1483
|
return ok(stateRes.value.source);
|
|
1453
1484
|
}
|
|
1454
1485
|
|
|
1455
|
-
// SceneInstanceStatePayload — internal echo of the runtime
|
|
1456
|
-
// `SceneInstanceState` interface for ECS-side consumption (engine-ecs cannot
|
|
1457
|
-
// value-import engine-runtime by AC-29; structural shape only).
|
|
1458
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
1459
|
-
|
|
1460
|
-
/** @internal Structural payload behind `SceneInstance.state` ref column. */
|
|
1461
|
-
export interface SceneInstanceStatePayload {
|
|
1462
|
-
readonly source: Handle<'SceneAsset', 'shared'>;
|
|
1463
|
-
readonly entityToLocalId: Map<EntityHandle, LocalEntityId>;
|
|
1464
|
-
readonly detachedLocalIds: Set<LocalEntityId>;
|
|
1465
|
-
readonly overrides: Map<
|
|
1466
|
-
LocalEntityId,
|
|
1467
|
-
Map<string, { readonly comp: string; readonly field?: string; readonly value: unknown }>
|
|
1468
|
-
>;
|
|
1469
|
-
readonly rootEntities: EntityHandle[];
|
|
1470
|
-
/** Synthetic roots of recursively mounted SceneAssets owned by this instance. */
|
|
1471
|
-
readonly mountRoots: EntityHandle[];
|
|
1472
|
-
readonly totalSlots: number;
|
|
1473
|
-
readonly mountTimeOverrides: readonly MountOverride[];
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
1486
|
/**
|
|
1477
1487
|
* Topological sort over the implicit ChildOf graph (parents before children).
|
|
1478
1488
|
* Cycle-free input always covers all n nodes; cyclic input emits whatever was
|
|
@@ -1523,42 +1533,3 @@ function sceneTopoSort(
|
|
|
1523
1533
|
}
|
|
1524
1534
|
return order;
|
|
1525
1535
|
}
|
|
1526
|
-
|
|
1527
|
-
/**
|
|
1528
|
-
* @internal feat-20260713 M2 / w8: SceneInstanceState map key for a
|
|
1529
|
-
* MountOverride. Field-patch form keys by `comp:field` (one entry per patched
|
|
1530
|
-
* field); component-add form keys by `comp` (one entry per added component). The
|
|
1531
|
-
* two key shapes cannot collide because a field-patch always carries a `:field`
|
|
1532
|
-
* suffix. Later array entries for the same key overwrite earlier ones, matching
|
|
1533
|
-
* the array-order apply semantics.
|
|
1534
|
-
*/
|
|
1535
|
-
function mountOverrideStateKey(ov: MountOverride): string {
|
|
1536
|
-
return ov.field !== undefined ? `${ov.comp}:${ov.field}` : ov.comp;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
/** Schema field types that are JS primitives (typeof checkable). */
|
|
1540
|
-
function isPrimitiveScalarFieldType(fieldType: string): boolean {
|
|
1541
|
-
if (
|
|
1542
|
-
fieldType === 'f32' ||
|
|
1543
|
-
fieldType === 'f64' ||
|
|
1544
|
-
fieldType === 'u32' ||
|
|
1545
|
-
fieldType === 'i32' ||
|
|
1546
|
-
fieldType === 'u8' ||
|
|
1547
|
-
fieldType === 'i8' ||
|
|
1548
|
-
fieldType === 'u16' ||
|
|
1549
|
-
fieldType === 'i16' ||
|
|
1550
|
-
fieldType === 'bool' ||
|
|
1551
|
-
fieldType === 'string'
|
|
1552
|
-
) {
|
|
1553
|
-
return true;
|
|
1554
|
-
}
|
|
1555
|
-
if (fieldType.startsWith('enum<')) return true;
|
|
1556
|
-
return false;
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
/** Map a primitive scalar field type to the runtime `typeof` it should narrow to. */
|
|
1560
|
-
function primitiveJsType(fieldType: string): string {
|
|
1561
|
-
if (fieldType === 'bool') return 'boolean';
|
|
1562
|
-
if (fieldType === 'string') return 'string';
|
|
1563
|
-
return 'number';
|
|
1564
|
-
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { EntityHandle, World } from '@forgeax/engine-ecs';
|
|
2
|
+
import type { Handle, LocalEntityId, MountOverride } from '@forgeax/engine-types';
|
|
3
|
+
|
|
4
|
+
/** Internal state retained by a SceneInstance root. */
|
|
5
|
+
export interface SceneInstanceStatePayload {
|
|
6
|
+
readonly source: Handle<'SceneAsset', 'shared'>;
|
|
7
|
+
readonly sceneSourceKey?: string;
|
|
8
|
+
readonly bindings: Map<string, EntityHandle>;
|
|
9
|
+
readonly entityToLocalId: Map<EntityHandle, LocalEntityId>;
|
|
10
|
+
readonly detachedLocalIds: Set<LocalEntityId>;
|
|
11
|
+
readonly overrides: Map<
|
|
12
|
+
LocalEntityId,
|
|
13
|
+
Map<string, { readonly comp: string; readonly field?: string; readonly value: unknown }>
|
|
14
|
+
>;
|
|
15
|
+
readonly rootEntities: EntityHandle[];
|
|
16
|
+
readonly mountRoots: EntityHandle[];
|
|
17
|
+
readonly totalSlots: number;
|
|
18
|
+
readonly mountTimeOverrides: readonly MountOverride[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SceneWorldState {
|
|
22
|
+
resolver: unknown;
|
|
23
|
+
readonly statePayloads: Map<number, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sceneWorldStates = new WeakMap<World, SceneWorldState>();
|
|
27
|
+
|
|
28
|
+
export function sceneWorldState(world: World): SceneWorldState {
|
|
29
|
+
const current = sceneWorldStates.get(world);
|
|
30
|
+
if (current !== undefined) return current;
|
|
31
|
+
const created: SceneWorldState = { resolver: null, statePayloads: new Map<number, unknown>() };
|
|
32
|
+
sceneWorldStates.set(world, created);
|
|
33
|
+
return created;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function mountOverrideStateKey(ov: MountOverride): string {
|
|
37
|
+
return ov.field !== undefined ? `${ov.comp}:${ov.field}` : ov.comp;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isPrimitiveScalarFieldType(fieldType: string): boolean {
|
|
41
|
+
if (
|
|
42
|
+
fieldType === 'f32' ||
|
|
43
|
+
fieldType === 'f64' ||
|
|
44
|
+
fieldType === 'u32' ||
|
|
45
|
+
fieldType === 'i32' ||
|
|
46
|
+
fieldType === 'u8' ||
|
|
47
|
+
fieldType === 'i8' ||
|
|
48
|
+
fieldType === 'u16' ||
|
|
49
|
+
fieldType === 'i16' ||
|
|
50
|
+
fieldType === 'bool' ||
|
|
51
|
+
fieldType === 'string'
|
|
52
|
+
) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return fieldType.startsWith('enum<');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function primitiveJsType(fieldType: string): string {
|
|
59
|
+
if (fieldType === 'bool') return 'boolean';
|
|
60
|
+
if (fieldType === 'string') return 'string';
|
|
61
|
+
return 'number';
|
|
62
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -4,10 +4,17 @@ import { ChildOf } from './components/child-of';
|
|
|
4
4
|
import { Children } from './components/children';
|
|
5
5
|
import { MorphWeights } from './components/morph-weights';
|
|
6
6
|
import { Name } from './components/name';
|
|
7
|
-
import { Transform } from './components/transform';
|
|
7
|
+
import { GlobalTransform, Transform } from './components/transform';
|
|
8
8
|
import { registerPropagateTransforms } from './systems/propagate-transforms';
|
|
9
9
|
|
|
10
|
-
const SCENE_COMPONENTS: readonly Component[] = [
|
|
10
|
+
const SCENE_COMPONENTS: readonly Component[] = [
|
|
11
|
+
ChildOf,
|
|
12
|
+
Children,
|
|
13
|
+
MorphWeights,
|
|
14
|
+
Name,
|
|
15
|
+
Transform,
|
|
16
|
+
GlobalTransform,
|
|
17
|
+
];
|
|
11
18
|
|
|
12
19
|
function registerSceneComponents(world: World): () => void {
|
|
13
20
|
const leases = SCENE_COMPONENTS.map((component) => world.components.register(component).unwrap());
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { Entity, type EntityHandle, type World } from '@forgeax/engine-ecs';
|
|
2
|
-
import { createWorldProjection } from '@forgeax/engine-ecs/projection';
|
|
1
|
+
import { Entity, type EntityHandle, type Query, type World } from '@forgeax/engine-ecs';
|
|
3
2
|
import { ChildOf } from '../components/child-of';
|
|
4
3
|
import type { SceneErrorCode, SceneErrorDetail } from '../errors';
|
|
5
4
|
|
|
@@ -17,7 +16,8 @@ export interface SceneHierarchySnapshot {
|
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
interface HierarchyProjectionCacheEntry {
|
|
20
|
-
readonly
|
|
19
|
+
readonly structureEpoch: number;
|
|
20
|
+
readonly childOfChanges: Query;
|
|
21
21
|
readonly snapshot: SceneHierarchySnapshot;
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -30,6 +30,18 @@ interface HierarchyProjectionCacheEntry {
|
|
|
30
30
|
// writes are internal-only and must not mutate authored hierarchy state.
|
|
31
31
|
const HIERARCHY_PROJECTION_CACHE = new WeakMap<World, HierarchyProjectionCacheEntry>();
|
|
32
32
|
|
|
33
|
+
function createChildOfChangeQuery(world: World): Query {
|
|
34
|
+
const result = world.query({ changed: [ChildOf] });
|
|
35
|
+
if (!result.ok) throw result.error;
|
|
36
|
+
return result.value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function drainChanges(query: Query): boolean {
|
|
40
|
+
let changed = false;
|
|
41
|
+
for (const span of query.spans().unwrap()) changed ||= span.length > 0;
|
|
42
|
+
return changed;
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
function diagnostic(
|
|
34
46
|
code: SceneErrorCode,
|
|
35
47
|
entity: EntityHandle,
|
|
@@ -54,10 +66,12 @@ function diagnostic(
|
|
|
54
66
|
/** Build the only World-local projection of ChildOf parent facts. */
|
|
55
67
|
export function projectHierarchy(world: World): SceneHierarchySnapshot {
|
|
56
68
|
const cached = HIERARCHY_PROJECTION_CACHE.get(world);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
if (
|
|
70
|
+
cached !== undefined &&
|
|
71
|
+
cached.structureEpoch === world.getStructureEpoch() &&
|
|
72
|
+
!drainChanges(cached.childOfChanges)
|
|
73
|
+
) {
|
|
74
|
+
return cached.snapshot;
|
|
61
75
|
}
|
|
62
76
|
const liveEntities = new Set<EntityHandle>();
|
|
63
77
|
const authoredParents = new Map<EntityHandle, EntityHandle>();
|
|
@@ -126,8 +140,11 @@ export function projectHierarchy(world: World): SceneHierarchySnapshot {
|
|
|
126
140
|
return stableParentOf.get(entity);
|
|
127
141
|
},
|
|
128
142
|
};
|
|
143
|
+
const childOfChanges = createChildOfChangeQuery(world);
|
|
144
|
+
drainChanges(childOfChanges);
|
|
129
145
|
HIERARCHY_PROJECTION_CACHE.set(world, {
|
|
130
|
-
|
|
146
|
+
structureEpoch: world.getStructureEpoch(),
|
|
147
|
+
childOfChanges,
|
|
131
148
|
snapshot,
|
|
132
149
|
});
|
|
133
150
|
return snapshot;
|
package/src/systems/index.ts
CHANGED
|
@@ -4,8 +4,11 @@ export {
|
|
|
4
4
|
type SceneHierarchySnapshot,
|
|
5
5
|
} from './hierarchy-projection';
|
|
6
6
|
export {
|
|
7
|
+
PROPAGATE_TRANSFORMS_FIXED_SYSTEM,
|
|
7
8
|
PROPAGATE_TRANSFORMS_SYSTEM,
|
|
9
|
+
PropagateTransformsFixed,
|
|
8
10
|
propagateTransforms,
|
|
9
11
|
registerPropagateTransforms,
|
|
12
|
+
TransformFixedSet,
|
|
10
13
|
TransformSet,
|
|
11
14
|
} from './propagate-transforms';
|