@forgeax/engine-gltf 0.1.4 → 0.1.6
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 +18 -11
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/extension-admission.unit.test.d.ts +2 -0
- package/dist/__tests__/extension-admission.unit.test.d.ts.map +1 -0
- package/dist/__tests__/source-key-diagnostics.integration.test.d.ts +2 -0
- package/dist/__tests__/source-key-diagnostics.integration.test.d.ts.map +1 -0
- package/dist/bridge.d.ts +5 -6
- package/dist/bridge.d.ts.map +1 -1
- package/dist/check-extensions.d.ts.map +1 -1
- package/dist/cli-gltf.mjs +32 -16
- package/dist/cli-gltf.mjs.map +1 -1
- package/dist/gltf-importer.d.ts.map +1 -1
- package/dist/importer-entry.mjs +5 -1
- package/dist/importer-entry.mjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +31 -10
- package/dist/index.mjs.map +1 -1
- package/dist/node-file-entry.mjs +5 -1
- package/dist/node-file-entry.mjs.map +1 -1
- package/dist/parse-gltf.d.ts.map +1 -1
- package/dist/source-key.d.ts +26 -4
- package/dist/source-key.d.ts.map +1 -1
- package/package.json +13 -15
- package/src/__tests__/extension-admission.unit.test.ts +19 -0
- package/src/__tests__/gltf.unit.test.ts +1 -1
- package/src/__tests__/material-texture-transform.unit.test.ts +30 -0
- package/src/__tests__/meshopt-real-matrix.integration.test.ts +108 -24
- package/src/__tests__/morph-import.integration.test.ts +31 -44
- package/src/__tests__/source-key-diagnostics.integration.test.ts +140 -0
- package/src/__tests__/source-key-producer.integration.test.ts +2 -2
- package/src/bridge.ts +9 -10
- package/src/check-extensions.ts +8 -4
- package/src/cli-gltf.ts +1 -6
- package/src/gltf-importer.ts +1 -2
- package/src/index.ts +1 -0
- package/src/parse-gltf.ts +7 -5
- package/src/source-key.ts +51 -17
- package/dist/__tests__/asset-runtime-fixture.d.ts +0 -14
- package/dist/__tests__/asset-runtime-fixture.d.ts.map +0 -1
- package/src/__tests__/asset-runtime-fixture.ts +0 -278
|
@@ -86,7 +86,7 @@ describe('glTF producer source-key boundary', () => {
|
|
|
86
86
|
expect(result.ok).toBe(false);
|
|
87
87
|
if (result.ok) return;
|
|
88
88
|
expect(result.error.code).toBe('duplicate-source-key');
|
|
89
|
-
expect(result.error.sourceIndices).toEqual([0, 1]);
|
|
89
|
+
expect(result.error.detail.sourceIndices).toEqual([0, 1]);
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
it('applies the same preflight to duplicate material names', () => {
|
|
@@ -95,7 +95,7 @@ describe('glTF producer source-key boundary', () => {
|
|
|
95
95
|
expect(result.ok).toBe(false);
|
|
96
96
|
if (result.ok) return;
|
|
97
97
|
expect(result.error.code).toBe('duplicate-source-key');
|
|
98
|
-
expect(result.error.sourceIndices).toEqual([0, 1]);
|
|
98
|
+
expect(result.error.detail.sourceIndices).toEqual([0, 1]);
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
it('publishes producer-valid output when semantic identity is unique', () => {
|
package/src/bridge.ts
CHANGED
|
@@ -424,17 +424,16 @@ export interface GltfBridgeContext {
|
|
|
424
424
|
/**
|
|
425
425
|
* glTF skin index -> SkeletonAsset GUID (string form). When a GltfNodeIr carries
|
|
426
426
|
* a skin reference, the bridge emits `Skin: { skeleton: <guid-string> }` on
|
|
427
|
-
* that node's entity;
|
|
428
|
-
* at
|
|
427
|
+
* that node's entity; AssetRegistry._resolveSceneGuids resolves the GUID to
|
|
428
|
+
* a runtime Handle at instantiate time (same protocol as MeshFilter and
|
|
429
429
|
* MeshRenderer.materials[]). Optional — skinless glTFs pass an empty Map
|
|
430
430
|
* (or omit the field) and the bridge does not emit Skin.
|
|
431
431
|
*
|
|
432
432
|
* tweak-20260611 M6 / D-7: emitting Skin from the bridge means the standard
|
|
433
|
-
*
|
|
434
|
-
* path Just Works for skinned glTF;
|
|
433
|
+
* loadByGuid<SceneAsset> + instantiate path Just Works for skinned glTF;
|
|
435
434
|
* demos no longer need to runtime-parseGlb + post-load patch the SceneAsset.
|
|
436
|
-
* postSpawnResolveJoints
|
|
437
|
-
* spawn subtree to fill Skin.joints[].
|
|
435
|
+
* postSpawnResolveJoints (called from AssetRegistry.instantiate) walks the
|
|
436
|
+
* matching SkinAsset.jointPaths against the spawn subtree to fill Skin.joints[].
|
|
438
437
|
*/
|
|
439
438
|
readonly skeletonGuidBySkinIndex?: ReadonlyMap<number, string>;
|
|
440
439
|
}
|
|
@@ -648,10 +647,10 @@ export function gltfDocToSceneAsset(doc: GltfDoc, ctx: GltfBridgeContext): Scene
|
|
|
648
647
|
}
|
|
649
648
|
// tweak-20260611 M6: when this node references a glTF skin, stamp a
|
|
650
649
|
// Skin component carrying the SkeletonAsset GUID as a string. The
|
|
651
|
-
//
|
|
652
|
-
// instantiate time (same protocol as MeshFilter/MeshRenderer).
|
|
653
|
-
// postSpawnResolveJoints
|
|
654
|
-
// SkinAsset.jointPaths
|
|
650
|
+
// runtime AssetRegistry._resolveSceneGuids resolves the string to a
|
|
651
|
+
// Handle at instantiate time (same protocol as MeshFilter/MeshRenderer).
|
|
652
|
+
// postSpawnResolveJoints (called from AssetRegistry.instantiate) then
|
|
653
|
+
// fills Skin.joints[] by walking the matching SkinAsset.jointPaths
|
|
655
654
|
// against the spawn subtree's Name index.
|
|
656
655
|
if (ir.skinIndex !== null && ctx.skeletonGuidBySkinIndex !== undefined) {
|
|
657
656
|
const skeletonGuid = ctx.skeletonGuidBySkinIndex.get(ir.skinIndex);
|
package/src/check-extensions.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// check-extensions.ts - KHR / vendor extension gate.
|
|
2
2
|
//
|
|
3
|
-
// v1 required-extension support contains EXT_mesh_gpu_instancing
|
|
4
|
-
// KHR_lights_punctual. The exported legacy list remains the original mesh
|
|
3
|
+
// v1 required-extension support contains EXT_mesh_gpu_instancing,
|
|
4
|
+
// KHR_lights_punctual, and KHR_texture_transform. The exported legacy list remains the original mesh
|
|
5
5
|
// extension list for callers that display the v1 mesh-only surface.
|
|
6
6
|
// (feat-20260518-gltf-instancing-and-name-component plan-strategy section
|
|
7
7
|
// 2 D-1 / D-3). Any extension listed in `extensionsRequired[]` outside
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// material). A stderr warn for those would be a false positive, so the
|
|
16
16
|
// diagnostics list is the single channel (no `console.error`).
|
|
17
17
|
//
|
|
18
|
-
// Future expansion (KHR_materials_unlit,
|
|
18
|
+
// Future expansion (KHR_materials_unlit, ...) extends
|
|
19
19
|
// `EXTENSION_ALLOWLIST` in place; each addition lands under its own feat-*
|
|
20
20
|
// loop with breaking-change registry entry.
|
|
21
21
|
|
|
@@ -26,7 +26,11 @@ export const EXTENSION_ALLOWLIST: readonly string[] = [
|
|
|
26
26
|
'EXT_mesh_gpu_instancing',
|
|
27
27
|
'EXT_meshopt_compression',
|
|
28
28
|
];
|
|
29
|
-
const SUPPORTED_EXTENSIONS: readonly string[] = [
|
|
29
|
+
const SUPPORTED_EXTENSIONS: readonly string[] = [
|
|
30
|
+
...EXTENSION_ALLOWLIST,
|
|
31
|
+
'KHR_lights_punctual',
|
|
32
|
+
'KHR_texture_transform',
|
|
33
|
+
];
|
|
30
34
|
|
|
31
35
|
export interface ExtensionsCheckResult {
|
|
32
36
|
/** Names listed in extensionsUsed but not in the allowlist. */
|
package/src/cli-gltf.ts
CHANGED
|
@@ -314,12 +314,7 @@ async function runWrite(target: string, ctx: AssetCtx): Promise<number> {
|
|
|
314
314
|
sourceRelative,
|
|
315
315
|
);
|
|
316
316
|
if (!pack.ok) {
|
|
317
|
-
return emitError(ctx,
|
|
318
|
-
code: pack.error.code,
|
|
319
|
-
expected: pack.error.expected,
|
|
320
|
-
hint: pack.error.hint,
|
|
321
|
-
detail: { source: sourceRelative, sourceIndices: pack.error.sourceIndices },
|
|
322
|
-
});
|
|
317
|
+
return emitError(ctx, pack.error);
|
|
323
318
|
}
|
|
324
319
|
await writeFile(metaPath, serializeMetaJson(pack.value.meta), 'utf-8');
|
|
325
320
|
return 0;
|
package/src/gltf-importer.ts
CHANGED
|
@@ -531,8 +531,7 @@ async function importGltf(
|
|
|
531
531
|
// share `sourceIndex` (toAssetPack emits 1:1 per GltfSkeletonRecord) but live
|
|
532
532
|
// as distinct `kind` sub-assets, so the SkinAsset GUIDs differ from the
|
|
533
533
|
// SkeletonAsset GUIDs. The scene branch below appends these GUIDs to both
|
|
534
|
-
// its refs[] (the runtime recursion source:
|
|
535
|
-
// sceneAssetKind) walks
|
|
534
|
+
// its refs[] (the runtime recursion source: loadByGuid<SceneAsset> walks
|
|
536
535
|
// envelope.refs to recursively pull every SkinAsset before instantiate) and
|
|
537
536
|
// its payload.skinGuids (the reverse-decode hint) -- without the refs[] edge,
|
|
538
537
|
// browser-async-pack-fetch never loads SkinAssets and Skin.joints stays
|
package/src/index.ts
CHANGED
package/src/parse-gltf.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface MeshJson {
|
|
|
58
58
|
// === Tier-B v1 IR (GltfDoc) ===
|
|
59
59
|
//
|
|
60
60
|
// Shape: a denormalised view of the parsed glTF JSON, post-validation,
|
|
61
|
-
// suitable for downstream `toAssetPack` and
|
|
61
|
+
// suitable for downstream `toAssetPack` and runtime AssetRegistry
|
|
62
62
|
// hand-off. Math types are POD (number tuples), no Vec3/Quat brand at
|
|
63
63
|
// the boundary (charter proposition 5).
|
|
64
64
|
|
|
@@ -1582,8 +1582,8 @@ export function toAssetPack(
|
|
|
1582
1582
|
// requirements G-2 / AC-13). Orphan images (declared but unreferenced by
|
|
1583
1583
|
// any `textures[]` row) still produce a sub-asset; the importer assigns
|
|
1584
1584
|
// them colorSpace 'linear' (no colour-encoded purpose inferable). Without
|
|
1585
|
-
// this loop the meta carries no `kind: 'texture'` row,
|
|
1586
|
-
// never imports the bytes, and the
|
|
1585
|
+
// this loop the meta carries no `kind: 'texture'` row, AssetRegistry
|
|
1586
|
+
// never imports the bytes, and the runtime renders a white box (G-2).
|
|
1587
1587
|
const images = doc.images ?? [];
|
|
1588
1588
|
for (let i = 0; i < images.length; i++) {
|
|
1589
1589
|
const img = images[i];
|
|
@@ -1680,10 +1680,12 @@ export function toAssetPack(
|
|
|
1680
1680
|
if (!reconciled.ok) {
|
|
1681
1681
|
return err({
|
|
1682
1682
|
code: 'mesh-material-slot-topology-change',
|
|
1683
|
-
sourceIndices: reconciled.error.nextIndices,
|
|
1684
|
-
previousIndices: reconciled.error.previousIndices,
|
|
1685
1683
|
expected: `unambiguous material slot identity for mesh ${meshOutput.guid}`,
|
|
1686
1684
|
hint: reconciled.error.hint,
|
|
1685
|
+
detail: {
|
|
1686
|
+
sourceIndices: reconciled.error.nextIndices,
|
|
1687
|
+
previousIndices: reconciled.error.previousIndices,
|
|
1688
|
+
},
|
|
1687
1689
|
});
|
|
1688
1690
|
}
|
|
1689
1691
|
sourceOverrides[meshOutput.sourceKey] = {
|
package/src/source-key.ts
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
1
|
import type { GltfDocItemLike } from './sub-asset-key.js';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
readonly code:
|
|
5
|
-
| 'missing-source-key'
|
|
6
|
-
| 'duplicate-source-key'
|
|
7
|
-
| 'ambiguous-source-key'
|
|
8
|
-
| 'mesh-material-slot-topology-change';
|
|
9
|
-
readonly sourceIndices: readonly number[];
|
|
3
|
+
interface GltfSourceKeyErrorBase {
|
|
10
4
|
readonly expected: string;
|
|
11
5
|
readonly hint: string;
|
|
12
|
-
readonly previousIndices?: readonly number[];
|
|
13
6
|
}
|
|
14
7
|
|
|
8
|
+
export interface GltfSourceKeyConflictEntry {
|
|
9
|
+
readonly kind: string;
|
|
10
|
+
readonly name: string | null;
|
|
11
|
+
readonly sourceIndex: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type GltfSourceKeyError =
|
|
15
|
+
| (GltfSourceKeyErrorBase & {
|
|
16
|
+
readonly code: 'missing-source-key';
|
|
17
|
+
readonly detail: { readonly sourceIndices: readonly number[] };
|
|
18
|
+
})
|
|
19
|
+
| (GltfSourceKeyErrorBase & {
|
|
20
|
+
readonly code: 'duplicate-source-key' | 'ambiguous-source-key';
|
|
21
|
+
readonly detail: {
|
|
22
|
+
readonly key: string;
|
|
23
|
+
readonly sourceIndices: readonly number[];
|
|
24
|
+
readonly entries: readonly GltfSourceKeyConflictEntry[];
|
|
25
|
+
};
|
|
26
|
+
})
|
|
27
|
+
| (GltfSourceKeyErrorBase & {
|
|
28
|
+
readonly code: 'mesh-material-slot-topology-change';
|
|
29
|
+
readonly detail: {
|
|
30
|
+
readonly sourceIndices: readonly number[];
|
|
31
|
+
readonly previousIndices: readonly number[];
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
15
35
|
export type GltfSourceKeyErrorCode = GltfSourceKeyError['code'];
|
|
16
36
|
|
|
17
37
|
export type GltfSourceKeyResult =
|
|
@@ -39,33 +59,47 @@ export function deriveGltfSourceKeys(items: readonly GltfDocItemLike[]): GltfSou
|
|
|
39
59
|
ok: false,
|
|
40
60
|
error: {
|
|
41
61
|
code: 'missing-source-key',
|
|
42
|
-
sourceIndices: missing,
|
|
43
62
|
expected: 'every glTF output needs a stable semantic kind or name',
|
|
44
63
|
hint: 'publish a semantic kind/name key; do not use sourceIndex',
|
|
64
|
+
detail: { sourceIndices: missing },
|
|
45
65
|
},
|
|
46
66
|
};
|
|
47
67
|
}
|
|
48
68
|
|
|
49
|
-
const seen = new Map<string,
|
|
69
|
+
const seen = new Map<string, GltfDocItemLike>();
|
|
50
70
|
for (let index = 0; index < keys.length; index++) {
|
|
51
71
|
const key = keys[index];
|
|
52
72
|
if (key === undefined) continue;
|
|
73
|
+
const current = items[index];
|
|
74
|
+
if (current === undefined) continue;
|
|
53
75
|
const prior = seen.get(key);
|
|
54
76
|
if (prior !== undefined) {
|
|
77
|
+
const entry = (item: GltfDocItemLike): GltfSourceKeyConflictEntry => {
|
|
78
|
+
const name = item.name?.trim();
|
|
79
|
+
return {
|
|
80
|
+
kind: item.kind.trim(),
|
|
81
|
+
name: name === undefined || name.length === 0 ? null : name,
|
|
82
|
+
sourceIndex: item.sourceIndex,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const ambiguous = key === current.kind.trim();
|
|
55
86
|
return {
|
|
56
87
|
ok: false,
|
|
57
88
|
error: {
|
|
58
|
-
code:
|
|
59
|
-
sourceIndices: [prior, items[index]?.sourceIndex ?? 0],
|
|
89
|
+
code: ambiguous ? 'ambiguous-source-key' : 'duplicate-source-key',
|
|
60
90
|
expected: 'sourceKey values must be unique within one glTF package',
|
|
61
|
-
hint:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
91
|
+
hint: ambiguous
|
|
92
|
+
? 'name each otherwise anonymous output; do not use sourceIndex'
|
|
93
|
+
: 'rename duplicate outputs before publishing topology facts',
|
|
94
|
+
detail: {
|
|
95
|
+
key,
|
|
96
|
+
sourceIndices: [prior.sourceIndex, current.sourceIndex],
|
|
97
|
+
entries: [entry(prior), entry(current)],
|
|
98
|
+
},
|
|
65
99
|
},
|
|
66
100
|
};
|
|
67
101
|
}
|
|
68
|
-
seen.set(key,
|
|
102
|
+
seen.set(key, current);
|
|
69
103
|
}
|
|
70
104
|
|
|
71
105
|
return { ok: true, keys: keys as string[], conflicts: [] };
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { type AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
2
|
-
import { type DdcPack } from '@forgeax/engine-import';
|
|
3
|
-
import type { AssetDecoder, MeshAsset } from '@forgeax/engine-types';
|
|
4
|
-
type FixtureAsset = DdcPack['assets'][number];
|
|
5
|
-
export interface AssetRuntimeFixtureOptions {
|
|
6
|
-
readonly packageUrl?: string;
|
|
7
|
-
readonly artifactOverrides?: ReadonlyMap<string, Uint8Array>;
|
|
8
|
-
}
|
|
9
|
-
export declare const normalizedMeshAssetDecoder: AssetDecoder<MeshAsset>;
|
|
10
|
-
export declare function installMeshDecoder(assets: AssetRegistry): import("@forgeax/engine-types").AssetDecoderLease;
|
|
11
|
-
export declare function installPassthroughDecoder(assets: AssetRegistry, kind: string): import("@forgeax/engine-types").AssetDecoderLease;
|
|
12
|
-
export declare function createAssetRuntimeFixture(sourceAssets: readonly FixtureAsset[], options?: AssetRuntimeFixtureOptions): AssetRegistry;
|
|
13
|
-
export {};
|
|
14
|
-
//# sourceMappingURL=asset-runtime-fixture.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"asset-runtime-fixture.d.ts","sourceRoot":"","sources":["../../src/__tests__/asset-runtime-fixture.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,aAAa,EAGnB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAE,KAAK,OAAO,EAAoB,MAAM,wBAAwB,CAAC;AACxE,OAAO,KAAK,EACV,YAAY,EAGZ,SAAS,EAIV,MAAM,uBAAuB,CAAC;AAwB/B,KAAK,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9C,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC9D;AAoID,eAAO,MAAM,0BAA0B,EAAE,YAAY,CAAC,SAAS,CAqB9D,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,qDAEvD;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,qDAS5E;AAED,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,SAAS,YAAY,EAAE,EACrC,OAAO,GAAE,0BAA+B,GACvC,aAAa,CA2Df"}
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
3
|
-
type AssetRegistry,
|
|
4
|
-
createAssetRegistry,
|
|
5
|
-
createCatalogSource,
|
|
6
|
-
} from '@forgeax/engine-assets-runtime';
|
|
7
|
-
import { meshAssetDecoder, meshAssetKind } from '@forgeax/engine-geometry';
|
|
8
|
-
import { type DdcPack, normaliseForPack } from '@forgeax/engine-import';
|
|
9
|
-
import type {
|
|
10
|
-
AssetDecoder,
|
|
11
|
-
AssetKind,
|
|
12
|
-
AssetLoadError,
|
|
13
|
-
MeshAsset,
|
|
14
|
-
MorphTarget,
|
|
15
|
-
Result,
|
|
16
|
-
VertexAttributeMap,
|
|
17
|
-
} from '@forgeax/engine-types';
|
|
18
|
-
import { err, ok } from '@forgeax/engine-types';
|
|
19
|
-
|
|
20
|
-
const SCOPE_ID = 'gltf-test-scope';
|
|
21
|
-
const GENERATION = 1;
|
|
22
|
-
const PACK_DIGEST = `sha256:${'1'.repeat(64)}`;
|
|
23
|
-
const OUTPUT_SET_DIGEST = `sha256:${'2'.repeat(64)}`;
|
|
24
|
-
const OUTPUT_DIGEST = `sha256:${'3'.repeat(64)}`;
|
|
25
|
-
const ATTRIBUTE_KEYS = [
|
|
26
|
-
'position',
|
|
27
|
-
'normal',
|
|
28
|
-
'uv',
|
|
29
|
-
'tangent',
|
|
30
|
-
'skinIndex',
|
|
31
|
-
'skinWeight',
|
|
32
|
-
'uv1',
|
|
33
|
-
'uv2',
|
|
34
|
-
'uv3',
|
|
35
|
-
'uv4',
|
|
36
|
-
'uv5',
|
|
37
|
-
'uv6',
|
|
38
|
-
'uv7',
|
|
39
|
-
] as const;
|
|
40
|
-
|
|
41
|
-
type FixtureAsset = DdcPack['assets'][number];
|
|
42
|
-
|
|
43
|
-
export interface AssetRuntimeFixtureOptions {
|
|
44
|
-
readonly packageUrl?: string;
|
|
45
|
-
readonly artifactOverrides?: ReadonlyMap<string, Uint8Array>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function sha256(bytes: Uint8Array): string {
|
|
49
|
-
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function row(asset: FixtureAsset, packageUrl: string) {
|
|
53
|
-
const sourcePath = `fixtures/${asset.guid}`;
|
|
54
|
-
return {
|
|
55
|
-
guid: asset.guid,
|
|
56
|
-
packageUrl,
|
|
57
|
-
kind: asset.kind,
|
|
58
|
-
sourcePath,
|
|
59
|
-
revision: { rootId: 'gltf-fixture', digest: 'sha256:catalog', observedAt: 1 },
|
|
60
|
-
publication: {
|
|
61
|
-
schemaVersion: 'asset-publication/1' as const,
|
|
62
|
-
sourcePath,
|
|
63
|
-
sourceRevision: 'gltf-fixture-source',
|
|
64
|
-
generation: GENERATION,
|
|
65
|
-
digest: PACK_DIGEST,
|
|
66
|
-
outputSetDigest: OUTPUT_SET_DIGEST,
|
|
67
|
-
outputs: [
|
|
68
|
-
{
|
|
69
|
-
guid: asset.guid,
|
|
70
|
-
sourceKey: asset.sourceKey ?? asset.guid,
|
|
71
|
-
kind: asset.kind,
|
|
72
|
-
digest: OUTPUT_DIGEST,
|
|
73
|
-
refs: asset.refs,
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
receipt: {
|
|
77
|
-
schemaVersion: 'asset-publication-receipt/1' as const,
|
|
78
|
-
sourcePath,
|
|
79
|
-
sourceRevision: 'gltf-fixture-source',
|
|
80
|
-
inputFingerprint: 'gltf-fixture-input',
|
|
81
|
-
outputDigest: OUTPUT_DIGEST,
|
|
82
|
-
outputSetDigest: OUTPUT_SET_DIGEST,
|
|
83
|
-
externalEvidence: [],
|
|
84
|
-
},
|
|
85
|
-
externalEvidence: [],
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function floatArray(value: unknown): Float32Array | undefined {
|
|
91
|
-
if (value instanceof Float32Array) return value;
|
|
92
|
-
if (value instanceof ArrayBuffer) return new Float32Array(value);
|
|
93
|
-
if (ArrayBuffer.isView(value))
|
|
94
|
-
return new Float32Array(Array.from(value as unknown as ArrayLike<number>));
|
|
95
|
-
if (Array.isArray(value)) return new Float32Array(value as number[]);
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function indexArray(value: unknown): Uint16Array | Uint32Array | undefined {
|
|
100
|
-
if (value instanceof Uint16Array || value instanceof Uint32Array) return value;
|
|
101
|
-
if (ArrayBuffer.isView(value) || Array.isArray(value)) {
|
|
102
|
-
const values = Array.from(value as unknown as ArrayLike<number>);
|
|
103
|
-
return values.some((entry) => entry > 0xffff)
|
|
104
|
-
? new Uint32Array(values)
|
|
105
|
-
: new Uint16Array(values);
|
|
106
|
-
}
|
|
107
|
-
return undefined;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function integerAttribute(value: unknown): Uint16Array | undefined {
|
|
111
|
-
if (value instanceof Uint16Array) return value;
|
|
112
|
-
if (Array.isArray(value) || ArrayBuffer.isView(value)) {
|
|
113
|
-
return new Uint16Array(Array.from(value as unknown as ArrayLike<number>));
|
|
114
|
-
}
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function morphTarget(value: unknown): MorphTarget | undefined {
|
|
119
|
-
if (value === null || typeof value !== 'object') return undefined;
|
|
120
|
-
const source = value as Record<string, unknown>;
|
|
121
|
-
const position = floatArray(source.position);
|
|
122
|
-
const normal = floatArray(source.normal);
|
|
123
|
-
const tangent = floatArray(source.tangent);
|
|
124
|
-
return {
|
|
125
|
-
...(position === undefined ? {} : { position }),
|
|
126
|
-
...(normal === undefined ? {} : { normal }),
|
|
127
|
-
...(tangent === undefined ? {} : { tangent }),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function meshPayload(value: unknown): MeshAsset | undefined {
|
|
132
|
-
if (value === null || typeof value !== 'object') return undefined;
|
|
133
|
-
const source = value as Record<string, unknown>;
|
|
134
|
-
const vertices = floatArray(source.vertices);
|
|
135
|
-
const rawAttributes = source.attributes;
|
|
136
|
-
const rawSubmeshes = source.submeshes;
|
|
137
|
-
const rawMaterialSlots = source.materialSlots;
|
|
138
|
-
if (
|
|
139
|
-
source.kind !== 'mesh' ||
|
|
140
|
-
vertices === undefined ||
|
|
141
|
-
rawAttributes === null ||
|
|
142
|
-
typeof rawAttributes !== 'object' ||
|
|
143
|
-
!Array.isArray(rawSubmeshes) ||
|
|
144
|
-
!Array.isArray(rawMaterialSlots)
|
|
145
|
-
) {
|
|
146
|
-
return undefined;
|
|
147
|
-
}
|
|
148
|
-
const attributes: VertexAttributeMap = {};
|
|
149
|
-
const sourceAttributes = rawAttributes as Record<string, unknown>;
|
|
150
|
-
for (const key of ATTRIBUTE_KEYS) {
|
|
151
|
-
const raw = sourceAttributes[key];
|
|
152
|
-
if (raw === undefined) continue;
|
|
153
|
-
const converted = key === 'skinIndex' ? integerAttribute(raw) : floatArray(raw);
|
|
154
|
-
if (converted !== undefined) attributes[key] = converted;
|
|
155
|
-
}
|
|
156
|
-
const indices = indexArray(source.indices);
|
|
157
|
-
const aabb = floatArray(source.aabb);
|
|
158
|
-
const morphWeights = floatArray(source.morphWeights);
|
|
159
|
-
const morphTargets = Array.isArray(source.morphTargets)
|
|
160
|
-
? source.morphTargets.flatMap((target) => {
|
|
161
|
-
const converted = morphTarget(target);
|
|
162
|
-
return converted === undefined ? [] : [converted];
|
|
163
|
-
})
|
|
164
|
-
: undefined;
|
|
165
|
-
return {
|
|
166
|
-
kind: 'mesh',
|
|
167
|
-
vertices,
|
|
168
|
-
...(indices === undefined ? {} : { indices }),
|
|
169
|
-
attributes,
|
|
170
|
-
...(aabb === undefined ? {} : { aabb }),
|
|
171
|
-
submeshes: rawSubmeshes as MeshAsset['submeshes'],
|
|
172
|
-
materialSlots: rawMaterialSlots as MeshAsset['materialSlots'],
|
|
173
|
-
...(morphTargets === undefined ? {} : { morphTargets }),
|
|
174
|
-
...(morphWeights === undefined ? {} : { morphWeights }),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export const normalizedMeshAssetDecoder: AssetDecoder<MeshAsset> = {
|
|
179
|
-
async decode(input): Promise<Result<MeshAsset, AssetLoadError>> {
|
|
180
|
-
const body = input.envelope.artifacts.body;
|
|
181
|
-
if (body !== undefined) {
|
|
182
|
-
const bytes = await input.artifacts.read(body);
|
|
183
|
-
if (!bytes.ok) return bytes;
|
|
184
|
-
}
|
|
185
|
-
const payload = meshPayload(input.envelope.payload);
|
|
186
|
-
if (payload === undefined) {
|
|
187
|
-
return err({
|
|
188
|
-
code: 'asset-package-invalid',
|
|
189
|
-
expected: 'a JSON-safe mesh payload that can be owned by Geometry',
|
|
190
|
-
hint: 'recook the mesh payload with typed vertex and attribute fields',
|
|
191
|
-
detail: { guid: input.envelope.guid, reason: 'mesh payload normalization failed' },
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
return meshAssetDecoder.decode({
|
|
195
|
-
...input,
|
|
196
|
-
envelope: { ...input.envelope, payload },
|
|
197
|
-
});
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
export function installMeshDecoder(assets: AssetRegistry) {
|
|
202
|
-
return assets.installDecoder(meshAssetKind, normalizedMeshAssetDecoder);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function installPassthroughDecoder(assets: AssetRegistry, kind: string) {
|
|
206
|
-
const token: AssetKind<Record<string, unknown>, string> = {
|
|
207
|
-
kind,
|
|
208
|
-
} as AssetKind<Record<string, unknown>, string>;
|
|
209
|
-
return assets.installDecoder(token, {
|
|
210
|
-
async decode({ envelope }) {
|
|
211
|
-
return ok(envelope.payload);
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function createAssetRuntimeFixture(
|
|
217
|
-
sourceAssets: readonly FixtureAsset[],
|
|
218
|
-
options: AssetRuntimeFixtureOptions = {},
|
|
219
|
-
): AssetRegistry {
|
|
220
|
-
const packageUrl = options.packageUrl ?? 'https://assets.test/gltf-fixture.pack.json';
|
|
221
|
-
const artifactBytes = new Map<string, Uint8Array>();
|
|
222
|
-
const runtimeAssets = sourceAssets.map((asset) => {
|
|
223
|
-
const artifacts = Object.fromEntries(
|
|
224
|
-
Object.entries(asset.artifacts).map(([key, body]) => {
|
|
225
|
-
const path = `${asset.guid}/${key}.bin`;
|
|
226
|
-
const url = new URL(path, packageUrl).toString();
|
|
227
|
-
artifactBytes.set(
|
|
228
|
-
url,
|
|
229
|
-
options.artifactOverrides?.get(`${asset.guid}:${key}`) ?? body.bytes,
|
|
230
|
-
);
|
|
231
|
-
return [
|
|
232
|
-
key,
|
|
233
|
-
{
|
|
234
|
-
path,
|
|
235
|
-
mediaType: body.mediaType,
|
|
236
|
-
contentEncoding: 'identity' as const,
|
|
237
|
-
byteLength: body.bytes.byteLength,
|
|
238
|
-
integrity: { algorithm: 'sha256' as const, digest: sha256(body.bytes) },
|
|
239
|
-
...(body.assetCodec === undefined ? {} : { assetCodec: body.assetCodec }),
|
|
240
|
-
},
|
|
241
|
-
];
|
|
242
|
-
}),
|
|
243
|
-
);
|
|
244
|
-
return {
|
|
245
|
-
guid: asset.guid,
|
|
246
|
-
kind: asset.kind,
|
|
247
|
-
...(asset.name === undefined ? {} : { name: asset.name }),
|
|
248
|
-
payload: normaliseForPack(asset.payload) as Record<string, unknown>,
|
|
249
|
-
refs: asset.refs,
|
|
250
|
-
artifacts,
|
|
251
|
-
};
|
|
252
|
-
});
|
|
253
|
-
const pack = {
|
|
254
|
-
schemaVersion: '2.0.0' as const,
|
|
255
|
-
kind: 'internal-text-package' as const,
|
|
256
|
-
scopeId: SCOPE_ID,
|
|
257
|
-
generation: GENERATION,
|
|
258
|
-
digest: PACK_DIGEST,
|
|
259
|
-
outputSetDigest: OUTPUT_SET_DIGEST,
|
|
260
|
-
assets: runtimeAssets,
|
|
261
|
-
};
|
|
262
|
-
const fetcher: typeof globalThis.fetch = async (input) => {
|
|
263
|
-
const url = String(input);
|
|
264
|
-
if (url === packageUrl) return new Response(JSON.stringify(pack));
|
|
265
|
-
const bytes = artifactBytes.get(url);
|
|
266
|
-
return bytes === undefined
|
|
267
|
-
? new Response('not found', { status: 404 })
|
|
268
|
-
: new Response(bytes as unknown as BodyInit);
|
|
269
|
-
};
|
|
270
|
-
return createAssetRegistry({
|
|
271
|
-
catalog: createCatalogSource({
|
|
272
|
-
entries: sourceAssets.map((asset) => row(asset, packageUrl)),
|
|
273
|
-
}),
|
|
274
|
-
fetcher,
|
|
275
|
-
scopeId: SCOPE_ID,
|
|
276
|
-
generation: GENERATION,
|
|
277
|
-
});
|
|
278
|
-
}
|