@forgeax/engine-scene 0.1.4 → 0.1.7
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/.tsbuildinfo +1 -1
- package/dist/collect-subtree.d.ts +4 -0
- package/dist/collect-subtree.d.ts.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +169 -605
- package/dist/index.mjs.map +1 -1
- package/dist/instances/scene-instances.d.ts +9 -8
- package/dist/instances/scene-instances.d.ts.map +1 -1
- package/package.json +4 -5
- package/src/__tests__/asset-owner.integration.test.ts +15 -203
- package/src/collect-subtree.ts +30 -0
- package/src/index.ts +3 -16
- package/src/instances/scene-instances.ts +26 -27
- package/dist/assets/instantiate-scene.d.ts +0 -7
- package/dist/assets/instantiate-scene.d.ts.map +0 -1
- package/dist/assets/publication-observer.d.ts +0 -13
- package/dist/assets/publication-observer.d.ts.map +0 -1
- package/dist/assets/scene-projection.d.ts +0 -70
- package/dist/assets/scene-projection.d.ts.map +0 -1
- package/src/assets/instantiate-scene.ts +0 -14
- package/src/assets/publication-observer.ts +0 -24
- package/src/assets/scene-projection.ts +0 -644
|
@@ -1,644 +0,0 @@
|
|
|
1
|
-
import type { World } from '@forgeax/engine-ecs';
|
|
2
|
-
import { componentSchema } from '@forgeax/engine-ecs/internal';
|
|
3
|
-
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
4
|
-
import {
|
|
5
|
-
type BuiltinAssetKind,
|
|
6
|
-
type ComponentValuesMap,
|
|
7
|
-
err,
|
|
8
|
-
type Handle,
|
|
9
|
-
type MeshAsset,
|
|
10
|
-
type MountOverride,
|
|
11
|
-
ok,
|
|
12
|
-
type Result,
|
|
13
|
-
type SceneAsset,
|
|
14
|
-
type SceneEntity,
|
|
15
|
-
type SceneInstanceMount,
|
|
16
|
-
type SkinAsset,
|
|
17
|
-
toShared,
|
|
18
|
-
} from '@forgeax/engine-types';
|
|
19
|
-
import { worldSetSceneAssetResolver } from '../instances/scene-instances';
|
|
20
|
-
import { sceneAssetWireRefs } from './scene-decoder';
|
|
21
|
-
|
|
22
|
-
/** World resource key for the scene projection's skin dependency view. */
|
|
23
|
-
export const SCENE_ASSET_SKIN_RESOLVER_RESOURCE_KEY = 'SceneAssetSkinResolver';
|
|
24
|
-
|
|
25
|
-
/** World resource key for mesh-owned material default resolution. */
|
|
26
|
-
export const SCENE_ASSET_MESH_DEFAULT_RESOLVER_RESOURCE_KEY = 'SceneAssetMeshDefaultResolver';
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Scene projection output consumed by the renderer's post-instantiate hook.
|
|
30
|
-
* Scene owns the GUID-to-payload dependency view; ECS and Render only depend
|
|
31
|
-
* on this narrow resolver shape.
|
|
32
|
-
*/
|
|
33
|
-
export interface SceneAssetSkinResolver {
|
|
34
|
-
resolveSkinAsset(skeletonHandle: number): SkinAsset | undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Render-facing view of material defaults loaded while projecting a scene.
|
|
39
|
-
* MeshAsset keeps these defaults as GUID facts; Render consumes World handles
|
|
40
|
-
* without importing the AssetRegistry or teaching the scene component about
|
|
41
|
-
* renderer policy.
|
|
42
|
-
*/
|
|
43
|
-
export interface SceneAssetMeshDefaultResolver {
|
|
44
|
-
resolveMeshDefaultMaterial(guid: string): number | undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Errors raised while projecting a decoded SceneAsset into one World. */
|
|
48
|
-
export type SceneAssetProjectionError =
|
|
49
|
-
| {
|
|
50
|
-
readonly code: 'scene-reference-target-unsupported';
|
|
51
|
-
readonly expected: string;
|
|
52
|
-
readonly hint: string;
|
|
53
|
-
readonly detail: { readonly guid: string; readonly target: string; readonly field: string };
|
|
54
|
-
}
|
|
55
|
-
| {
|
|
56
|
-
readonly code: 'scene-reference-kind-mismatch';
|
|
57
|
-
readonly expected: string;
|
|
58
|
-
readonly hint: string;
|
|
59
|
-
readonly detail: {
|
|
60
|
-
readonly guid: string;
|
|
61
|
-
readonly target: string;
|
|
62
|
-
readonly expectedKind: BuiltinAssetKind;
|
|
63
|
-
readonly actualKind: string;
|
|
64
|
-
readonly field: string;
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
| {
|
|
68
|
-
readonly code: 'scene-reference-cycle';
|
|
69
|
-
readonly expected: string;
|
|
70
|
-
readonly hint: string;
|
|
71
|
-
readonly detail: { readonly cycle: readonly string[] };
|
|
72
|
-
}
|
|
73
|
-
| {
|
|
74
|
-
readonly code: 'scene-reference-unresolved';
|
|
75
|
-
readonly expected: string;
|
|
76
|
-
readonly hint: string;
|
|
77
|
-
readonly detail: { readonly source: string };
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
/** Loader seam supplied by the current App-owned AssetRegistry. */
|
|
81
|
-
export type SceneAssetReferenceLoader<E = unknown> = (
|
|
82
|
-
guid: string,
|
|
83
|
-
kind: BuiltinAssetKind,
|
|
84
|
-
) => Promise<Result<unknown, E>>;
|
|
85
|
-
|
|
86
|
-
const ASSET_KIND_BY_TARGET: Readonly<Record<string, BuiltinAssetKind>> = Object.freeze({
|
|
87
|
-
MeshAsset: 'mesh',
|
|
88
|
-
TextureAsset: 'texture',
|
|
89
|
-
EquirectAsset: 'equirect',
|
|
90
|
-
SamplerAsset: 'sampler',
|
|
91
|
-
MaterialAsset: 'material',
|
|
92
|
-
SceneAsset: 'scene',
|
|
93
|
-
AudioClipAsset: 'audio',
|
|
94
|
-
SkinAsset: 'skin',
|
|
95
|
-
SkeletonAsset: 'skeleton',
|
|
96
|
-
AnimationClip: 'animation-clip',
|
|
97
|
-
AnimationGraph: 'animation-graph',
|
|
98
|
-
FontAsset: 'font',
|
|
99
|
-
RenderPipelineAsset: 'render-pipeline',
|
|
100
|
-
TilesetAsset: 'tileset',
|
|
101
|
-
VideoAsset: 'video',
|
|
102
|
-
ParticleEffectAsset: 'particle-effect',
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
type ProjectError<E> = E | SceneAssetProjectionError;
|
|
106
|
-
|
|
107
|
-
interface ProjectionState<E> {
|
|
108
|
-
readonly world: World;
|
|
109
|
-
readonly load: SceneAssetReferenceLoader<E>;
|
|
110
|
-
readonly assetHandles: Map<string, Handle<string, 'shared'>>;
|
|
111
|
-
readonly sceneHandles: Map<string, Handle<'SceneAsset', 'shared'>>;
|
|
112
|
-
readonly skinStore: SceneAssetProjectionStore;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
interface SceneAssetProjectionStore {
|
|
116
|
-
readonly skeletonGuidByHandle: Map<number, string>;
|
|
117
|
-
readonly skinByGuid: Map<string, SkinAsset>;
|
|
118
|
-
readonly skinBySkeletonGuid: Map<string, SkinAsset>;
|
|
119
|
-
readonly resolver: SceneAssetSkinResolver;
|
|
120
|
-
readonly materialHandleByGuid: Map<string, number>;
|
|
121
|
-
readonly meshDefaultResolver: SceneAssetMeshDefaultResolver;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const projectionStores = new WeakMap<World, SceneAssetProjectionStore>();
|
|
125
|
-
|
|
126
|
-
function projectionStoreFor(world: World): SceneAssetProjectionStore {
|
|
127
|
-
const current = projectionStores.get(world);
|
|
128
|
-
if (current !== undefined) return current;
|
|
129
|
-
|
|
130
|
-
const skeletonGuidByHandle = new Map<number, string>();
|
|
131
|
-
const skinByGuid = new Map<string, SkinAsset>();
|
|
132
|
-
const skinBySkeletonGuid = new Map<string, SkinAsset>();
|
|
133
|
-
const materialHandleByGuid = new Map<string, number>();
|
|
134
|
-
const resolver: SceneAssetSkinResolver = {
|
|
135
|
-
resolveSkinAsset(skeletonHandle) {
|
|
136
|
-
const skeletonGuid = skeletonGuidByHandle.get(skeletonHandle);
|
|
137
|
-
return skeletonGuid === undefined ? undefined : skinBySkeletonGuid.get(skeletonGuid);
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
const meshDefaultResolver: SceneAssetMeshDefaultResolver = {
|
|
141
|
-
resolveMeshDefaultMaterial(guid) {
|
|
142
|
-
return materialHandleByGuid.get(guid.toLowerCase());
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
const store = {
|
|
146
|
-
skeletonGuidByHandle,
|
|
147
|
-
skinByGuid,
|
|
148
|
-
skinBySkeletonGuid,
|
|
149
|
-
resolver,
|
|
150
|
-
materialHandleByGuid,
|
|
151
|
-
meshDefaultResolver,
|
|
152
|
-
};
|
|
153
|
-
projectionStores.set(world, store);
|
|
154
|
-
world.insertResource(SCENE_ASSET_SKIN_RESOLVER_RESOURCE_KEY, resolver);
|
|
155
|
-
world.insertResource(SCENE_ASSET_MESH_DEFAULT_RESOLVER_RESOURCE_KEY, meshDefaultResolver);
|
|
156
|
-
return store;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function projectionError(
|
|
160
|
-
code: SceneAssetProjectionError['code'],
|
|
161
|
-
detail: SceneAssetProjectionError['detail'],
|
|
162
|
-
): SceneAssetProjectionError {
|
|
163
|
-
switch (code) {
|
|
164
|
-
case 'scene-reference-target-unsupported':
|
|
165
|
-
return {
|
|
166
|
-
code,
|
|
167
|
-
expected: 'every shared SceneAsset field target maps to a built-in Asset kind',
|
|
168
|
-
hint: 'register the target in the AssetTagMap before projecting the scene',
|
|
169
|
-
detail: detail as Extract<SceneAssetProjectionError, { code: typeof code }>['detail'],
|
|
170
|
-
};
|
|
171
|
-
case 'scene-reference-kind-mismatch':
|
|
172
|
-
return {
|
|
173
|
-
code,
|
|
174
|
-
expected: 'the loaded payload kind matches the component shared-handle target',
|
|
175
|
-
hint: 'repair the scene refs[] edge or republish the referenced asset',
|
|
176
|
-
detail: detail as Extract<SceneAssetProjectionError, { code: typeof code }>['detail'],
|
|
177
|
-
};
|
|
178
|
-
case 'scene-reference-cycle':
|
|
179
|
-
return {
|
|
180
|
-
code,
|
|
181
|
-
expected: 'an acyclic SceneAsset mount graph',
|
|
182
|
-
hint: 'remove the circular mount.source reference and republish the scenes',
|
|
183
|
-
detail: detail as Extract<SceneAssetProjectionError, { code: typeof code }>['detail'],
|
|
184
|
-
};
|
|
185
|
-
case 'scene-reference-unresolved':
|
|
186
|
-
return {
|
|
187
|
-
code,
|
|
188
|
-
expected: 'a mount.source GUID that is loadable by the current AssetRegistry',
|
|
189
|
-
hint: 'publish the child SceneAsset before projecting its parent',
|
|
190
|
-
detail: detail as Extract<SceneAssetProjectionError, { code: typeof code }>['detail'],
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function isPayloadOfKind(
|
|
196
|
-
value: unknown,
|
|
197
|
-
kind: BuiltinAssetKind,
|
|
198
|
-
): value is { readonly kind: string } {
|
|
199
|
-
return (
|
|
200
|
-
typeof value === 'object' &&
|
|
201
|
-
value !== null &&
|
|
202
|
-
(value as { readonly kind?: unknown }).kind === kind
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function sharedTarget(fieldType: unknown): string | undefined {
|
|
207
|
-
if (typeof fieldType !== 'string') return undefined;
|
|
208
|
-
const match = /^shared<([^>]+)>$/.exec(fieldType);
|
|
209
|
-
return match?.[1];
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function schemaFieldType(field: unknown): unknown {
|
|
213
|
-
if (typeof field === 'object' && field !== null && 'type' in field) {
|
|
214
|
-
return (field as { readonly type?: unknown }).type;
|
|
215
|
-
}
|
|
216
|
-
return field;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function sharedArrayTarget(fieldType: unknown): string | undefined {
|
|
220
|
-
if (typeof fieldType !== 'string') return undefined;
|
|
221
|
-
const match = /^array<shared<([^>]+)>(?:,\s*\d+)? *>$/.exec(fieldType);
|
|
222
|
-
return match?.[1];
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function resolveWireValue<E>(
|
|
226
|
-
value: unknown,
|
|
227
|
-
refs: readonly string[] | undefined,
|
|
228
|
-
location: string,
|
|
229
|
-
): Result<unknown, ProjectError<E>> {
|
|
230
|
-
if (refs === undefined || typeof value !== 'number') return ok(value);
|
|
231
|
-
const guid = refs[value];
|
|
232
|
-
if (!Number.isInteger(value) || guid === undefined) {
|
|
233
|
-
return err(
|
|
234
|
-
projectionError('scene-reference-unresolved', {
|
|
235
|
-
source: `${location} refs[${value}]`,
|
|
236
|
-
}),
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
return ok(guid);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async function projectSharedValue<E>(
|
|
243
|
-
state: ProjectionState<E>,
|
|
244
|
-
value: unknown,
|
|
245
|
-
target: string,
|
|
246
|
-
field: string,
|
|
247
|
-
): Promise<Result<unknown, ProjectError<E>>> {
|
|
248
|
-
if (typeof value !== 'string') {
|
|
249
|
-
if (target === 'MeshAsset' && typeof value === 'number') {
|
|
250
|
-
const resolved = state.world.sharedRefs.resolve(toShared<'MeshAsset'>(value));
|
|
251
|
-
if (resolved.ok && isPayloadOfKind(resolved.value, 'mesh')) {
|
|
252
|
-
const meshDefaults = await projectMeshDefaults(state, resolved.value as MeshAsset, field);
|
|
253
|
-
if (!meshDefaults.ok) return meshDefaults;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return ok(value);
|
|
257
|
-
}
|
|
258
|
-
const kind = ASSET_KIND_BY_TARGET[target];
|
|
259
|
-
if (kind === undefined) {
|
|
260
|
-
return err(
|
|
261
|
-
projectionError('scene-reference-target-unsupported', {
|
|
262
|
-
guid: value,
|
|
263
|
-
target,
|
|
264
|
-
field,
|
|
265
|
-
}),
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const cacheKey = `${target}:${value.toLowerCase()}`;
|
|
270
|
-
const cached = state.assetHandles.get(cacheKey);
|
|
271
|
-
if (cached !== undefined) {
|
|
272
|
-
if (target === 'SkeletonAsset') {
|
|
273
|
-
state.skinStore.skeletonGuidByHandle.set(Number(cached), value.toLowerCase());
|
|
274
|
-
}
|
|
275
|
-
return ok(cached);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const loaded = await state.load(value, kind);
|
|
279
|
-
if (!loaded.ok) return loaded;
|
|
280
|
-
if (!isPayloadOfKind(loaded.value, kind)) {
|
|
281
|
-
return err(
|
|
282
|
-
projectionError('scene-reference-kind-mismatch', {
|
|
283
|
-
guid: value,
|
|
284
|
-
target,
|
|
285
|
-
expectedKind: kind,
|
|
286
|
-
actualKind:
|
|
287
|
-
typeof loaded.value === 'object' && loaded.value !== null && 'kind' in loaded.value
|
|
288
|
-
? String((loaded.value as { readonly kind: unknown }).kind)
|
|
289
|
-
: typeof loaded.value,
|
|
290
|
-
field,
|
|
291
|
-
}),
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (target === 'MeshAsset') {
|
|
296
|
-
const meshDefaults = await projectMeshDefaults(state, loaded.value as MeshAsset, field);
|
|
297
|
-
if (!meshDefaults.ok) return meshDefaults;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const handle = state.world.allocSharedRef(target, loaded.value);
|
|
301
|
-
state.assetHandles.set(cacheKey, handle);
|
|
302
|
-
if (target === 'SkeletonAsset') {
|
|
303
|
-
state.skinStore.skeletonGuidByHandle.set(Number(handle), value.toLowerCase());
|
|
304
|
-
}
|
|
305
|
-
return ok(handle);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function projectMeshDefaults<E>(
|
|
309
|
-
state: ProjectionState<E>,
|
|
310
|
-
mesh: MeshAsset,
|
|
311
|
-
field: string,
|
|
312
|
-
): Promise<Result<void, ProjectError<E>>> {
|
|
313
|
-
if (!Array.isArray(mesh.materialSlots)) return ok(undefined);
|
|
314
|
-
for (let slotIndex = 0; slotIndex < mesh.materialSlots.length; slotIndex += 1) {
|
|
315
|
-
const defaultMaterial = mesh.materialSlots[slotIndex]?.defaultMaterial;
|
|
316
|
-
if (defaultMaterial === undefined) continue;
|
|
317
|
-
const guid = AssetGuid.format(defaultMaterial);
|
|
318
|
-
const projected = await projectSharedValue(
|
|
319
|
-
state,
|
|
320
|
-
guid,
|
|
321
|
-
'MaterialAsset',
|
|
322
|
-
`${field}.materialSlots[${slotIndex}].defaultMaterial`,
|
|
323
|
-
);
|
|
324
|
-
if (!projected.ok) return projected;
|
|
325
|
-
state.skinStore.materialHandleByGuid.set(guid, Number(projected.value));
|
|
326
|
-
}
|
|
327
|
-
return ok(undefined);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
async function projectSkinDependencies<E>(
|
|
331
|
-
state: ProjectionState<E>,
|
|
332
|
-
asset: SceneAsset,
|
|
333
|
-
): Promise<Result<void, ProjectError<E>>> {
|
|
334
|
-
for (let index = 0; index < (asset.skinGuids?.length ?? 0); index += 1) {
|
|
335
|
-
const guid = asset.skinGuids?.[index];
|
|
336
|
-
if (guid === undefined) continue;
|
|
337
|
-
const guidKey = guid.toLowerCase();
|
|
338
|
-
if (state.skinStore.skinByGuid.has(guidKey)) continue;
|
|
339
|
-
|
|
340
|
-
const loaded = await state.load(guid, 'skin');
|
|
341
|
-
if (!loaded.ok) return loaded;
|
|
342
|
-
if (!isPayloadOfKind(loaded.value, 'skin')) {
|
|
343
|
-
return err(
|
|
344
|
-
projectionError('scene-reference-kind-mismatch', {
|
|
345
|
-
guid,
|
|
346
|
-
target: 'SkinAsset',
|
|
347
|
-
expectedKind: 'skin',
|
|
348
|
-
actualKind:
|
|
349
|
-
typeof loaded.value === 'object' && loaded.value !== null && 'kind' in loaded.value
|
|
350
|
-
? String((loaded.value as { readonly kind: unknown }).kind)
|
|
351
|
-
: typeof loaded.value,
|
|
352
|
-
field: `scene.skinGuids[${index}]`,
|
|
353
|
-
}),
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
const skin = loaded.value as SkinAsset;
|
|
357
|
-
state.skinStore.skinByGuid.set(guidKey, skin);
|
|
358
|
-
state.skinStore.skinBySkeletonGuid.set(skin.skeletonGuid.toLowerCase(), skin);
|
|
359
|
-
}
|
|
360
|
-
return ok(undefined);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
async function projectFields<E>(
|
|
364
|
-
state: ProjectionState<E>,
|
|
365
|
-
componentName: string,
|
|
366
|
-
rawFields: Record<string, unknown>,
|
|
367
|
-
location: string,
|
|
368
|
-
wireRefs: readonly string[] | undefined,
|
|
369
|
-
): Promise<Result<Record<string, unknown>, ProjectError<E>>> {
|
|
370
|
-
const component = state.world.components.resolve(componentName);
|
|
371
|
-
if (component === undefined) return ok({ ...rawFields });
|
|
372
|
-
|
|
373
|
-
const fields: Record<string, unknown> = { ...rawFields };
|
|
374
|
-
for (const [fieldName, value] of Object.entries(rawFields)) {
|
|
375
|
-
const fieldType = schemaFieldType(componentSchema(component)[fieldName]);
|
|
376
|
-
const target = sharedTarget(fieldType);
|
|
377
|
-
if (target !== undefined) {
|
|
378
|
-
const wireValue = await resolveWireValue<E>(
|
|
379
|
-
value,
|
|
380
|
-
wireRefs,
|
|
381
|
-
`${location}.${componentName}.${fieldName}`,
|
|
382
|
-
);
|
|
383
|
-
if (!wireValue.ok) return wireValue;
|
|
384
|
-
const projected = await projectSharedValue(
|
|
385
|
-
state,
|
|
386
|
-
wireValue.value,
|
|
387
|
-
target,
|
|
388
|
-
`${location}.${componentName}.${fieldName}`,
|
|
389
|
-
);
|
|
390
|
-
if (!projected.ok) return projected;
|
|
391
|
-
fields[fieldName] = projected.value;
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const arrayTarget = sharedArrayTarget(fieldType);
|
|
396
|
-
if (arrayTarget === undefined || !Array.isArray(value)) continue;
|
|
397
|
-
const projectedValues: unknown[] = [];
|
|
398
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
399
|
-
const wireValue = await resolveWireValue<E>(
|
|
400
|
-
value[index],
|
|
401
|
-
wireRefs,
|
|
402
|
-
`${location}.${componentName}.${fieldName}[${index}]`,
|
|
403
|
-
);
|
|
404
|
-
if (!wireValue.ok) return wireValue;
|
|
405
|
-
const projected = await projectSharedValue(
|
|
406
|
-
state,
|
|
407
|
-
wireValue.value,
|
|
408
|
-
arrayTarget,
|
|
409
|
-
`${location}.${componentName}.${fieldName}[${index}]`,
|
|
410
|
-
);
|
|
411
|
-
if (!projected.ok) return projected;
|
|
412
|
-
projectedValues.push(projected.value);
|
|
413
|
-
}
|
|
414
|
-
fields[fieldName] = projectedValues;
|
|
415
|
-
}
|
|
416
|
-
return ok(fields);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
async function projectComponents<E>(
|
|
420
|
-
state: ProjectionState<E>,
|
|
421
|
-
components: Partial<ComponentValuesMap>,
|
|
422
|
-
location: string,
|
|
423
|
-
wireRefs: readonly string[] | undefined,
|
|
424
|
-
): Promise<Result<Partial<ComponentValuesMap>, ProjectError<E>>> {
|
|
425
|
-
const projected: Record<string, Record<string, unknown>> = {};
|
|
426
|
-
for (const [componentName, rawFields] of Object.entries(components)) {
|
|
427
|
-
if (typeof rawFields !== 'object' || rawFields === null || Array.isArray(rawFields)) continue;
|
|
428
|
-
const fields = await projectFields(
|
|
429
|
-
state,
|
|
430
|
-
componentName,
|
|
431
|
-
rawFields as Record<string, unknown>,
|
|
432
|
-
location,
|
|
433
|
-
wireRefs,
|
|
434
|
-
);
|
|
435
|
-
if (!fields.ok) return fields;
|
|
436
|
-
projected[componentName] = fields.value;
|
|
437
|
-
}
|
|
438
|
-
return ok(projected);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
async function projectOverride<E>(
|
|
442
|
-
state: ProjectionState<E>,
|
|
443
|
-
override: MountOverride,
|
|
444
|
-
index: number,
|
|
445
|
-
wireRefs: readonly string[] | undefined,
|
|
446
|
-
): Promise<Result<MountOverride, ProjectError<E>>> {
|
|
447
|
-
const component = state.world.components.resolve(override.comp);
|
|
448
|
-
if (component === undefined) return ok(override);
|
|
449
|
-
|
|
450
|
-
if (override.field !== undefined) {
|
|
451
|
-
const fieldType = schemaFieldType(componentSchema(component)[override.field]);
|
|
452
|
-
const target = sharedTarget(fieldType);
|
|
453
|
-
if (target !== undefined) {
|
|
454
|
-
const wireValue = await resolveWireValue<E>(
|
|
455
|
-
override.value,
|
|
456
|
-
wireRefs,
|
|
457
|
-
`mount override ${index}.${override.comp}.${override.field}`,
|
|
458
|
-
);
|
|
459
|
-
if (!wireValue.ok) return wireValue;
|
|
460
|
-
const value = await projectSharedValue(
|
|
461
|
-
state,
|
|
462
|
-
wireValue.value,
|
|
463
|
-
target,
|
|
464
|
-
`mount override ${index}.${override.comp}.${override.field}`,
|
|
465
|
-
);
|
|
466
|
-
if (!value.ok) return value;
|
|
467
|
-
return ok({ ...override, value: value.value });
|
|
468
|
-
}
|
|
469
|
-
const arrayTarget = sharedArrayTarget(fieldType);
|
|
470
|
-
if (arrayTarget !== undefined && Array.isArray(override.value)) {
|
|
471
|
-
const values: unknown[] = [];
|
|
472
|
-
for (let element = 0; element < override.value.length; element += 1) {
|
|
473
|
-
const wireValue = await resolveWireValue<E>(
|
|
474
|
-
override.value[element],
|
|
475
|
-
wireRefs,
|
|
476
|
-
`mount override ${index}.${override.comp}.${override.field}[${element}]`,
|
|
477
|
-
);
|
|
478
|
-
if (!wireValue.ok) return wireValue;
|
|
479
|
-
const value = await projectSharedValue(
|
|
480
|
-
state,
|
|
481
|
-
wireValue.value,
|
|
482
|
-
arrayTarget,
|
|
483
|
-
`mount override ${index}.${override.comp}.${override.field}[${element}]`,
|
|
484
|
-
);
|
|
485
|
-
if (!value.ok) return value;
|
|
486
|
-
values.push(value.value);
|
|
487
|
-
}
|
|
488
|
-
return ok({ ...override, value: values });
|
|
489
|
-
}
|
|
490
|
-
return ok(override);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (
|
|
494
|
-
typeof override.value !== 'object' ||
|
|
495
|
-
override.value === null ||
|
|
496
|
-
Array.isArray(override.value)
|
|
497
|
-
) {
|
|
498
|
-
return ok(override);
|
|
499
|
-
}
|
|
500
|
-
const value = await projectFields(
|
|
501
|
-
state,
|
|
502
|
-
override.comp,
|
|
503
|
-
override.value as Record<string, unknown>,
|
|
504
|
-
`mount override ${index}`,
|
|
505
|
-
wireRefs,
|
|
506
|
-
);
|
|
507
|
-
if (!value.ok) return value;
|
|
508
|
-
return ok({ ...override, value: value.value });
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
async function projectMount<E>(
|
|
512
|
-
state: ProjectionState<E>,
|
|
513
|
-
mount: SceneInstanceMount,
|
|
514
|
-
index: number,
|
|
515
|
-
visiting: Set<string>,
|
|
516
|
-
wireRefs: readonly string[] | undefined,
|
|
517
|
-
): Promise<Result<SceneInstanceMount, ProjectError<E>>> {
|
|
518
|
-
const components =
|
|
519
|
-
mount.components === undefined
|
|
520
|
-
? undefined
|
|
521
|
-
: await projectComponents(state, mount.components, `mount ${index}`, wireRefs);
|
|
522
|
-
if (components !== undefined && !components.ok) return components;
|
|
523
|
-
|
|
524
|
-
const overrides: MountOverride[] = [];
|
|
525
|
-
for (let overrideIndex = 0; overrideIndex < (mount.overrides?.length ?? 0); overrideIndex += 1) {
|
|
526
|
-
const override = mount.overrides?.[overrideIndex];
|
|
527
|
-
if (override === undefined) continue;
|
|
528
|
-
const projected = await projectOverride(state, override, overrideIndex, wireRefs);
|
|
529
|
-
if (!projected.ok) return projected;
|
|
530
|
-
overrides.push(projected.value);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (typeof mount.source !== 'string') {
|
|
534
|
-
return ok({
|
|
535
|
-
...mount,
|
|
536
|
-
...(components === undefined ? {} : { components: components.value }),
|
|
537
|
-
...(mount.overrides === undefined ? {} : { overrides }),
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const sourceKey = mount.source.toLowerCase();
|
|
542
|
-
const cached = state.sceneHandles.get(sourceKey);
|
|
543
|
-
if (cached !== undefined) {
|
|
544
|
-
return ok({
|
|
545
|
-
...mount,
|
|
546
|
-
source: cached as unknown as number,
|
|
547
|
-
...(components === undefined ? {} : { components: components.value }),
|
|
548
|
-
...(mount.overrides === undefined ? {} : { overrides }),
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
if (visiting.has(sourceKey)) {
|
|
552
|
-
return err(
|
|
553
|
-
projectionError('scene-reference-cycle', {
|
|
554
|
-
cycle: [...visiting, sourceKey],
|
|
555
|
-
}),
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const loaded = await state.load(mount.source, 'scene');
|
|
560
|
-
if (!loaded.ok) return loaded;
|
|
561
|
-
if (!isPayloadOfKind(loaded.value, 'scene')) {
|
|
562
|
-
return err(projectionError('scene-reference-unresolved', { source: mount.source }));
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
visiting.add(sourceKey);
|
|
566
|
-
const projectedChild = await projectScene(state, loaded.value as SceneAsset, visiting);
|
|
567
|
-
visiting.delete(sourceKey);
|
|
568
|
-
if (!projectedChild.ok) return projectedChild;
|
|
569
|
-
|
|
570
|
-
const childHandle = state.world.allocSharedRef('SceneAsset', projectedChild.value);
|
|
571
|
-
state.sceneHandles.set(sourceKey, childHandle);
|
|
572
|
-
return ok({
|
|
573
|
-
...mount,
|
|
574
|
-
source: childHandle as unknown as number,
|
|
575
|
-
...(components === undefined ? {} : { components: components.value }),
|
|
576
|
-
...(mount.overrides === undefined ? {} : { overrides }),
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
async function projectScene<E>(
|
|
581
|
-
state: ProjectionState<E>,
|
|
582
|
-
asset: SceneAsset,
|
|
583
|
-
visiting: Set<string>,
|
|
584
|
-
): Promise<Result<SceneAsset, ProjectError<E>>> {
|
|
585
|
-
const skins = await projectSkinDependencies(state, asset);
|
|
586
|
-
if (!skins.ok) return skins;
|
|
587
|
-
|
|
588
|
-
const entities: SceneEntity[] = [];
|
|
589
|
-
const wireRefs = sceneAssetWireRefs(asset);
|
|
590
|
-
for (const entity of asset.entities) {
|
|
591
|
-
const components = await projectComponents(
|
|
592
|
-
state,
|
|
593
|
-
entity.components,
|
|
594
|
-
`entity ${entity.localId as number}`,
|
|
595
|
-
wireRefs,
|
|
596
|
-
);
|
|
597
|
-
if (!components.ok) return components;
|
|
598
|
-
entities.push({ localId: entity.localId, components: components.value });
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const mounts: SceneInstanceMount[] = [];
|
|
602
|
-
for (let index = 0; index < (asset.mounts?.length ?? 0); index += 1) {
|
|
603
|
-
const mount = asset.mounts?.[index];
|
|
604
|
-
if (mount === undefined) continue;
|
|
605
|
-
const projected = await projectMount(state, mount, index, visiting, wireRefs);
|
|
606
|
-
if (!projected.ok) return projected;
|
|
607
|
-
mounts.push(projected.value);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
return ok({
|
|
611
|
-
kind: 'scene',
|
|
612
|
-
entities,
|
|
613
|
-
...(asset.mounts === undefined ? {} : { mounts }),
|
|
614
|
-
...(asset.skinGuids === undefined ? {} : { skinGuids: [...asset.skinGuids] }),
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* Convert decoded GUID fields and nested SceneAsset mounts into references
|
|
620
|
-
* owned by one World. The AssetRegistry remains a loader; this World-facing
|
|
621
|
-
* projection belongs to Scene and is intentionally explicit at each consumer.
|
|
622
|
-
*/
|
|
623
|
-
export async function projectSceneAsset<E = unknown>(
|
|
624
|
-
world: World,
|
|
625
|
-
asset: SceneAsset,
|
|
626
|
-
load: SceneAssetReferenceLoader<E>,
|
|
627
|
-
): Promise<Result<SceneAsset, ProjectError<E>>> {
|
|
628
|
-
const skinStore = projectionStoreFor(world);
|
|
629
|
-
worldSetSceneAssetResolver(world, (source) => {
|
|
630
|
-
if (typeof source === 'number') return ok(toShared<'SceneAsset'>(source));
|
|
631
|
-
return err(projectionError('scene-reference-unresolved', { source }));
|
|
632
|
-
});
|
|
633
|
-
return projectScene(
|
|
634
|
-
{
|
|
635
|
-
world,
|
|
636
|
-
load,
|
|
637
|
-
assetHandles: new Map(),
|
|
638
|
-
sceneHandles: new Map(),
|
|
639
|
-
skinStore,
|
|
640
|
-
},
|
|
641
|
-
asset,
|
|
642
|
-
new Set(),
|
|
643
|
-
);
|
|
644
|
-
}
|