@vgai/engine 0.2.0 → 0.4.0-canary.20260715.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 +3 -1
- package/package.json +24 -4
- package/schemas/engine-api.json +124 -0
- package/schemas/engine-api.md +53 -0
- package/schemas/engine-capabilities.json +124 -0
- package/schemas/inputmap.schema.json +314 -0
- package/schemas/mat.schema.json +286 -0
- package/schemas/prefab.schema.json +10148 -0
- package/schemas/scn2d.schema.json +475 -0
- package/schemas/vgai-game.schema.json +383 -0
- package/schemas/vscn.schema.json +11007 -0
- package/src/adapter/{world-kind.ts → adapter-surface.ts} +6 -6
- package/src/adapter/authoring.ts +77 -0
- package/src/adapter/first-party-systems.ts +23 -34
- package/src/adapter/game-adapter.ts +8 -8
- package/src/adapter/host-context.ts +2 -4
- package/src/adapter/index.ts +4 -4
- package/src/adapter/system-adapter.ts +88 -22
- package/src/adapter/vgai-scene-game-adapter.ts +244 -194
- package/src/animation/anim-graph-types.ts +12 -43
- package/src/animation/animation-clock.ts +479 -0
- package/src/animation/camera-ownership.ts +467 -0
- package/src/animation/cinematic-cues.ts +451 -0
- package/src/animation/clip-map.ts +41 -0
- package/src/animation/gsap-registration.ts +184 -0
- package/src/animation/theatre-clock-binding.ts +111 -0
- package/src/animation/theatre-director.ts +347 -0
- package/src/animation/theatre-object-binding.ts +661 -0
- package/src/animation/xstate-animation-binding.ts +436 -0
- package/src/animation/xstate-animation-meta.ts +319 -0
- package/src/audio/index.ts +39 -7
- package/src/audio/tone-clock-binding.ts +98 -0
- package/src/audio/tone-context.ts +129 -0
- package/src/audio/tone-offline-render.ts +167 -0
- package/src/audio/wav-encode.ts +119 -0
- package/src/character/cloth-sim.ts +533 -0
- package/src/character/spring-chain.ts +307 -0
- package/src/core/game-loop.ts +57 -2
- package/src/core/seeded-random.ts +161 -0
- package/src/core/system-runner.ts +20 -3
- package/src/core/types.ts +50 -0
- package/src/data/data-asset.ts +167 -0
- package/src/data/data-check-core.ts +242 -0
- package/src/data/data-ref.ts +145 -0
- package/src/data/vite-plugin-data.ts +290 -0
- package/src/dev/performance-profiler.ts +213 -0
- package/src/dev/webgl-gpu-timer.ts +53 -0
- package/src/ecs/component-manager.ts +45 -12
- package/src/ecs/game-component.ts +95 -11
- package/src/humanoid/bake.operation.ts +326 -0
- package/src/humanoid/body.ts +663 -0
- package/src/humanoid/clips.ts +149 -0
- package/src/humanoid/compose.ts +209 -0
- package/src/humanoid/generate.ts +189 -0
- package/src/humanoid/index.ts +36 -0
- package/src/humanoid/schema.ts +108 -0
- package/src/humanoid/skeleton.ts +345 -0
- package/src/index.ts +48 -0
- package/src/input/input-manager.ts +1886 -33
- package/src/input/input-types.ts +158 -3
- package/src/input/prompt-labels.ts +122 -0
- package/src/input/rebind-controller.ts +105 -0
- package/src/input/schema.ts +206 -52
- package/src/manifest/index.ts +5 -5
- package/src/manifest/load.ts +125 -72
- package/src/manifest/schema.ts +362 -255
- package/src/react/game-state.tsx +135 -32
- package/src/react/root-adapter.tsx +49 -0
- package/src/react/unmanaged-root-detector.ts +66 -0
- package/src/react/use-data.ts +124 -0
- package/src/react/use-selection.tsx +135 -0
- package/src/runtime/create-runtime.ts +112 -273
- package/src/runtime/debug-bridge.ts +483 -0
- package/src/runtime/debug-registry.ts +856 -0
- package/src/runtime/game.ts +342 -93
- package/src/runtime/gameplay-rng-trap.ts +134 -0
- package/src/runtime/input-router.ts +7 -7
- package/src/runtime/mount-game.ts +40 -38
- package/src/runtime/mount-manifest.ts +169 -37
- package/src/runtime/render-audio-control.ts +168 -0
- package/src/runtime/render-control.ts +522 -0
- package/src/runtime/render-seed.ts +79 -0
- package/src/runtime/state-bridge.ts +24 -10
- package/src/runtime/types.ts +110 -33
- package/src/scene/asset-loaders.ts +10 -36
- package/src/scene/asset-paths.ts +0 -2
- package/src/scene/asset-ref-check.ts +248 -0
- package/src/scene/asset-registry.ts +22 -0
- package/src/scene/component-registry.ts +14 -3
- package/src/scene/defaults.ts +1 -0
- package/src/scene/light-camera-factory.ts +11 -3
- package/src/scene/parse.ts +133 -0
- package/src/scene/scene-apply.ts +55 -4
- package/src/scene/scene-loader.ts +91 -123
- package/src/scene/scene-types.ts +0 -1
- package/src/scene/schema/animation.ts +30 -79
- package/src/scene/schema/entity.ts +20 -0
- package/src/scene/schema/index.ts +2 -46
- package/src/scene/schema/light.ts +16 -1
- package/src/scene/schema/material.ts +96 -91
- package/src/scene/schema/scene-file.ts +1 -7
- package/src/scene/user-data.ts +22 -10
- package/src/setup/setup-renderer.ts +10 -3
- package/src/tools/define-tool.ts +191 -0
- package/src/world2d/authoring-2d.ts +17 -1
- package/src/world2d/collision-2d.ts +1 -1
- package/src/world2d/pixi-game-adapter.ts +19 -17
- package/src/world2d/scene2d-loader.ts +1 -0
- package/src/world2d/types.ts +8 -2
- package/src/animation/anim-graph.ts +0 -406
- package/src/animation/anim-system.ts +0 -28
- package/src/animation/property-track.ts +0 -178
- package/src/animation/schema.ts +0 -204
- package/src/audio/ambient.ts +0 -300
- package/src/audio/impacts.ts +0 -212
- package/src/audio/movement.ts +0 -140
- package/src/audio/musical.ts +0 -200
- package/src/audio/ui-sounds.ts +0 -171
- package/src/audio/vehicle.ts +0 -235
- package/src/audio/weapons.ts +0 -152
- package/src/runtime/scene-ui-bridge.ts +0 -86
- package/src/runtime/scene-ui-data.ts +0 -119
- package/src/scene/schema/ui.ts +0 -602
package/src/scene/parse.ts
CHANGED
|
@@ -23,7 +23,138 @@ export class SceneParseError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// D8 — removed-format migration guard: declarative property tracks
|
|
28
|
+
// (`animation.tracks`) were deleted (spec §3.2/§10 D8; Theatre — `@theatre/core`
|
|
29
|
+
// — is the sole authored continuous-animation source now). `SceneAnimationSchema`
|
|
30
|
+
// no longer declares a `tracks` field at all (so the generated JSON schema stays
|
|
31
|
+
// clean), which means Zod's default object mode would otherwise silently STRIP
|
|
32
|
+
// an authored `animation.tracks` rather than reporting it — violating Global AC
|
|
33
|
+
// §6 ("Removed formats fail with a concise migration/removal error rather than
|
|
34
|
+
// being partially read"). This walks the RAW pre-Zod JSON (Zod would already
|
|
35
|
+
// have stripped `tracks` by the time any schema-level check could see it) and
|
|
36
|
+
// throws a `SceneParseError` before Zod ever parses the data. Matches on the
|
|
37
|
+
// literal key `animation` (singular, an object) carrying a `tracks` property —
|
|
38
|
+
// this is deliberately narrower than Scene UI's unrelated, RETAINED
|
|
39
|
+
// `ui[].animations[].tracks` (plural `animations`), which this guard must not
|
|
40
|
+
// trip on.
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/** True when `value` is a plain object (not an array) carrying a `tracks` property — the shape
|
|
44
|
+
* `SceneAnimationSchema` used to declare before D8 deleted it. */
|
|
45
|
+
function isRemovedTracksAnimation(value: unknown): boolean {
|
|
46
|
+
return !!value && typeof value === 'object' && !Array.isArray(value) && 'tracks' in value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findRemovedPropertyTrackPathInArray(node: unknown[], path: string): string | undefined {
|
|
50
|
+
for (let i = 0; i < node.length; i++) {
|
|
51
|
+
const hit = findRemovedPropertyTrackPath(node[i], `${path}[${i}]`);
|
|
52
|
+
if (hit) return hit;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findRemovedPropertyTrackPathInObject(
|
|
58
|
+
obj: Record<string, unknown>,
|
|
59
|
+
path: string,
|
|
60
|
+
): string | undefined {
|
|
61
|
+
if (isRemovedTracksAnimation(obj['animation'])) {
|
|
62
|
+
return `${path ? `${path}.` : ''}animation.tracks`;
|
|
63
|
+
}
|
|
64
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
65
|
+
const hit = findRemovedPropertyTrackPath(value, path ? `${path}.${key}` : key);
|
|
66
|
+
if (hit) return hit;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function findRemovedPropertyTrackPath(node: unknown, path: string): string | undefined {
|
|
72
|
+
if (Array.isArray(node)) return findRemovedPropertyTrackPathInArray(node, path);
|
|
73
|
+
if (node && typeof node === 'object') {
|
|
74
|
+
return findRemovedPropertyTrackPathInObject(node as Record<string, unknown>, path);
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function assertNoRemovedPropertyTracks(json: unknown, filePath?: string): void {
|
|
80
|
+
const hit = findRemovedPropertyTrackPath(json, '');
|
|
81
|
+
if (!hit) return;
|
|
82
|
+
const issue: ZodIssue = {
|
|
83
|
+
code: 'custom',
|
|
84
|
+
message:
|
|
85
|
+
'property tracks were removed; author continuous animation with Theatre (@theatre/core) ' +
|
|
86
|
+
'instead — see docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §3.2/§10 D8.',
|
|
87
|
+
path: hit.split('.'),
|
|
88
|
+
};
|
|
89
|
+
throw new SceneParseError([issue], filePath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// E5 — removed-format migration guard: the AnimGraph state-machine runtime,
|
|
94
|
+
// `.animgraph.json` format, and `SceneAnimationSchema.animGraph` field were
|
|
95
|
+
// deleted (spec §3.3/§11 E5; XState + native Three `AnimationMixer` — see
|
|
96
|
+
// `xstate-animation-binding.ts` — replace it). `SceneAnimationSchema` no
|
|
97
|
+
// longer declares an `animGraph` field at all (so it carries zero trace in
|
|
98
|
+
// the generated JSON schema), which means Zod's default object mode would
|
|
99
|
+
// otherwise silently STRIP an authored `animation.animGraph` rather than
|
|
100
|
+
// reporting it — violating Global AC §6. Mirrors
|
|
101
|
+
// `assertNoRemovedPropertyTracks` above exactly: walks the RAW pre-Zod JSON
|
|
102
|
+
// and throws a `SceneParseError` before Zod ever parses the data.
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
/** True when `value` is a plain object (not an array) carrying an `animGraph` property — the
|
|
106
|
+
* shape `SceneAnimationSchema` used to declare before E5 deleted it. */
|
|
107
|
+
function isRemovedAnimGraphAnimation(value: unknown): boolean {
|
|
108
|
+
return !!value && typeof value === 'object' && !Array.isArray(value) && 'animGraph' in value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function findRemovedAnimGraphPathInArray(node: unknown[], path: string): string | undefined {
|
|
112
|
+
for (let i = 0; i < node.length; i++) {
|
|
113
|
+
const hit = findRemovedAnimGraphPath(node[i], `${path}[${i}]`);
|
|
114
|
+
if (hit) return hit;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function findRemovedAnimGraphPathInObject(
|
|
120
|
+
obj: Record<string, unknown>,
|
|
121
|
+
path: string,
|
|
122
|
+
): string | undefined {
|
|
123
|
+
if (isRemovedAnimGraphAnimation(obj['animation'])) {
|
|
124
|
+
return `${path ? `${path}.` : ''}animation.animGraph`;
|
|
125
|
+
}
|
|
126
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
127
|
+
const hit = findRemovedAnimGraphPath(value, path ? `${path}.${key}` : key);
|
|
128
|
+
if (hit) return hit;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function findRemovedAnimGraphPath(node: unknown, path: string): string | undefined {
|
|
134
|
+
if (Array.isArray(node)) return findRemovedAnimGraphPathInArray(node, path);
|
|
135
|
+
if (node && typeof node === 'object') {
|
|
136
|
+
return findRemovedAnimGraphPathInObject(node as Record<string, unknown>, path);
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function assertNoRemovedAnimGraph(json: unknown, filePath?: string): void {
|
|
142
|
+
const hit = findRemovedAnimGraphPath(json, '');
|
|
143
|
+
if (!hit) return;
|
|
144
|
+
const issue: ZodIssue = {
|
|
145
|
+
code: 'custom',
|
|
146
|
+
message:
|
|
147
|
+
'AnimGraph was removed; drive character animation with an XState animation machine instead ' +
|
|
148
|
+
'— see docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §3.3/§5.6/§11 E5 and ' +
|
|
149
|
+
'packages/engine/src/animation/xstate-animation-binding.ts.',
|
|
150
|
+
path: hit.split('.'),
|
|
151
|
+
};
|
|
152
|
+
throw new SceneParseError([issue], filePath);
|
|
153
|
+
}
|
|
154
|
+
|
|
26
155
|
export function parseSceneFile(json: unknown, filePath?: string): SceneFile {
|
|
156
|
+
assertNoRemovedPropertyTracks(json, filePath);
|
|
157
|
+
assertNoRemovedAnimGraph(json, filePath);
|
|
27
158
|
const result = SceneFileSchema.safeParse(json);
|
|
28
159
|
if (!result.success) {
|
|
29
160
|
throw new SceneParseError(result.error.issues, filePath);
|
|
@@ -36,6 +167,8 @@ export function parseSceneFile(json: unknown, filePath?: string): SceneFile {
|
|
|
36
167
|
}
|
|
37
168
|
|
|
38
169
|
export function parsePrefabFile(json: unknown, filePath?: string): PrefabFile {
|
|
170
|
+
assertNoRemovedPropertyTracks(json, filePath);
|
|
171
|
+
assertNoRemovedAnimGraph(json, filePath);
|
|
39
172
|
const result = PrefabFileSchema.safeParse(json);
|
|
40
173
|
if (!result.success) {
|
|
41
174
|
throw new SceneParseError(result.error.issues, filePath);
|
package/src/scene/scene-apply.ts
CHANGED
|
@@ -114,6 +114,49 @@ function cloneEntity(e: SceneEntity): SceneEntity {
|
|
|
114
114
|
return JSON.parse(JSON.stringify(e)) as SceneEntity;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/** True iff at least one entity in the tree (at any depth) carries a persisted `id`. */
|
|
118
|
+
function hasAnyPersistedId(entities: SceneEntity[]): boolean {
|
|
119
|
+
for (const e of entities) {
|
|
120
|
+
if (e.id) return true;
|
|
121
|
+
if (e.children && hasAnyPersistedId(e.children)) return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The id-persistence trap, made honest.
|
|
128
|
+
*
|
|
129
|
+
* `id` is `z.string().optional()` in the entity schema ("auto-generated if
|
|
130
|
+
* omitted"), and the ids an agent can actually DISCOVER — `vgai status`' —
|
|
131
|
+
* are the RUNTIME uuids a live editor session assigns to id-less entities on
|
|
132
|
+
* load (`editor-store.ts`'s `assignMissingIds`). `applyDiff` resolves ops
|
|
133
|
+
* against the FILE. So for any scene authored the normal way (hand-written,
|
|
134
|
+
* or scaffolded — none of which shipped with ids), the only ids on offer
|
|
135
|
+
* could never resolve here, and the failure read `entity not found` — as if
|
|
136
|
+
* the CALLER had typo'd the id. It is not a typo: NOTHING in that file could
|
|
137
|
+
* ever have matched.
|
|
138
|
+
*
|
|
139
|
+
* The durable half of the fix makes ids real (the editor now persists
|
|
140
|
+
* auto-assigned ids back to the scene file on load — see `loadDocument`), so
|
|
141
|
+
* this message is the safety net for a file that has not been through that
|
|
142
|
+
* path yet. It fires ONLY when the scene has zero persisted ids at all — a
|
|
143
|
+
* genuine bad-id typo against an id-carrying scene still gets the plain,
|
|
144
|
+
* unpadded message.
|
|
145
|
+
*/
|
|
146
|
+
const NO_IDS_HINT =
|
|
147
|
+
' — and this scene file has NO persisted entity ids AT ALL, so nothing in it could have ' +
|
|
148
|
+
'matched, whatever id the op named. This is almost certainly not a typo: `vgai status` ' +
|
|
149
|
+
'reports RUNTIME ids a live editor session assigns to id-less entities on load, and those ' +
|
|
150
|
+
'only exist in the FILE once the editor has written them back (it now does that ' +
|
|
151
|
+
'automatically, via autosave, shortly after the scene loads). Open this scene in the editor ' +
|
|
152
|
+
'(`vgai edit`), let it autosave, then re-read the ids with `vgai status` and retry. Do not ' +
|
|
153
|
+
'hand-write ids into the file.';
|
|
154
|
+
|
|
155
|
+
/** Append the no-ids explanation to a not-found message when (and only when) the file has no ids. */
|
|
156
|
+
function notFound(message: string, sceneHasIds: boolean): string {
|
|
157
|
+
return sceneHasIds ? message : message + NO_IDS_HINT;
|
|
158
|
+
}
|
|
159
|
+
|
|
117
160
|
/** Recursively search `list` (and descendants) for `id`; if found, splice it out (subtree intact) and return it. */
|
|
118
161
|
function detachById(list: SceneEntity[], id: string): SceneEntity | undefined {
|
|
119
162
|
for (let i = 0; i < list.length; i++) {
|
|
@@ -168,6 +211,9 @@ interface Placement {
|
|
|
168
211
|
export function applyDiff(scene: SceneFile, diff: SceneDiff): SceneFile {
|
|
169
212
|
const working: SceneFile = JSON.parse(JSON.stringify(scene)) as SceneFile;
|
|
170
213
|
const ops = diff.ops;
|
|
214
|
+
// Captured from the INPUT scene, before any op mutates `working` — see
|
|
215
|
+
// `NO_IDS_HINT` above for what this distinguishes and why.
|
|
216
|
+
const sceneHasIds = hasAnyPersistedId(scene.entities);
|
|
171
217
|
|
|
172
218
|
// --- Metadata / environment (independent of entity ops) ---
|
|
173
219
|
if (diff.metaChanged && diff.newName !== undefined) {
|
|
@@ -227,7 +273,12 @@ export function applyDiff(scene: SceneFile, diff: SceneDiff): SceneFile {
|
|
|
227
273
|
if (pendingMoves.size > 0) {
|
|
228
274
|
const i = [...pendingMoves][0]!;
|
|
229
275
|
const op = ops[i] as Extract<DiffOp, { type: 'move' }>;
|
|
230
|
-
throw new ApplyDiffError(
|
|
276
|
+
throw new ApplyDiffError(
|
|
277
|
+
notFound('entity not found (nothing to move)', sceneHasIds),
|
|
278
|
+
i,
|
|
279
|
+
'move',
|
|
280
|
+
op.id,
|
|
281
|
+
);
|
|
231
282
|
}
|
|
232
283
|
|
|
233
284
|
// --- Phase 2: removes (a not-found target is fine if it was already swept
|
|
@@ -243,7 +294,7 @@ export function applyDiff(scene: SceneFile, diff: SceneDiff): SceneFile {
|
|
|
243
294
|
}
|
|
244
295
|
if (sweptIds.has(op.id)) return;
|
|
245
296
|
throw new ApplyDiffError(
|
|
246
|
-
'entity not found (already removed via an ancestor, or an invalid id)',
|
|
297
|
+
notFound('entity not found (already removed via an ancestor, or an invalid id)', sceneHasIds),
|
|
247
298
|
i,
|
|
248
299
|
'remove',
|
|
249
300
|
op.id,
|
|
@@ -311,7 +362,7 @@ export function applyDiff(scene: SceneFile, diff: SceneDiff): SceneFile {
|
|
|
311
362
|
if (!parentNode) {
|
|
312
363
|
const bad = placements[0]!;
|
|
313
364
|
throw new ApplyDiffError(
|
|
314
|
-
`parent entity "${parentId}" not found`,
|
|
365
|
+
notFound(`parent entity "${parentId}" not found`, sceneHasIds),
|
|
315
366
|
bad.opIndex,
|
|
316
367
|
bad.opType,
|
|
317
368
|
bad.entityId,
|
|
@@ -341,7 +392,7 @@ export function applyDiff(scene: SceneFile, diff: SceneDiff): SceneFile {
|
|
|
341
392
|
if (op.type !== 'update') return;
|
|
342
393
|
const node = byId.get(op.id);
|
|
343
394
|
if (!node) {
|
|
344
|
-
throw new ApplyDiffError('entity not found', i, 'update', op.id);
|
|
395
|
+
throw new ApplyDiffError(notFound('entity not found', sceneHasIds), i, 'update', op.id);
|
|
345
396
|
}
|
|
346
397
|
const { id: _id, children: _children, ...fields } = op.entity;
|
|
347
398
|
for (const key of Object.keys(node) as (keyof SceneEntity)[]) {
|
|
@@ -3,8 +3,7 @@ import * as THREE from 'three';
|
|
|
3
3
|
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
|
|
4
4
|
import type { BatchedRenderer } from 'three.quarks';
|
|
5
5
|
import { initNavigation, NavMeshManager } from '../ai/navigation';
|
|
6
|
-
import {
|
|
7
|
-
import { PropertyTrackRunner } from '../animation/property-track';
|
|
6
|
+
import { buildClipMap } from '../animation/clip-map';
|
|
8
7
|
import { log } from '../dev/logger';
|
|
9
8
|
import type { ComponentManager } from '../ecs/component-manager';
|
|
10
9
|
import type { GameComponentClass } from '../ecs/game-component';
|
|
@@ -14,11 +13,10 @@ import { type RenderScope, resolveRenderSettings } from '../render/render-settin
|
|
|
14
13
|
import {
|
|
15
14
|
assetCacheSizes,
|
|
16
15
|
clearAssetCaches as clearSharedAssetCaches,
|
|
17
|
-
loadAnimGraphData,
|
|
18
16
|
loadGLTF,
|
|
19
17
|
resolveGltfNode,
|
|
20
18
|
} from './asset-loaders';
|
|
21
|
-
import { getAssetMeta, loadRegistry } from './asset-registry';
|
|
19
|
+
import { applyImportCorrection, getAssetMeta, loadRegistry } from './asset-registry';
|
|
22
20
|
import { computeColliderWorldDimensions, computeColliderWorldOffset } from './collider-dimensions';
|
|
23
21
|
import { applyComponents, type ComponentRegistry } from './component-registry';
|
|
24
22
|
import { DEFAULTS } from './defaults';
|
|
@@ -41,23 +39,22 @@ import {
|
|
|
41
39
|
type SceneMaterial,
|
|
42
40
|
type ScenePhysics,
|
|
43
41
|
type SceneSpline,
|
|
44
|
-
type UIRoot,
|
|
45
42
|
} from './scene-types';
|
|
46
43
|
import { isEntityRefSchema } from './schema/entity-ref';
|
|
47
44
|
import { getUserData, setUserData } from './user-data';
|
|
48
45
|
|
|
49
|
-
// GLTF / texture
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
46
|
+
// GLTF / texture load+cache logic lives in the shared ./asset-loaders module
|
|
47
|
+
// (A2) so the editor and runtime share one cache + one clone-safety contract.
|
|
48
|
+
// Only the IBL/skybox env-map cache stays here — it needs a WebGLRenderer
|
|
49
|
+
// (PMREM), which the shared module deliberately doesn't depend on.
|
|
53
50
|
const envTextureCache = new Map<string, THREE.Texture>();
|
|
54
51
|
let pmremGenerator: THREE.PMREMGenerator | null = null;
|
|
55
52
|
|
|
56
53
|
/**
|
|
57
|
-
* Clear module-level asset caches (textures, GLTF scenes,
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
54
|
+
* Clear module-level asset caches (textures, GLTF scenes, IBL env maps). Call
|
|
55
|
+
* on full runtime teardown to release cached GPU resources and avoid leaking
|
|
56
|
+
* across editor Play sessions. Delegates the GLTF/texture caches to
|
|
57
|
+
* {@link clearSharedAssetCaches} and additionally frees the local
|
|
61
58
|
* IBL env-map cache + PMREM generator.
|
|
62
59
|
*
|
|
63
60
|
* Lifetime / P1.8: these caches are intentionally module-level and persist
|
|
@@ -99,14 +96,12 @@ export function updateSceneLODs(scene: THREE.Scene, camera: THREE.Camera): void
|
|
|
99
96
|
export function __assetCacheSizesForTest(): {
|
|
100
97
|
textures: number;
|
|
101
98
|
gltf: number;
|
|
102
|
-
animGraphs: number;
|
|
103
99
|
env: number;
|
|
104
100
|
} {
|
|
105
101
|
const sizes = assetCacheSizes();
|
|
106
102
|
return {
|
|
107
103
|
textures: sizes.textures,
|
|
108
104
|
gltf: sizes.gltf,
|
|
109
|
-
animGraphs: sizes.animGraphs,
|
|
110
105
|
env: envTextureCache.size,
|
|
111
106
|
};
|
|
112
107
|
}
|
|
@@ -176,19 +171,6 @@ function createCurveFromSpline(spline: SceneSpline): THREE.CatmullRomCurve3 | nu
|
|
|
176
171
|
);
|
|
177
172
|
}
|
|
178
173
|
|
|
179
|
-
function buildClipMap(
|
|
180
|
-
animations: THREE.AnimationClip[],
|
|
181
|
-
aliases?: Record<string, string>,
|
|
182
|
-
): Map<string, THREE.AnimationClip> {
|
|
183
|
-
const clips = new Map<string, THREE.AnimationClip>();
|
|
184
|
-
for (const clip of animations) {
|
|
185
|
-
const raw = clip.name.split('|').pop()?.toLowerCase() ?? clip.name;
|
|
186
|
-
const name = aliases?.[raw] ?? raw;
|
|
187
|
-
clips.set(name, clip);
|
|
188
|
-
}
|
|
189
|
-
return clips;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
174
|
export interface SceneLoadContext {
|
|
193
175
|
scene: THREE.Scene;
|
|
194
176
|
rapierWorld: RAPIER.World;
|
|
@@ -205,10 +187,6 @@ export interface SceneLoadContext {
|
|
|
205
187
|
instancesFileCache?: Map<string, InstancesFile>;
|
|
206
188
|
/** Supply an AudioListener (attached to camera) to enable audio entity spawning. */
|
|
207
189
|
audioListener?: THREE.AudioListener;
|
|
208
|
-
/** AnimGraph registry keyed by Object3D. If provided, loader registers created
|
|
209
|
-
* AnimGraphs here (and skips tickFns to avoid double-ticking with the global
|
|
210
|
-
* animationSystem). */
|
|
211
|
-
animGraphs?: Map<THREE.Object3D, AnimGraph>;
|
|
212
190
|
/** ComponentManager for instantiating GameComponent classes from scene data. */
|
|
213
191
|
componentManager?: ComponentManager;
|
|
214
192
|
/** three.quarks BatchedRenderer. If provided, particle systems declared on
|
|
@@ -234,16 +212,6 @@ export interface SceneInstance {
|
|
|
234
212
|
/** Parsed scene environment, if any — lets the runtime apply post-processing
|
|
235
213
|
* (which needs the composer/camera the loader doesn't have). */
|
|
236
214
|
environment?: SceneEnvironment | undefined;
|
|
237
|
-
/**
|
|
238
|
-
* The scene file's top-level `ui` tree (React UI roots/canvases, A1),
|
|
239
|
-
* carried through verbatim from the parsed `SceneFile` by BOTH `loadScene`
|
|
240
|
-
* and `loadSceneFromData` (single source of truth — R8 item 7 / B). Opaque
|
|
241
|
-
* to the loader itself; the adapter passes it to `renderSceneUI` (the
|
|
242
|
-
* injected React renderer) so scenePath and sceneData mounts render the
|
|
243
|
-
* SAME scene-authored UI instead of only the sceneData branch special-
|
|
244
|
-
* casing it. `undefined` when the scene file has no `ui` field.
|
|
245
|
-
*/
|
|
246
|
-
ui?: UIRoot[] | undefined;
|
|
247
215
|
/**
|
|
248
216
|
* Authored entity id -> spawned Object3D (design/24-scene-ui.md D5). The
|
|
249
217
|
* SAME map `spawnEntity`/`createSceneJoints` build internally (id-less
|
|
@@ -351,15 +319,15 @@ function entityLabel(def: SceneEntity): string {
|
|
|
351
319
|
// ---------------------------------------------------------------------------
|
|
352
320
|
// T3.9 — bounded-concurrency asset prefetch.
|
|
353
321
|
//
|
|
354
|
-
// Child/asset fetches (prefab files, materials, GLTF models
|
|
355
|
-
//
|
|
322
|
+
// Child/asset fetches (prefab files, materials, GLTF models) used to only
|
|
323
|
+
// ever happen inline, one at a time, inside the fully-sequential
|
|
356
324
|
// `spawnEntity` recursion — fine at ~20 assets, product-breaking at hundreds
|
|
357
325
|
// (every fetch's network round-trip is paid serially). Rather than restructure
|
|
358
326
|
// the deterministic sequential spawn walk itself (which authored array order,
|
|
359
327
|
// tickFns/mixers order, and GameComponent registration order all depend on),
|
|
360
328
|
// this prefetch pass walks the WHOLE entity tree up front and warms the
|
|
361
|
-
// existing URL-keyed caches (asset-loaders.ts's module-level GLTF
|
|
362
|
-
//
|
|
329
|
+
// existing URL-keyed caches (asset-loaders.ts's module-level GLTF cache, and
|
|
330
|
+
// this module's ctx-level prefab/material file caches) with a
|
|
363
331
|
// small concurrency limiter. The sequential spawn pass below is completely
|
|
364
332
|
// unchanged — its `await`s just resolve instantly against a warm cache, so
|
|
365
333
|
// wall-clock time is bounded by ceil(assetCount / limit) round-trips instead
|
|
@@ -416,10 +384,6 @@ function collectPrefetchTasks(
|
|
|
416
384
|
const ref = def.materialRef;
|
|
417
385
|
tasks.push(() => loadMaterialFile(ref, ctx));
|
|
418
386
|
}
|
|
419
|
-
if (def.animation?.animGraph) {
|
|
420
|
-
const graph = def.animation.animGraph;
|
|
421
|
-
tasks.push(() => loadAnimGraphData(graph));
|
|
422
|
-
}
|
|
423
387
|
if (def.prefab && !ctx.prefabs?.has(def.prefab) && !seenPrefabUrls.has(def.prefab)) {
|
|
424
388
|
const prefabUrl = def.prefab;
|
|
425
389
|
seenPrefabUrls.add(prefabUrl);
|
|
@@ -502,6 +466,12 @@ async function instantiateScene(
|
|
|
502
466
|
ctx.scene.add(obj);
|
|
503
467
|
}
|
|
504
468
|
|
|
469
|
+
// Pass 1.5: animated attachments must resolve only after every entity and
|
|
470
|
+
// GLTF skeleton has spawned. Reparenting preserves the authored transform
|
|
471
|
+
// as the local socket offset; gameplay components remain on the same
|
|
472
|
+
// Object3D and therefore require no mirror or adapter entity.
|
|
473
|
+
resolveBoneAttachments(data.entities, idMap);
|
|
474
|
+
|
|
505
475
|
// Pass 2: create joints between physics bodies
|
|
506
476
|
createSceneJoints(data.entities, idMap, ctx);
|
|
507
477
|
|
|
@@ -545,7 +515,6 @@ async function instantiateScene(
|
|
|
545
515
|
navMesh,
|
|
546
516
|
cameras,
|
|
547
517
|
environment: data.environment,
|
|
548
|
-
ui: data.ui,
|
|
549
518
|
entities: idMap,
|
|
550
519
|
update(dt: number) {
|
|
551
520
|
for (const fn of tickFns) fn(dt);
|
|
@@ -693,20 +662,7 @@ async function spawnEntity(
|
|
|
693
662
|
}
|
|
694
663
|
|
|
695
664
|
// Apply import correction from asset registry
|
|
696
|
-
|
|
697
|
-
if (meta?.importCorrection) {
|
|
698
|
-
const ic = meta.importCorrection;
|
|
699
|
-
if (ic.scale !== undefined) {
|
|
700
|
-
content.scale.multiplyScalar(ic.scale);
|
|
701
|
-
}
|
|
702
|
-
if (ic.rotation) {
|
|
703
|
-
content.rotation.set(
|
|
704
|
-
THREE.MathUtils.degToRad(ic.rotation[0]),
|
|
705
|
-
THREE.MathUtils.degToRad(ic.rotation[1]),
|
|
706
|
-
THREE.MathUtils.degToRad(ic.rotation[2]),
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
665
|
+
applyImportCorrection(content, getAssetMeta(def.mesh.src)?.importCorrection);
|
|
710
666
|
|
|
711
667
|
// Wrap in a group so entity transform and import correction are separated.
|
|
712
668
|
// `content` carries import correction; wrapper carries entity transform.
|
|
@@ -714,40 +670,33 @@ async function spawnEntity(
|
|
|
714
670
|
wrapper.add(content);
|
|
715
671
|
object3d = wrapper;
|
|
716
672
|
|
|
717
|
-
// Set up animation if defined
|
|
673
|
+
// Set up animation if defined. Always build the mixer + clip map (and
|
|
674
|
+
// expose both via userData) whenever the entity has clips — not just
|
|
675
|
+
// when `autoplay` is set — so a GameComponent (e.g. an XState-driven
|
|
676
|
+
// character controller, E5's replacement for the removed AnimGraph) can
|
|
677
|
+
// pick up `_animMixer`/`_animClips` in its own `init()` and drive the
|
|
678
|
+
// SAME mixer via `bindXStateAnimation` (xstate-animation-binding.ts).
|
|
679
|
+
// Only `autoplay` self-ticks here (`tickFns`) — a component driving its
|
|
680
|
+
// own binding calls `mixer.update(dt)` itself (inside the binding's
|
|
681
|
+
// `tick`), so auto-ticking unconditionally would double-advance it.
|
|
718
682
|
if (def.animation && animations.length > 0) {
|
|
719
683
|
const clipMap = buildClipMap(animations, def.animation.clipAliases);
|
|
684
|
+
const mixer = new THREE.AnimationMixer(gltfScene);
|
|
685
|
+
setUserData(object3d, '_animMixer', mixer);
|
|
686
|
+
setUserData(object3d, '_animClips', clipMap);
|
|
687
|
+
setUserData(object3d, '_availableClips', [...clipMap.keys()]);
|
|
688
|
+
mixers.push(mixer);
|
|
720
689
|
|
|
721
|
-
if (def.animation.
|
|
722
|
-
// Full AnimGraph state machine
|
|
723
|
-
const graphData = await loadAnimGraphData(def.animation.animGraph);
|
|
724
|
-
const mixer = new THREE.AnimationMixer(gltfScene);
|
|
725
|
-
const graph = new AnimGraph(mixer, graphData, clipMap);
|
|
726
|
-
if (def.animation.parameters) {
|
|
727
|
-
for (const [key, value] of Object.entries(def.animation.parameters)) {
|
|
728
|
-
graph.setParameter(key, value);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
if (ctx.animGraphs) {
|
|
732
|
-
// Register in global map (keyed by Object3D); animationSystem ticks it.
|
|
733
|
-
ctx.animGraphs.set(object3d, graph);
|
|
734
|
-
} else {
|
|
735
|
-
// Standalone: tick via scene instance update
|
|
736
|
-
tickFns.push((dt) => graph.update(dt));
|
|
737
|
-
}
|
|
738
|
-
mixers.push(mixer);
|
|
739
|
-
} else if (def.animation.autoplay) {
|
|
690
|
+
if (def.animation.autoplay) {
|
|
740
691
|
// Simple clip autoplay
|
|
741
692
|
const clip = clipMap.get(def.animation.autoplay);
|
|
742
693
|
if (clip) {
|
|
743
|
-
const mixer = new THREE.AnimationMixer(gltfScene);
|
|
744
694
|
const action = mixer.clipAction(clip);
|
|
745
695
|
const loop = def.animation.loop ?? DEFAULTS.animation.loop;
|
|
746
696
|
action.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce, Infinity);
|
|
747
697
|
action.clampWhenFinished = !loop;
|
|
748
698
|
action.play();
|
|
749
699
|
tickFns.push((dt) => mixer.update(dt));
|
|
750
|
-
mixers.push(mixer);
|
|
751
700
|
}
|
|
752
701
|
}
|
|
753
702
|
}
|
|
@@ -914,17 +863,6 @@ async function spawnEntity(
|
|
|
914
863
|
// Visibility (mirrors editor: visible unless explicitly false)
|
|
915
864
|
object3d.visible = def.visible ?? DEFAULTS.entity.visible;
|
|
916
865
|
|
|
917
|
-
// F5 — declarative property tracks (docs/VSCN-STRUCTURAL-GAPS-DESIGN.md).
|
|
918
|
-
// Unlike gltf autoplay/animGraph above (clip-driven, gltf-only), tracks work
|
|
919
|
-
// on ANY entity — a box, a light, no gltf/clips required — so this lives
|
|
920
|
-
// OUTSIDE the gltf branch. Registered in the same tickFns list the
|
|
921
|
-
// AnimationMixers use, so it ticks on the same deterministic dt (honors
|
|
922
|
-
// pause/timeScale for free — no new clock).
|
|
923
|
-
if (def.animation?.tracks?.length) {
|
|
924
|
-
const runner = new PropertyTrackRunner(object3d, def.animation.tracks);
|
|
925
|
-
tickFns.push((dt) => runner.update(dt));
|
|
926
|
-
}
|
|
927
|
-
|
|
928
866
|
// Physics — stores {body, collider} in the physics registry keyed by object3d.
|
|
929
867
|
// T1.1: the body is created at the COMPOSED WORLD transform (ancestorCtx's
|
|
930
868
|
// world matrix × this entity's own local transform), not the raw authored
|
|
@@ -1135,6 +1073,14 @@ function createPhysicsBody(
|
|
|
1135
1073
|
}
|
|
1136
1074
|
if (col.isSensor) {
|
|
1137
1075
|
colliderDesc.setSensor(true);
|
|
1076
|
+
// Rapier's DEFAULT omits every non-dynamic pair. Player characters are
|
|
1077
|
+
// commonly position-based kinematic bodies and authored pickups commonly
|
|
1078
|
+
// use fixed sensors, so DEFAULT makes the pair invisible to the physics
|
|
1079
|
+
// event queue even after the KCC correctly excludes sensors from movement
|
|
1080
|
+
// blocking. A sensor's contract is observation, not contact response:
|
|
1081
|
+
// enable every body-type pair so real overlaps consistently reach
|
|
1082
|
+
// onTriggerEnter/Exit.
|
|
1083
|
+
colliderDesc.setActiveCollisionTypes(rapier.ActiveCollisionTypes.ALL);
|
|
1138
1084
|
}
|
|
1139
1085
|
// All scene-loaded colliders opt into collision events — sensors need them for
|
|
1140
1086
|
// onTriggerEnter/onTriggerExit, and solid colliders need them for onCollision
|
|
@@ -1161,6 +1107,42 @@ function walkEntities(entities: SceneEntity[], fn: (e: SceneEntity) => void): vo
|
|
|
1161
1107
|
}
|
|
1162
1108
|
}
|
|
1163
1109
|
|
|
1110
|
+
/** Reparent scene-authored entities to named bones/sockets in loaded GLTF skeletons. */
|
|
1111
|
+
function resolveBoneAttachments(entities: SceneEntity[], idMap: Map<string, THREE.Object3D>): void {
|
|
1112
|
+
walkEntities(entities, (def) => {
|
|
1113
|
+
const attachment = def.boneAttachment;
|
|
1114
|
+
if (!attachment || !def.id) return;
|
|
1115
|
+
|
|
1116
|
+
const object = idMap.get(def.id);
|
|
1117
|
+
if (!object) return;
|
|
1118
|
+
const target = idMap.get(attachment.target);
|
|
1119
|
+
if (!target) {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
`Bone attachment target "${attachment.target}" not found for entity "${entityLabel(def)}".`,
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
if (target === object) {
|
|
1125
|
+
throw new Error(`Entity "${entityLabel(def)}" cannot attach to its own skeleton.`);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const socket = target.getObjectByName(attachment.bone);
|
|
1129
|
+
if (!socket) {
|
|
1130
|
+
const available: string[] = [];
|
|
1131
|
+
target.traverse((node) => {
|
|
1132
|
+
if (node.name && node instanceof THREE.Bone) {
|
|
1133
|
+
available.push(node.name);
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
const suffix = available.length > 0 ? ` Available bones: ${available.sort().join(', ')}` : '';
|
|
1137
|
+
throw new Error(
|
|
1138
|
+
`Bone/socket "${attachment.bone}" was not found on target "${attachment.target}" for entity "${entityLabel(def)}".${suffix}`,
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
socket.add(object);
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1164
1146
|
/** Create Rapier joints for all entities that define them. */
|
|
1165
1147
|
function createSceneJoints(
|
|
1166
1148
|
entities: SceneEntity[],
|
|
@@ -1370,10 +1352,10 @@ export interface SpawnedPrefab {
|
|
|
1370
1352
|
/** The Object3D of the spawned prefab root (added to the scene). */
|
|
1371
1353
|
object3D: THREE.Object3D;
|
|
1372
1354
|
/**
|
|
1373
|
-
* Tick standalone
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1355
|
+
* Tick standalone `animation.autoplay` clips for this prefab. A GameComponent
|
|
1356
|
+
* that attaches its own XState-driven animation binding (E5 — see
|
|
1357
|
+
* `xstate-animation-binding.ts`) ticks that binding itself, so this is a
|
|
1358
|
+
* no-op for those. Call once per frame, mirroring SceneInstance.update.
|
|
1377
1359
|
*/
|
|
1378
1360
|
update(dt: number): void;
|
|
1379
1361
|
/** Stop any mixers this prefab created. */
|
|
@@ -1385,16 +1367,16 @@ export interface SpawnedPrefab {
|
|
|
1385
1367
|
*
|
|
1386
1368
|
* Returns a {@link SpawnedPrefab}. The previous version passed throwaway
|
|
1387
1369
|
* tickFns/mixers arrays, so a runtime-spawned prefab's `animation.autoplay`
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1370
|
+
* never advanced. Now the tick functions are captured and exposed via
|
|
1371
|
+
* `update()`, consistent with how loadScene's SceneInstance.update drives
|
|
1372
|
+
* animations.
|
|
1391
1373
|
*
|
|
1392
1374
|
* Usage:
|
|
1393
1375
|
* const crate = await spawnPrefab('/data/prefabs/crate.prefab.json', ctx, {
|
|
1394
1376
|
* transform: { position: [x, y, z] },
|
|
1395
1377
|
* components: { Health: { current: 50 } },
|
|
1396
1378
|
* });
|
|
1397
|
-
* // each frame (only needed for autoplay clips
|
|
1379
|
+
* // each frame (only needed for autoplay clips):
|
|
1398
1380
|
* crate.update(dt);
|
|
1399
1381
|
*/
|
|
1400
1382
|
export async function spawnPrefab(
|
|
@@ -1445,13 +1427,8 @@ export async function spawnPrefab(
|
|
|
1445
1427
|
// - mixers (THREE.AnimationMixer, simple `animation.autoplay` clips)
|
|
1446
1428
|
// DISPOSED — stopAllAction() on every mixer this spawn created
|
|
1447
1429
|
// (pre-existing).
|
|
1448
|
-
// - tickFns (standalone
|
|
1449
|
-
//
|
|
1450
|
-
// update() becomes a no-op (pre-existing).
|
|
1451
|
-
// - ctx.animGraphs (Object3D -> AnimGraph, when provided)
|
|
1452
|
-
// NEWLY-DISPOSED — every entry whose key is inside the spawned
|
|
1453
|
-
// subtree is deleted, so the global animationSystem stops ticking a
|
|
1454
|
-
// despawned graph (probe1: previously leaked forever).
|
|
1430
|
+
// - tickFns (standalone `animation.autoplay` mixer tick closures)
|
|
1431
|
+
// DISPOSED — array cleared so update() becomes a no-op (pre-existing).
|
|
1455
1432
|
// - ctx.particleRenderer (three.quarks BatchedRenderer, when provided)
|
|
1456
1433
|
// NEWLY-DISPOSED — every particle system tagged via the
|
|
1457
1434
|
// `_particleSystem` userData key (mirrors editor scene-sync's own
|
|
@@ -1469,7 +1446,7 @@ export async function spawnPrefab(
|
|
|
1469
1446
|
// tags on Object3Ds that get garbage-collected with the subtree once
|
|
1470
1447
|
// `ctx.scene.remove(object3D)` drops the last reference; there is no
|
|
1471
1448
|
// separate ctx-level registry entry to leak.
|
|
1472
|
-
// - GLTF/texture
|
|
1449
|
+
// - GLTF/texture asset caches (asset-loaders.ts, module-level)
|
|
1473
1450
|
// DELIBERATELY NOT DISPOSED — these are shared, URL-keyed caches
|
|
1474
1451
|
// across the whole runtime session (P1.8 contract: repeated
|
|
1475
1452
|
// spawns/loads of the same asset must NOT re-fetch or re-parse), not
|
|
@@ -1492,15 +1469,6 @@ export async function spawnPrefab(
|
|
|
1492
1469
|
ctx.rapierWorld.removeRigidBody(refs.body);
|
|
1493
1470
|
ctx.physics.remove(node);
|
|
1494
1471
|
});
|
|
1495
|
-
// Unregister any AnimGraphs this subtree registered into the shared,
|
|
1496
|
-
// Object3D-keyed ctx.animGraphs map — otherwise the global
|
|
1497
|
-
// animationSystem keeps ticking a despawned graph forever.
|
|
1498
|
-
if (ctx.animGraphs) {
|
|
1499
|
-
const animGraphs = ctx.animGraphs;
|
|
1500
|
-
object3D.traverse((node) => {
|
|
1501
|
-
animGraphs.delete(node);
|
|
1502
|
-
});
|
|
1503
|
-
}
|
|
1504
1472
|
// Unregister any particle systems this subtree registered with the
|
|
1505
1473
|
// shared BatchedRenderer — otherwise they keep simulating/rendering
|
|
1506
1474
|
// after the visual object is gone. `_particleSystem` is the same
|