@vgai/engine 0.4.1 → 0.5.0-canary.20260719.0
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 +48 -15
- package/package.json +11 -25
- package/schemas/engine-capabilities.json +10 -10
- package/schemas/entity2d.schema.json +468 -0
- package/schemas/mat.schema.json +2 -33
- package/schemas/prefab.schema.json +16 -172
- package/schemas/scn2d.schema.json +42 -23
- package/schemas/vgai-game.schema.json +34 -0
- package/schemas/vscn.schema.json +16 -172
- package/src/adapter/authoring.ts +152 -2
- package/src/adapter/colyseus-networking-adapter.ts +35 -1
- package/src/adapter/first-party-systems.ts +7 -1
- package/src/adapter/game-adapter.ts +13 -0
- package/src/adapter/index.ts +25 -0
- package/src/adapter/rapier-physics-adapter.ts +55 -2
- package/src/adapter/system-adapter.ts +249 -2
- package/src/adapter/vgai-scene-game-adapter.ts +149 -25
- package/src/ai/navigation.ts +28 -0
- package/src/animation/clip-map.ts +1 -8
- package/src/animation/theatre-director.ts +50 -0
- package/src/animation/xstate-animation-binding.ts +6 -0
- package/src/audio/audio-introspection.ts +290 -0
- package/src/audio/tone-context.ts +46 -0
- package/src/dev/chrome-trace.ts +153 -0
- package/src/dev/performance-profiler.ts +93 -6
- package/src/dev/render-debug-adapter.ts +199 -0
- package/src/dev/render-memory.ts +243 -0
- package/src/dev/webgl-frame-capture.ts +424 -0
- package/src/ecs/component-manager.ts +43 -10
- package/src/ecs/game-component.ts +39 -10
- package/src/input/input-manager.ts +24 -19
- package/src/input/input-types.ts +1 -1
- package/src/loader.ts +7 -0
- package/src/manifest/load.ts +14 -0
- package/src/manifest/schema.ts +65 -0
- package/src/react/game-state.tsx +1 -1
- package/src/render/render-batch-system.ts +26 -12
- package/src/render/spark-renderer-lifecycle.ts +64 -0
- package/src/runtime/create-runtime.ts +33 -5
- package/src/runtime/debug-bridge.ts +5 -5
- package/src/runtime/game.ts +102 -20
- package/src/runtime/mount-manifest.ts +1 -1
- package/src/runtime/render-control.ts +121 -0
- package/src/runtime/types.ts +1 -1
- package/src/scene/asset-loaders.ts +77 -3
- package/src/scene/instance-mesh.ts +25 -0
- package/src/scene/material-factory.ts +4 -15
- package/src/scene/mesh-shadow.ts +18 -0
- package/src/scene/particles-factory.ts +59 -0
- package/src/scene/scene-loader.ts +41 -16
- package/src/scene/schema/instances.ts +1 -2
- package/src/scene/schema/material.ts +83 -94
- package/src/scene/schema/mesh.ts +76 -90
- package/src/scene/schema/scene-file.ts +1 -2
- package/src/scene/user-data.ts +30 -14
- package/src/setup/setup-renderer.ts +6 -1
- package/src/world2d/asset-paths2d.ts +44 -0
- package/src/world2d/collision-2d.ts +7 -14
- package/src/world2d/entity2d-asset.ts +22 -0
- package/src/world2d/index.ts +27 -2
- package/src/world2d/physics2d-transform.ts +173 -0
- package/src/world2d/physics2d-units.ts +10 -0
- package/src/world2d/pixi-game-adapter.ts +148 -36
- package/src/world2d/scene2d-identity.ts +49 -0
- package/src/world2d/scene2d-loader.ts +243 -119
- package/src/world2d/schema/entity2d.ts +51 -33
- package/src/world2d/schema/physics2d.ts +14 -3
- package/src/world2d/schema/sprite.ts +32 -4
- package/src/world2d/schema/tilemap.ts +26 -9
- package/src/world2d/transform-writer-2d.ts +29 -11
- package/src/world2d/types.ts +21 -8
- package/src/world3d-react/behavior.tsx +138 -0
- package/src/world3d-react/engine-bridge.ts +48 -0
- package/src/world3d-react/index.ts +44 -0
- package/src/world3d-react/r3f-adapter.tsx +303 -0
- package/src/world3d-react/world-context.ts +294 -0
- package/vendor/realism-effects/LICENSE.md +21 -0
- package/vendor/realism-effects/UPSTREAM.md +19 -0
- package/vendor/realism-effects/dist/index.cjs +3447 -0
- package/vendor/realism-effects/dist/index.d.ts +59 -0
- package/vendor/realism-effects/dist/index.js +3434 -0
- package/vendor/realism-effects/package.json +23 -0
- package/src/character/cloth-sim.ts +0 -533
- package/src/character/spring-chain.ts +0 -307
- package/src/humanoid/body.ts +0 -663
- package/src/humanoid/clips.ts +0 -149
- package/src/humanoid/compose.ts +0 -209
- package/src/humanoid/generate.ts +0 -189
- package/src/humanoid/index.ts +0 -36
- package/src/humanoid/schema.ts +0 -108
- package/src/humanoid/skeleton.ts +0 -345
- package/src/react/humanoid-bake.document.tsx +0 -337
- package/src/scene/geometries/index.ts +0 -7
- package/src/scene/geometries/terrain.ts +0 -42
- package/src/scene/geometry-registry.ts +0 -42
- package/src/scene/instance-registry.ts +0 -84
- package/src/scene/instancers/grid.ts +0 -38
- package/src/scene/instancers/index.ts +0 -7
- package/src/scene/material-registry.ts +0 -73
- package/src/scene/materials/index.ts +0 -7
- package/src/scene/materials/water.ts +0 -56
- package/src/world2d/components-2d.ts +0 -86
- package/tools/humanoid-bake.tool.ts +0 -274
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import RAPIER from '@dimforge/rapier2d-compat';
|
|
2
2
|
import type { Container } from 'pixi.js';
|
|
3
|
+
import { createInputManagerAdapter } from '../adapter/first-party-systems';
|
|
3
4
|
import type { MountedPixiWorld } from '../adapter/game-adapter';
|
|
5
|
+
import type { SystemAdapters } from '../adapter/system-adapter';
|
|
4
6
|
import { createAssetCache } from '../assets';
|
|
5
7
|
import { createGameLoop } from '../core/game-loop';
|
|
8
|
+
import {
|
|
9
|
+
createSeededRandom,
|
|
10
|
+
DEFAULT_SEEDED_RANDOM_SEED,
|
|
11
|
+
getSeededRandom,
|
|
12
|
+
} from '../core/seeded-random';
|
|
6
13
|
import { createSystemRunner } from '../core/system-runner';
|
|
7
14
|
import { createComponentManager } from '../ecs/component-manager';
|
|
15
|
+
import { InputManager } from '../input/input-manager';
|
|
16
|
+
import {
|
|
17
|
+
createDebugRegistry,
|
|
18
|
+
type DebugRegistry,
|
|
19
|
+
getDebugRegistry,
|
|
20
|
+
} from '../runtime/debug-registry';
|
|
8
21
|
import {
|
|
9
22
|
createGame,
|
|
10
23
|
createWorldInstance,
|
|
11
24
|
type GameInternal,
|
|
12
25
|
type WorldFrameHooks,
|
|
13
26
|
} from '../runtime/game';
|
|
14
|
-
import type { GameContext } from '../runtime/types';
|
|
15
27
|
import { createCollision2DSystem, createTriggerDispatch2D } from './collision-2d';
|
|
16
28
|
import { createPhysics2DRegistry } from './physics2d-registry';
|
|
29
|
+
import { DEFAULT_WORLD2D_PIXELS_PER_METER, defaultWorld2DGravityY } from './physics2d-units';
|
|
17
30
|
import { createPixiSurface, type PixiSurface } from './pixi-surface';
|
|
18
31
|
import { loadScene2DFromData, type Scene2DInstance } from './scene2d-loader';
|
|
19
32
|
import type { Scene2DFile } from './schema/entity2d';
|
|
@@ -25,6 +38,8 @@ export interface World2DHost {
|
|
|
25
38
|
canvas: HTMLCanvasElement;
|
|
26
39
|
width: number;
|
|
27
40
|
height: number;
|
|
41
|
+
/** Headless validation mount: skip browser-only asset/input initialization. */
|
|
42
|
+
headless?: boolean;
|
|
28
43
|
/** Shared Game root when mounted by the universal multi-world host. */
|
|
29
44
|
game?: GameInternal;
|
|
30
45
|
/** DPR override (T6.1 slice 1, COMPOSITION-DESIGN.md D5 §3 — "one DPR
|
|
@@ -85,6 +100,9 @@ export interface PixiSceneConfig {
|
|
|
85
100
|
setup?: World2DSetupFn;
|
|
86
101
|
/** Component registry the scene's `components` reference. */
|
|
87
102
|
registry?: Component2DRegistry;
|
|
103
|
+
/** Input map JSON path. Defaults to the same conventional map as Three;
|
|
104
|
+
* explicit `null` disables loading for deliberately input-free/test mounts. */
|
|
105
|
+
inputMapPath?: string | null;
|
|
88
106
|
id?: string;
|
|
89
107
|
}
|
|
90
108
|
|
|
@@ -101,7 +119,12 @@ export class PixiSceneGameAdapter {
|
|
|
101
119
|
}
|
|
102
120
|
|
|
103
121
|
async mount(host: World2DHost): Promise<MountedGame2D> {
|
|
104
|
-
const {
|
|
122
|
+
const {
|
|
123
|
+
inputMapPath = '/inputmaps/default.inputmap.json',
|
|
124
|
+
sceneData,
|
|
125
|
+
setup,
|
|
126
|
+
registry = {},
|
|
127
|
+
} = this.config;
|
|
105
128
|
|
|
106
129
|
// --- Pixi surface (autoStart:false; engine loop drives render) ---
|
|
107
130
|
const surface: PixiSurface = await createPixiSurface({
|
|
@@ -120,15 +143,43 @@ export class PixiSceneGameAdapter {
|
|
|
120
143
|
|
|
121
144
|
// --- Rapier 2D world (separate from the 3D world) ---
|
|
122
145
|
await RAPIER.init();
|
|
123
|
-
const
|
|
146
|
+
const pixelsPerMeter = sceneData?.pixelsPerMeter ?? DEFAULT_WORLD2D_PIXELS_PER_METER;
|
|
147
|
+
const gravity = sceneData?.gravity ?? [0, defaultWorld2DGravityY(pixelsPerMeter)];
|
|
124
148
|
const rapier2dWorld = new RAPIER.World({ x: gravity[0], y: gravity[1] });
|
|
149
|
+
// Keep Rapier's public API in authored pixels. Its native length-unit
|
|
150
|
+
// setting scales solver/contact tolerances for those pixel-sized bodies.
|
|
151
|
+
rapier2dWorld.lengthUnit = pixelsPerMeter;
|
|
125
152
|
const eventQueue = new RAPIER.EventQueue(true);
|
|
126
153
|
|
|
127
154
|
const physics2d = createPhysics2DRegistry();
|
|
128
|
-
const writeTransforms2D = createTransformWriter2D(physics2d);
|
|
155
|
+
const writeTransforms2D = createTransformWriter2D(physics2d, surface.world);
|
|
129
156
|
const collisions2d = createCollision2DSystem(rapier2dWorld, eventQueue);
|
|
157
|
+
const ownsInput = !host.game;
|
|
158
|
+
const input = host.game?.input ?? new InputManager();
|
|
159
|
+
if (!host.headless && inputMapPath !== null) {
|
|
160
|
+
if (host.game) await host.game.loadInputMap(inputMapPath);
|
|
161
|
+
else await input.loadMap(inputMapPath);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const systemAdapters: SystemAdapters = {};
|
|
165
|
+
const debugRegistry: DebugRegistry =
|
|
166
|
+
(host.game ? getDebugRegistry(host.game) : null) ??
|
|
167
|
+
createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
|
|
168
|
+
if (ownsInput) systemAdapters.input = createInputManagerAdapter(input);
|
|
169
|
+
systemAdapters.debug = debugRegistry.adapter;
|
|
170
|
+
// W4b (F11 frame debugger): the pixijs adapter deliberately does NOT
|
|
171
|
+
// register a `renderDebug` SystemAdapter. The first-party WebGL2 frame
|
|
172
|
+
// capture (`dev/webgl-frame-capture.ts`) and its `onBeforeRender`-based
|
|
173
|
+
// attribution are three.js-shaped (three's own per-object pre-draw hook,
|
|
174
|
+
// a THREE.Scene traversal, `renderer.info` memory). PixiJS draws through
|
|
175
|
+
// its own batch renderer with no equivalent per-object seam, so a faithful
|
|
176
|
+
// pixi capture is a separate instrument — deferred, not faked. Absence of
|
|
177
|
+
// `renderDebug` here is the honest "capability not supported" signal; the
|
|
178
|
+
// editor's frame-debugger panel marks a pixi world's section absent rather
|
|
179
|
+
// than showing fabricated draws.
|
|
130
180
|
|
|
131
181
|
const systems = createSystemRunner(host.game?.profiler.systemObserver, 'pixijs');
|
|
182
|
+
if (ownsInput) systems.add('input', () => input.poll(), { name: 'input.poll' });
|
|
132
183
|
systems.add('physics', () => rapier2dWorld.step(eventQueue), { name: 'physics2d.step' });
|
|
133
184
|
systems.add(
|
|
134
185
|
'postPhysics',
|
|
@@ -149,44 +200,110 @@ export class PixiSceneGameAdapter {
|
|
|
149
200
|
collisions2d,
|
|
150
201
|
components: null!,
|
|
151
202
|
systems,
|
|
152
|
-
|
|
203
|
+
input,
|
|
204
|
+
debug: debugRegistry.forWorld(this.id),
|
|
205
|
+
random:
|
|
206
|
+
(host.game ? getSeededRandom(host.game) : null) ??
|
|
207
|
+
createSeededRandom(DEFAULT_SEEDED_RANDOM_SEED),
|
|
208
|
+
registerSystemAdapter: (kind, adapter) => {
|
|
209
|
+
systemAdapters[kind] = adapter;
|
|
210
|
+
},
|
|
211
|
+
...(host.game ? { game: host.game, roots: host.game.roots } : {}),
|
|
153
212
|
};
|
|
154
213
|
|
|
214
|
+
debugRegistry.setInputActionsSource(this.id, () =>
|
|
215
|
+
input.actionNames().map((name) => ({ name, valueType: input.getActionValueType(name) })),
|
|
216
|
+
);
|
|
217
|
+
debugRegistry.setInputTraceSource(this.id, () => {
|
|
218
|
+
const raw = input.getInputTrace();
|
|
219
|
+
return {
|
|
220
|
+
version: raw.version,
|
|
221
|
+
seed: (host.game ? getSeededRandom(host.game) : null)?.seed ?? null,
|
|
222
|
+
fixedDt: host.game?.loop.fixedDt ?? null,
|
|
223
|
+
ticks: raw.ticks,
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
debugRegistry.setVirtualInputTarget(this.id, {
|
|
227
|
+
setVirtualAction: (action, value) => input.setVirtualAction(action, value),
|
|
228
|
+
tapVirtualAction: (action) => input.tapVirtualAction(action),
|
|
229
|
+
clearVirtualActions: () => input.clearVirtualActions(),
|
|
230
|
+
scheduleActionAtTick: (tick, action, value) =>
|
|
231
|
+
input.scheduleActionAtTick(tick, action, value),
|
|
232
|
+
startInputRecording: () => input.startInputRecording(),
|
|
233
|
+
stopInputRecording: () => input.stopInputRecording(),
|
|
234
|
+
isInputRecording: () => input.isInputRecording(),
|
|
235
|
+
injectAxis: (sourceId, value) => input.injectAxis(sourceId, value),
|
|
236
|
+
injectVector2: (sourceId, value) => input.injectVector2(sourceId, value),
|
|
237
|
+
injectPointerDelta: (sourceId, delta) => input.injectPointerDelta(sourceId, delta),
|
|
238
|
+
injectPointerPosition: (sourceId, value) => input.injectPointerPosition(sourceId, value),
|
|
239
|
+
});
|
|
240
|
+
if (!host.game) input.setDebugEmit((event, detail) => ctx.debug?.emit(event, detail));
|
|
241
|
+
|
|
155
242
|
// T7.3 slice 1/2: the pixi world's components attach onto the SAME
|
|
156
243
|
// unified `ComponentManager` (`ecs/component-manager.ts`) the threejs
|
|
157
244
|
// path uses, constructed for kind `'pixijs'` — the world2d-silo manager
|
|
158
|
-
// is deleted (T7.3 slice 2).
|
|
159
|
-
//
|
|
160
|
-
// `
|
|
161
|
-
// need real 2D ctx fields (e.g. `Camera2DFollow`, which widens its own
|
|
162
|
-
// `update`'s ctx param to `GameContext | World2DContext` and narrows
|
|
163
|
-
// back down) still receive their real ctx, unchanged behavior — the
|
|
164
|
-
// same cast `test/mirror-tree-guard.test.ts` uses for its unit-level
|
|
165
|
-
// pixi manager. `createComponentManager`'s positional `physics`
|
|
245
|
+
// is deleted (T7.3 slice 2). The manager accepts this native
|
|
246
|
+
// `World2DContext`, so Pixi component lifecycles need no union/cast.
|
|
247
|
+
// `createComponentManager`'s positional `physics`
|
|
166
248
|
// parameter (the 3D `PhysicsRegistry`) is optional (T7.3 slice 2) and
|
|
167
249
|
// omitted here — a `'pixijs'`-kind manager never reads it (rigidBody/
|
|
168
250
|
// collider resolve from `physics2d` instead — see `resolvePhysicsRefs`
|
|
169
251
|
// in `ecs/component-manager.ts`).
|
|
170
|
-
ctx.components = createComponentManager(ctx
|
|
252
|
+
ctx.components = createComponentManager(ctx, undefined, {
|
|
171
253
|
kind: 'pixijs',
|
|
172
254
|
physics2d,
|
|
173
255
|
});
|
|
174
256
|
collisions2d.onCollision(
|
|
175
|
-
createTriggerDispatch2D(
|
|
176
|
-
physics2d,
|
|
177
|
-
ctx.components,
|
|
178
|
-
rapier2dWorld,
|
|
179
|
-
ctx as unknown as GameContext,
|
|
180
|
-
),
|
|
257
|
+
createTriggerDispatch2D(physics2d, ctx.components, rapier2dWorld, ctx),
|
|
181
258
|
);
|
|
182
259
|
|
|
183
260
|
// --- Load scene or run setup ---
|
|
184
261
|
let sceneInstance: Scene2DInstance | null = null;
|
|
185
262
|
let cleanup: { dispose: () => void } | null = null;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
263
|
+
let disposed = false;
|
|
264
|
+
systems.add('animation', (dt) => sceneInstance?.updateAnimations(dt), {
|
|
265
|
+
name: 'pixi.animated-sprites',
|
|
266
|
+
});
|
|
267
|
+
const disposeResources = (): void => {
|
|
268
|
+
if (disposed) return;
|
|
269
|
+
disposed = true;
|
|
270
|
+
const errors: unknown[] = [];
|
|
271
|
+
const attempt = (dispose: () => void): void => {
|
|
272
|
+
try {
|
|
273
|
+
dispose();
|
|
274
|
+
} catch (error) {
|
|
275
|
+
errors.push(error);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
attempt(() => cleanup?.dispose());
|
|
279
|
+
attempt(() => sceneInstance?.dispose());
|
|
280
|
+
attempt(() => ctx.components.clear());
|
|
281
|
+
attempt(() => debugRegistry.strip(this.id));
|
|
282
|
+
if (ownsInput) attempt(() => input.dispose());
|
|
283
|
+
attempt(() => physics2d.clear());
|
|
284
|
+
attempt(() => rapier2dWorld.free());
|
|
285
|
+
attempt(() => eventQueue.free());
|
|
286
|
+
attempt(() => surface.dispose());
|
|
287
|
+
if (errors.length > 0) {
|
|
288
|
+
throw new AggregateError(errors, `Failed to dispose Pixi world "${this.id}" cleanly`);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
try {
|
|
292
|
+
if (sceneData) {
|
|
293
|
+
sceneInstance = await loadScene2DFromData(sceneData, ctx, registry);
|
|
294
|
+
} else if (setup) {
|
|
295
|
+
cleanup = await setup(ctx);
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
try {
|
|
299
|
+
disposeResources();
|
|
300
|
+
} catch (cleanupError) {
|
|
301
|
+
throw new AggregateError(
|
|
302
|
+
[error, cleanupError],
|
|
303
|
+
`Pixi world "${this.id}" failed to mount and roll back cleanly`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
throw error;
|
|
190
307
|
}
|
|
191
308
|
|
|
192
309
|
return {
|
|
@@ -199,21 +316,15 @@ export class PixiSceneGameAdapter {
|
|
|
199
316
|
ctx,
|
|
200
317
|
update: (dt: number) => systems.run(dt),
|
|
201
318
|
resize: (w: number, h: number) => surface.resize(w, h),
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
319
|
+
// Bare adapters own and close their private input frame here. A
|
|
320
|
+
// game-hosted adapter shares the one game-owned manager, whose frame is
|
|
321
|
+
// closed once after every root has advanced.
|
|
205
322
|
frame: {
|
|
206
323
|
runPhase: (phase, dt) => systems.runPhase(phase, dt),
|
|
324
|
+
...(ownsInput ? { endFrame: () => input.endFrame() } : {}),
|
|
207
325
|
},
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
sceneInstance?.dispose();
|
|
211
|
-
ctx.components.clear();
|
|
212
|
-
physics2d.clear();
|
|
213
|
-
rapier2dWorld.free();
|
|
214
|
-
eventQueue.free();
|
|
215
|
-
surface.dispose();
|
|
216
|
-
},
|
|
326
|
+
systems: systemAdapters,
|
|
327
|
+
dispose: disposeResources,
|
|
217
328
|
};
|
|
218
329
|
}
|
|
219
330
|
}
|
|
@@ -298,6 +409,7 @@ export async function createWorld2DRuntime(
|
|
|
298
409
|
stop() {
|
|
299
410
|
loop.stop();
|
|
300
411
|
mounted.dispose();
|
|
412
|
+
game.dispose();
|
|
301
413
|
},
|
|
302
414
|
// D10/T7.6: route through the real `Game.play` control surface instead of
|
|
303
415
|
// `loop.timeScale = 0/1` — that starved the accumulator entirely (per D1,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Entity2D, Scene2DFile } from './schema/entity2d';
|
|
2
|
+
|
|
3
|
+
/** Resolve every entity to one collision-free identity without mutating source. */
|
|
4
|
+
export function resolveScene2DEntityIds(scene: Scene2DFile): Map<Entity2D, string> {
|
|
5
|
+
const authored = new Set<string>();
|
|
6
|
+
const collect = (entities: Entity2D[]): void => {
|
|
7
|
+
for (const entity of entities) {
|
|
8
|
+
if (entity.id) {
|
|
9
|
+
if (authored.has(entity.id)) throw new Error(`Duplicate world2d entity id: "${entity.id}"`);
|
|
10
|
+
authored.add(entity.id);
|
|
11
|
+
}
|
|
12
|
+
if (entity.children) collect(entity.children);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
collect(scene.entities);
|
|
16
|
+
|
|
17
|
+
const used = new Set(authored);
|
|
18
|
+
const result = new Map<Entity2D, string>();
|
|
19
|
+
const visit = (entities: Entity2D[], parentPath: readonly number[]): void => {
|
|
20
|
+
entities.forEach((entity, index) => {
|
|
21
|
+
const path = [...parentPath, index];
|
|
22
|
+
let id = entity.id;
|
|
23
|
+
if (!id) {
|
|
24
|
+
const base = `scn2d:${path.join('.')}`;
|
|
25
|
+
id = base;
|
|
26
|
+
let suffix = 1;
|
|
27
|
+
while (used.has(id)) id = `${base}~${suffix++}`;
|
|
28
|
+
used.add(id);
|
|
29
|
+
}
|
|
30
|
+
result.set(entity, id);
|
|
31
|
+
if (entity.children) visit(entity.children, path);
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
visit(scene.entities, []);
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Materialize the same identities the preview/hierarchy already project. */
|
|
39
|
+
export function materializeScene2DEntityIds(scene: Scene2DFile): boolean {
|
|
40
|
+
const ids = resolveScene2DEntityIds(scene);
|
|
41
|
+
let changed = false;
|
|
42
|
+
for (const [entity, id] of ids) {
|
|
43
|
+
if (!entity.id) {
|
|
44
|
+
entity.id = id;
|
|
45
|
+
changed = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return changed;
|
|
49
|
+
}
|