@forgeax/engine-gltf 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.
Files changed (41) hide show
  1. package/README.md +18 -11
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/extension-admission.unit.test.d.ts +2 -0
  4. package/dist/__tests__/extension-admission.unit.test.d.ts.map +1 -0
  5. package/dist/__tests__/source-key-diagnostics.integration.test.d.ts +2 -0
  6. package/dist/__tests__/source-key-diagnostics.integration.test.d.ts.map +1 -0
  7. package/dist/bridge.d.ts +5 -6
  8. package/dist/bridge.d.ts.map +1 -1
  9. package/dist/check-extensions.d.ts.map +1 -1
  10. package/dist/cli-gltf.mjs +32 -16
  11. package/dist/cli-gltf.mjs.map +1 -1
  12. package/dist/gltf-importer.d.ts.map +1 -1
  13. package/dist/importer-entry.mjs +5 -1
  14. package/dist/importer-entry.mjs.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.mjs +31 -10
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/node-file-entry.mjs +5 -1
  20. package/dist/node-file-entry.mjs.map +1 -1
  21. package/dist/parse-gltf.d.ts.map +1 -1
  22. package/dist/source-key.d.ts +26 -4
  23. package/dist/source-key.d.ts.map +1 -1
  24. package/package.json +13 -15
  25. package/src/__tests__/extension-admission.unit.test.ts +19 -0
  26. package/src/__tests__/gltf.unit.test.ts +1 -1
  27. package/src/__tests__/material-texture-transform.unit.test.ts +30 -0
  28. package/src/__tests__/meshopt-real-matrix.integration.test.ts +108 -24
  29. package/src/__tests__/morph-import.integration.test.ts +31 -44
  30. package/src/__tests__/source-key-diagnostics.integration.test.ts +140 -0
  31. package/src/__tests__/source-key-producer.integration.test.ts +2 -2
  32. package/src/bridge.ts +9 -10
  33. package/src/check-extensions.ts +8 -4
  34. package/src/cli-gltf.ts +1 -6
  35. package/src/gltf-importer.ts +1 -2
  36. package/src/index.ts +1 -0
  37. package/src/parse-gltf.ts +7 -5
  38. package/src/source-key.ts +51 -17
  39. package/dist/__tests__/asset-runtime-fixture.d.ts +0 -14
  40. package/dist/__tests__/asset-runtime-fixture.d.ts.map +0 -1
  41. 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; the World scene owner resolves the GUID to a shared ref
428
- * at `world.instantiateScene` time (same protocol as MeshFilter and
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
- * typed `assets.load(sceneGuid, sceneAssetKind)` + `world.instantiateScene`
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 walks the matching SkinAsset.jointPaths against the
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
- // the World scene owner resolves the string to a shared ref at
652
- // instantiate time (same protocol as MeshFilter/MeshRenderer).
653
- // postSpawnResolveJoints then fills Skin.joints[] by walking the matching
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);
@@ -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 and
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, KHR_texture_transform, ...) extends
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[] = [...EXTENSION_ALLOWLIST, 'KHR_lights_punctual'];
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;
@@ -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: assets.load(sceneGuid,
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
@@ -131,6 +131,7 @@ export {
131
131
  // Byte-stable meta JSON serialization (D-3; AC-04).
132
132
  export { serializeMetaJson } from './serialize-meta.js';
133
133
  export type {
134
+ GltfSourceKeyConflictEntry,
134
135
  GltfSourceKeyError,
135
136
  GltfSourceKeyErrorCode,
136
137
  GltfSourceKeyResult,
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 the typed runtime asset owners
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, the runtime catalog
1586
- // never imports the bytes, and the asset consumer renders a white box (G-2).
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
- export interface GltfSourceKeyError {
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, number>();
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: key === items[index]?.kind ? 'ambiguous-source-key' : 'duplicate-source-key',
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
- key === items[index]?.kind
63
- ? 'name each otherwise anonymous output; do not use sourceIndex'
64
- : 'rename duplicate outputs before publishing topology facts',
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, items[index]?.sourceIndex ?? 0);
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
- }