@forgeax/engine-gltf 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +204 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/animation-target-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-target-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-material-samplers.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-material-samplers.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-material-values.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-material-values.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-mixed-color.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-mixed-color.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-sampler-required.test-d.d.ts +2 -0
- package/dist/__tests__/bridge-sampler-required.test-d.d.ts.map +1 -0
- package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-skin-stride.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-skin-stride.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-test-helpers.d.ts +5 -0
- package/dist/__tests__/bridge-test-helpers.d.ts.map +1 -0
- package/dist/__tests__/bridge.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge.unit.test.d.ts.map +1 -0
- package/dist/__tests__/cli-gltf.integration.test.d.ts +2 -0
- package/dist/__tests__/cli-gltf.integration.test.d.ts.map +1 -0
- package/dist/__tests__/color-0-accessor.unit.test.d.ts +2 -0
- package/dist/__tests__/color-0-accessor.unit.test.d.ts.map +1 -0
- package/dist/__tests__/data-uri-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/data-uri-surface.unit.test.d.ts.map +1 -0
- 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__/gltf-error-derived-views.test-d.d.ts +2 -0
- package/dist/__tests__/gltf-error-derived-views.test-d.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-migration.test.d.ts +2 -0
- package/dist/__tests__/gltf-error-migration.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts +2 -0
- package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-importer.test.d.ts +2 -0
- package/dist/__tests__/gltf-importer.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-local-artifacts.test.d.ts +2 -0
- package/dist/__tests__/gltf-local-artifacts.test.d.ts.map +1 -0
- package/dist/__tests__/gltf.unit.test.d.ts +2 -0
- package/dist/__tests__/gltf.unit.test.d.ts.map +1 -0
- package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts +2 -0
- package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-missing-uv.unit.test.d.ts +2 -0
- package/dist/__tests__/material-missing-uv.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-pack-refs.integration.test.d.ts +2 -0
- package/dist/__tests__/material-pack-refs.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-per-slot-ir.unit.test.d.ts +2 -0
- package/dist/__tests__/material-per-slot-ir.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-texture-transform.unit.test.d.ts +2 -0
- package/dist/__tests__/material-texture-transform.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-unused-uv.unit.test.d.ts +2 -0
- package/dist/__tests__/material-unused-uv.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-decode.unit.test.d.ts +2 -0
- package/dist/__tests__/meshopt-decode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-falsifier.unit.test.d.ts +2 -0
- package/dist/__tests__/meshopt-falsifier.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts +2 -0
- package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts +2 -0
- package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts.map +1 -0
- package/dist/__tests__/morph-import.integration.test.d.ts +2 -0
- package/dist/__tests__/morph-import.integration.test.d.ts.map +1 -0
- package/dist/__tests__/morph-import.unit.test.d.ts +2 -0
- package/dist/__tests__/morph-import.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-decode.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-decode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf-multi-uv.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf-multi-uv.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts +2 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts.map +1 -0
- package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/public-surface.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/__tests__/source-key-producer.integration.test.d.ts +2 -0
- package/dist/__tests__/source-key-producer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/source-key.unit.test.d.ts +2 -0
- package/dist/__tests__/source-key.unit.test.d.ts.map +1 -0
- package/dist/accessor/decode-accessor.d.ts +75 -0
- package/dist/accessor/decode-accessor.d.ts.map +1 -0
- package/dist/accessor/decode-color.d.ts +15 -0
- package/dist/accessor/decode-color.d.ts.map +1 -0
- package/dist/accessor/index.d.ts +3 -0
- package/dist/accessor/index.d.ts.map +1 -0
- package/dist/bridge.d.ts +103 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/check-extensions.d.ts +29 -0
- package/dist/check-extensions.d.ts.map +1 -0
- package/dist/cli-gltf.d.ts +8 -0
- package/dist/cli-gltf.d.ts.map +1 -0
- package/dist/cli-gltf.mjs +2435 -0
- package/dist/cli-gltf.mjs.map +1 -0
- package/dist/data-uri.d.ts +6 -0
- package/dist/data-uri.d.ts.map +1 -0
- package/dist/errors.d.ts +201 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/extract-image-bytes.d.ts +42 -0
- package/dist/extract-image-bytes.d.ts.map +1 -0
- package/dist/gltf-importer.d.ts +21 -0
- package/dist/gltf-importer.d.ts.map +1 -0
- package/dist/image-color-space.d.ts +35 -0
- package/dist/image-color-space.d.ts.map +1 -0
- package/dist/importer-entry.d.ts +10 -0
- package/dist/importer-entry.d.ts.map +1 -0
- package/dist/importer-entry.mjs +3378 -0
- package/dist/importer-entry.mjs.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3673 -0
- package/dist/index.mjs.map +1 -0
- package/dist/meshopt-decode.d.ts +39 -0
- package/dist/meshopt-decode.d.ts.map +1 -0
- package/dist/node-file-entry.d.ts +14 -0
- package/dist/node-file-entry.d.ts.map +1 -0
- package/dist/node-file-entry.mjs +1946 -0
- package/dist/node-file-entry.mjs.map +1 -0
- package/dist/node-path.d.ts +14 -0
- package/dist/node-path.d.ts.map +1 -0
- package/dist/parse-animation.d.ts +70 -0
- package/dist/parse-animation.d.ts.map +1 -0
- package/dist/parse-glb-chunks.d.ts +25 -0
- package/dist/parse-glb-chunks.d.ts.map +1 -0
- package/dist/parse-glb.d.ts +3 -0
- package/dist/parse-glb.d.ts.map +1 -0
- package/dist/parse-gltf-header.d.ts +18 -0
- package/dist/parse-gltf-header.d.ts.map +1 -0
- package/dist/parse-gltf.d.ts +263 -0
- package/dist/parse-gltf.d.ts.map +1 -0
- package/dist/parse-skin.d.ts +70 -0
- package/dist/parse-skin.d.ts.map +1 -0
- package/dist/reimport-reuse-meta.d.ts +48 -0
- package/dist/reimport-reuse-meta.d.ts.map +1 -0
- package/dist/serialize-meta.d.ts +2 -0
- package/dist/serialize-meta.d.ts.map +1 -0
- package/dist/source-key.d.ts +44 -0
- package/dist/source-key.d.ts.map +1 -0
- package/dist/sub-asset-key.d.ts +12 -0
- package/dist/sub-asset-key.d.ts.map +1 -0
- package/dist/to-asset-pack.d.ts +2 -0
- package/dist/to-asset-pack.d.ts.map +1 -0
- package/dist/transform.d.ts +37 -0
- package/dist/transform.d.ts.map +1 -0
- package/package.json +92 -0
- package/src/__tests__/animation-target-errors.unit.test.ts +51 -0
- package/src/__tests__/bridge-material-samplers.unit.test.ts +67 -0
- package/src/__tests__/bridge-material-values.unit.test.ts +106 -0
- package/src/__tests__/bridge-mixed-color.unit.test.ts +100 -0
- package/src/__tests__/bridge-sampler-required.test-d.ts +59 -0
- package/src/__tests__/bridge-skin-shader-route.unit.test.ts +42 -0
- package/src/__tests__/bridge-skin-stride.unit.test.ts +115 -0
- package/src/__tests__/bridge-test-helpers.ts +9 -0
- package/src/__tests__/bridge.unit.test.ts +1227 -0
- package/src/__tests__/cli-gltf.integration.test.ts +122 -0
- package/src/__tests__/color-0-accessor.unit.test.ts +174 -0
- package/src/__tests__/data-uri-surface.unit.test.ts +26 -0
- package/src/__tests__/extension-admission.unit.test.ts +19 -0
- package/src/__tests__/fixtures/color-0/float-vec3.gltf +13 -0
- package/src/__tests__/fixtures/color-0/interleaved-vec3.gltf +6 -0
- package/src/__tests__/fixtures/color-0/normalized-ubyte.gltf +6 -0
- package/src/__tests__/fixtures/color-0/normalized-ushort.gltf +6 -0
- package/src/__tests__/gltf-error-derived-views.test-d.ts +152 -0
- package/src/__tests__/gltf-error-migration.test.ts +147 -0
- package/src/__tests__/gltf-error-policy-owner.unit.test.ts +185 -0
- package/src/__tests__/gltf-importer.test.ts +12 -0
- package/src/__tests__/gltf-local-artifacts.test.ts +11 -0
- package/src/__tests__/gltf.unit.test.ts +2975 -0
- package/src/__tests__/malformed-buffer-data-uri.integration.test.ts +140 -0
- package/src/__tests__/material-missing-uv.unit.test.ts +29 -0
- package/src/__tests__/material-pack-refs.integration.test.ts +58 -0
- package/src/__tests__/material-per-slot-ir.unit.test.ts +90 -0
- package/src/__tests__/material-texture-transform.unit.test.ts +105 -0
- package/src/__tests__/material-unused-uv.unit.test.ts +44 -0
- package/src/__tests__/meshopt-decode.unit.test.ts +87 -0
- package/src/__tests__/meshopt-falsifier.unit.test.ts +92 -0
- package/src/__tests__/meshopt-mode-filter-owner.test-d.ts +79 -0
- package/src/__tests__/meshopt-real-matrix.integration.test.ts +370 -0
- package/src/__tests__/morph-import.integration.test.ts +115 -0
- package/src/__tests__/morph-import.unit.test.ts +62 -0
- package/src/__tests__/parse-decode.unit.test.ts +919 -0
- package/src/__tests__/parse-gltf-multi-uv.test.ts +238 -0
- package/src/__tests__/parse-gltf-skin-attrs.unit.test.ts +191 -0
- package/src/__tests__/parse-gltf.unit.test.ts +133 -0
- package/src/__tests__/pick-e2e.integration.test.ts +238 -0
- package/src/__tests__/public-surface.unit.test.ts +10 -0
- package/src/__tests__/source-key-diagnostics.integration.test.ts +140 -0
- package/src/__tests__/source-key-producer.integration.test.ts +171 -0
- package/src/__tests__/source-key.unit.test.ts +159 -0
- package/src/accessor/decode-accessor.ts +309 -0
- package/src/accessor/decode-color.ts +128 -0
- package/src/accessor/index.ts +15 -0
- package/src/bridge.ts +928 -0
- package/src/check-extensions.ts +85 -0
- package/src/cli-gltf.ts +339 -0
- package/src/data-uri.ts +29 -0
- package/src/errors.ts +386 -0
- package/src/extract-image-bytes.ts +343 -0
- package/src/gltf-importer.ts +976 -0
- package/src/image-color-space.ts +104 -0
- package/src/importer-entry.ts +24 -0
- package/src/index.ts +148 -0
- package/src/meshopt-decode.ts +200 -0
- package/src/node-file-entry.ts +98 -0
- package/src/node-path.ts +41 -0
- package/src/parse-animation.ts +286 -0
- package/src/parse-glb-chunks.ts +167 -0
- package/src/parse-glb.ts +16 -0
- package/src/parse-gltf-header.ts +58 -0
- package/src/parse-gltf.ts +1731 -0
- package/src/parse-skin.ts +238 -0
- package/src/reimport-reuse-meta.ts +134 -0
- package/src/serialize-meta.ts +24 -0
- package/src/source-key.ts +106 -0
- package/src/sub-asset-key.ts +61 -0
- package/src/to-asset-pack.ts +8 -0
- package/src/transform.ts +117 -0
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
// gltf-importer.ts - the build-time gltfImporter (feat-20260603-asset-import-loader-injection M2 / w19,
|
|
2
|
+
// extended in feat-20260608 M3 w14 with the texture pipeline: three image
|
|
3
|
+
// source paths funnelled through the ImportContext decodeImage seam).
|
|
4
|
+
//
|
|
5
|
+
// The `{ key: 'gltf', import }` Importer the @forgeax/engine-import runner
|
|
6
|
+
// dispatches a `*.meta.json` with `importer: 'gltf'` to. It reads the source
|
|
7
|
+
// bytes via `ctx.readSource()`, parses them to a `GltfDoc` (parseGltf / parseGlb),
|
|
8
|
+
// and converts each declared sub-asset (mesh / material / scene / texture /
|
|
9
|
+
// skin / animation-clip) into an `ImportedAsset` POD stamped with the
|
|
10
|
+
// meta-declared GUID (GUID import-stable iron law: GUIDs come from
|
|
11
|
+
// `ctx.subAssets[]`, never minted here).
|
|
12
|
+
//
|
|
13
|
+
// Texture pipeline (M3 D-1 / D-3 / D-6, requirements AC-08 / 09 / 10 / 11 /
|
|
14
|
+
// 12 / 13): for every glTF `images[]` row the importer extracts the raw
|
|
15
|
+
// PNG / JPEG bytes from one of three sources (bufferView slice in
|
|
16
|
+
// .glb / data: URI in .gltf / external URI sibling read), funnels them
|
|
17
|
+
// through `ctx.decodeImage` (the only seam to @forgeax/engine-image — a
|
|
18
|
+
// grep gate enforces zero static `from '@forgeax/engine-image'` lines in
|
|
19
|
+
// this package), and emits a `kind: 'texture'` ImportedAsset stamped with
|
|
20
|
+
// the meta-declared GUID. The colorSpace is pre-derived per-image by
|
|
21
|
+
// `deriveTextureColorSpace` (D-3 walk of material slot bindings).
|
|
22
|
+
//
|
|
23
|
+
// Material refs[]: each material's emitted ImportedAsset carries the GUIDs
|
|
24
|
+
// of every texture sub-asset its slots reference, so the runner builds the
|
|
25
|
+
// scene/material -> texture cross-edge needed by AC-11 / AC-19.
|
|
26
|
+
//
|
|
27
|
+
// Sub-asset -> GUID mapping: `ctx.subAssets[]` carries one entry per declared
|
|
28
|
+
// sub-asset with `{ guid, sourceIndex, kind }`. The importer indexes into the
|
|
29
|
+
// parsed doc by (kind, sourceIndex) and emits the corresponding POD under the
|
|
30
|
+
// declared GUID. A sub-asset kind with no doc counterpart (or a sourceIndex out
|
|
31
|
+
// of range) is skipped; the runner's GUID iron-law check then surfaces the
|
|
32
|
+
// gap as `import-produced-no-assets`. A texture sub-asset that fails byte
|
|
33
|
+
// extraction surfaces as `gltf-image-extract-failed` (D-6).
|
|
34
|
+
|
|
35
|
+
import { packMeshBinV4 } from '@forgeax/engine-import/mesh-bin';
|
|
36
|
+
import { AssetGuid as AssetGuidCodec } from '@forgeax/engine-pack/guid';
|
|
37
|
+
import type {
|
|
38
|
+
AssetGuid,
|
|
39
|
+
AssetRef,
|
|
40
|
+
Handle,
|
|
41
|
+
ImportContext,
|
|
42
|
+
ImportedAsset,
|
|
43
|
+
Importer,
|
|
44
|
+
ImportResult,
|
|
45
|
+
MaterialTextureValue,
|
|
46
|
+
MaterialValue,
|
|
47
|
+
MeshAsset,
|
|
48
|
+
MeshMaterialSlotTopologyEntry,
|
|
49
|
+
} from '@forgeax/engine-types';
|
|
50
|
+
import {
|
|
51
|
+
IMPORT_ERROR_HINTS,
|
|
52
|
+
ImportError,
|
|
53
|
+
reconcileMeshMaterialSlotTopology,
|
|
54
|
+
resolveMeshMaterialSlotDefaultGuid,
|
|
55
|
+
toShared,
|
|
56
|
+
} from '@forgeax/engine-types';
|
|
57
|
+
import {
|
|
58
|
+
gltfDocToSceneAsset,
|
|
59
|
+
meshIrToMeshAsset,
|
|
60
|
+
toMaterialAsset,
|
|
61
|
+
validateMaterialUvSets,
|
|
62
|
+
} from './bridge.js';
|
|
63
|
+
import { gltfErr } from './errors.js';
|
|
64
|
+
import { extractImageBytes } from './extract-image-bytes.js';
|
|
65
|
+
import { deriveTextureColorSpace } from './image-color-space.js';
|
|
66
|
+
import type { GltfBufferViewDecodeCapability } from './meshopt-decode.js';
|
|
67
|
+
import type { GltfDoc, GltfMaterialIr, GltfTextureInfoIr } from './parse-gltf.js';
|
|
68
|
+
import { parseGlbForImporter, parseGltfForImporter } from './parse-gltf.js';
|
|
69
|
+
|
|
70
|
+
type ParseDocResult =
|
|
71
|
+
| { readonly ok: true; readonly value: GltfDoc }
|
|
72
|
+
| { readonly ok: false; readonly error: ImportError };
|
|
73
|
+
|
|
74
|
+
function isGlbBytes(source: string): boolean {
|
|
75
|
+
return source.toLowerCase().endsWith('.glb');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function publishesCatalogProduct(input: {
|
|
79
|
+
readonly importSettings: Readonly<Record<string, unknown>>;
|
|
80
|
+
}): boolean {
|
|
81
|
+
return input.importSettings.geometry !== 'procedural';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function previousMaterialSlotTopology(
|
|
85
|
+
ctx: ImportContext,
|
|
86
|
+
meshSourceKey: string | undefined,
|
|
87
|
+
): readonly MeshMaterialSlotTopologyEntry[] | undefined {
|
|
88
|
+
if (meshSourceKey === undefined) return undefined;
|
|
89
|
+
const value = ctx.sourceOverrides?.[meshSourceKey]?.materialSlots;
|
|
90
|
+
if (value === undefined) return undefined;
|
|
91
|
+
if (!Array.isArray(value)) {
|
|
92
|
+
throw new ImportError({
|
|
93
|
+
code: 'invalid-source-override-payload',
|
|
94
|
+
expected: `${meshSourceKey}.materialSlots to be an array`,
|
|
95
|
+
hint: IMPORT_ERROR_HINTS['invalid-source-override-payload'],
|
|
96
|
+
detail: {
|
|
97
|
+
sourceKey: meshSourceKey,
|
|
98
|
+
declaredSourceKeys: ctx.subAssets.flatMap((entry) => entry.sourceKey ?? []),
|
|
99
|
+
reason: 'materialSlots is not an array',
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const slots: MeshMaterialSlotTopologyEntry[] = [];
|
|
104
|
+
for (const [index, raw] of value.entries()) {
|
|
105
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
106
|
+
throw new ImportError({
|
|
107
|
+
code: 'invalid-source-override-payload',
|
|
108
|
+
expected: `${meshSourceKey}.materialSlots[${index}] to be an object`,
|
|
109
|
+
hint: IMPORT_ERROR_HINTS['invalid-source-override-payload'],
|
|
110
|
+
detail: {
|
|
111
|
+
sourceKey: meshSourceKey,
|
|
112
|
+
declaredSourceKeys: ctx.subAssets.flatMap((entry) => entry.sourceKey ?? []),
|
|
113
|
+
reason: `materialSlots[${index}] is not an object`,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const slot = raw as Record<string, unknown>;
|
|
118
|
+
if (typeof slot.slotName !== 'string' || slot.slotName.trim().length === 0) {
|
|
119
|
+
throw new ImportError({
|
|
120
|
+
code: 'invalid-source-override-payload',
|
|
121
|
+
expected: `${meshSourceKey}.materialSlots[${index}].slotName to be non-empty`,
|
|
122
|
+
hint: IMPORT_ERROR_HINTS['invalid-source-override-payload'],
|
|
123
|
+
detail: {
|
|
124
|
+
sourceKey: meshSourceKey,
|
|
125
|
+
declaredSourceKeys: ctx.subAssets.flatMap((entry) => entry.sourceKey ?? []),
|
|
126
|
+
reason: `materialSlots[${index}].slotName is invalid`,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
slots.push({
|
|
131
|
+
slotName: slot.slotName,
|
|
132
|
+
...(typeof slot.sourceKey === 'string' ? { sourceKey: slot.sourceKey } : {}),
|
|
133
|
+
...(typeof slot.defaultMaterialGuid === 'string'
|
|
134
|
+
? { defaultMaterialGuid: slot.defaultMaterialGuid }
|
|
135
|
+
: {}),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return slots;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function stabilizeMeshMaterialSlots(
|
|
142
|
+
mesh: MeshAsset,
|
|
143
|
+
ctx: ImportContext,
|
|
144
|
+
meshGuid: string,
|
|
145
|
+
meshSourceKey: string | undefined,
|
|
146
|
+
): MeshAsset {
|
|
147
|
+
const current = mesh.materialSlots.map(
|
|
148
|
+
(slot): MeshMaterialSlotTopologyEntry => ({
|
|
149
|
+
slotName: slot.slotName,
|
|
150
|
+
...(slot.sourceKey === undefined ? {} : { sourceKey: slot.sourceKey }),
|
|
151
|
+
...(slot.defaultMaterial === undefined
|
|
152
|
+
? {}
|
|
153
|
+
: { defaultMaterialGuid: AssetGuidCodec.format(slot.defaultMaterial) }),
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
const reconciled = reconcileMeshMaterialSlotTopology(
|
|
157
|
+
current,
|
|
158
|
+
previousMaterialSlotTopology(ctx, meshSourceKey),
|
|
159
|
+
);
|
|
160
|
+
if (!reconciled.ok) {
|
|
161
|
+
throw new ImportError({
|
|
162
|
+
code: 'mesh-material-slot-topology-change',
|
|
163
|
+
expected: `unambiguous material slot identity for mesh ${meshGuid}`,
|
|
164
|
+
hint: IMPORT_ERROR_HINTS['mesh-material-slot-topology-change'],
|
|
165
|
+
detail: {
|
|
166
|
+
meshGuid,
|
|
167
|
+
...(meshSourceKey === undefined ? {} : { meshSourceKey }),
|
|
168
|
+
previousIndices: reconciled.error.previousIndices,
|
|
169
|
+
nextIndices: reconciled.error.nextIndices,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const authoredDefaults =
|
|
174
|
+
meshSourceKey === undefined
|
|
175
|
+
? undefined
|
|
176
|
+
: ctx.sourceOverrides?.[meshSourceKey]?.materialSlotDefaultOverrides;
|
|
177
|
+
if (
|
|
178
|
+
authoredDefaults !== undefined &&
|
|
179
|
+
(authoredDefaults === null ||
|
|
180
|
+
typeof authoredDefaults !== 'object' ||
|
|
181
|
+
Array.isArray(authoredDefaults))
|
|
182
|
+
) {
|
|
183
|
+
throw new ImportError({
|
|
184
|
+
code: 'invalid-source-override-payload',
|
|
185
|
+
expected: `${meshSourceKey}.materialSlotDefaultOverrides to be an object`,
|
|
186
|
+
hint: IMPORT_ERROR_HINTS['invalid-source-override-payload'],
|
|
187
|
+
detail: {
|
|
188
|
+
sourceKey: meshSourceKey,
|
|
189
|
+
declaredSourceKeys: [],
|
|
190
|
+
reason: 'materialSlotDefaultOverrides is invalid',
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const authoredBySlot = authoredDefaults as Readonly<Record<string, unknown>> | undefined;
|
|
195
|
+
const activeStableSlots = new Set(reconciled.currentToStableSlot);
|
|
196
|
+
return {
|
|
197
|
+
...mesh,
|
|
198
|
+
submeshes: mesh.submeshes.map((submesh) => ({
|
|
199
|
+
...submesh,
|
|
200
|
+
materialSlot: reconciled.currentToStableSlot[submesh.materialSlot] as number,
|
|
201
|
+
})),
|
|
202
|
+
materialSlots: reconciled.slots.map((slot, stableIndex) => {
|
|
203
|
+
const active = activeStableSlots.has(stableIndex);
|
|
204
|
+
const authored = authoredBySlot?.[slot.sourceKey ?? slot.slotName];
|
|
205
|
+
const effectiveDefault = resolveMeshMaterialSlotDefaultGuid(
|
|
206
|
+
slot,
|
|
207
|
+
active && (typeof authored === 'string' || authored === null) ? authored : undefined,
|
|
208
|
+
);
|
|
209
|
+
const parsed =
|
|
210
|
+
effectiveDefault === undefined ? undefined : AssetGuidCodec.parse(effectiveDefault);
|
|
211
|
+
return {
|
|
212
|
+
slotName: slot.slotName,
|
|
213
|
+
...(slot.sourceKey === undefined ? {} : { sourceKey: slot.sourceKey }),
|
|
214
|
+
...(active && parsed?.ok ? { defaultMaterial: parsed.value } : {}),
|
|
215
|
+
};
|
|
216
|
+
}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function parseDoc(
|
|
221
|
+
source: string,
|
|
222
|
+
bytes: Uint8Array,
|
|
223
|
+
ctx: ImportContext,
|
|
224
|
+
meshopt?: GltfBufferViewDecodeCapability,
|
|
225
|
+
): Promise<ParseDocResult> {
|
|
226
|
+
const ab = bytes.buffer.slice(
|
|
227
|
+
bytes.byteOffset,
|
|
228
|
+
bytes.byteOffset + bytes.byteLength,
|
|
229
|
+
) as ArrayBuffer;
|
|
230
|
+
if (isGlbBytes(source)) {
|
|
231
|
+
const res = await parseGlbForImporter(ab, source, meshopt === undefined ? {} : { meshopt });
|
|
232
|
+
if (!res.ok) {
|
|
233
|
+
if (res.error instanceof ImportError) return { ok: false, error: res.error };
|
|
234
|
+
throw new Error(`parseGlb failed: ${res.error.code} ${res.error.expected}`);
|
|
235
|
+
}
|
|
236
|
+
return { ok: true, value: res.value };
|
|
237
|
+
}
|
|
238
|
+
let json: unknown;
|
|
239
|
+
try {
|
|
240
|
+
json = JSON.parse(new TextDecoder().decode(bytes));
|
|
241
|
+
} catch (e) {
|
|
242
|
+
throw new Error(`gltf JSON parse failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
243
|
+
}
|
|
244
|
+
// External buffers: read sibling files relative to meta.source. data: URIs
|
|
245
|
+
// are decoded inline by parseGltf; only external URIs reach the loader.
|
|
246
|
+
const externalLoader = async (uri: string): Promise<ArrayBuffer> => {
|
|
247
|
+
const sib = await ctx.readSibling(uri);
|
|
248
|
+
if (!sib.ok) {
|
|
249
|
+
throw new Error(`gltfImporter: external buffer "${uri}" read failed: ${sib.error.code}`);
|
|
250
|
+
}
|
|
251
|
+
return sib.value.buffer.slice(
|
|
252
|
+
sib.value.byteOffset,
|
|
253
|
+
sib.value.byteOffset + sib.value.byteLength,
|
|
254
|
+
) as ArrayBuffer;
|
|
255
|
+
};
|
|
256
|
+
const res = await parseGltfForImporter(
|
|
257
|
+
json,
|
|
258
|
+
externalLoader,
|
|
259
|
+
source,
|
|
260
|
+
meshopt === undefined ? {} : { meshopt },
|
|
261
|
+
);
|
|
262
|
+
if (!res.ok) {
|
|
263
|
+
if (res.error instanceof ImportError) return { ok: false, error: res.error };
|
|
264
|
+
throw new Error(`parseGltf failed: ${res.error.code} ${res.error.expected}`);
|
|
265
|
+
}
|
|
266
|
+
return { ok: true, value: res.value };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
interface HandleMaps {
|
|
270
|
+
readonly meshHandles: Map<number, Handle<'MeshAsset', 'shared'>>;
|
|
271
|
+
readonly materialHandles: Map<number, Handle<'MaterialAsset', 'shared'>>;
|
|
272
|
+
readonly textureHandles: Map<number, Handle<'TextureAsset', 'shared'>>;
|
|
273
|
+
readonly samplerHandles: Map<number, Handle<'SamplerAsset', 'shared'>>;
|
|
274
|
+
readonly meshGuidByIndex: Map<number, string>;
|
|
275
|
+
readonly materialGuidByIndex: Map<number, string>;
|
|
276
|
+
readonly textureGuidByIndex: Map<number, string>;
|
|
277
|
+
readonly samplerGuidByIndex: Map<number, string>;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Build the gltf-index -> declared-GUID maps so scene refs resolve to the
|
|
282
|
+
* sub-asset GUIDs (not runtime handles, which do not exist at import time).
|
|
283
|
+
* The scene POD therefore carries deterministic synthetic handle slots; the
|
|
284
|
+
* GUID cross-references are recorded on the ImportedAsset `refs[]`.
|
|
285
|
+
*
|
|
286
|
+
* Texture handles are seeded from `subAssets[]` of kind 'texture'; their
|
|
287
|
+
* sourceIndex is the glTF `images[]` row (toAssetPack emits one texture
|
|
288
|
+
* sub-asset per image row, so the mapping is image-index keyed even though
|
|
289
|
+
* the field is named textureHandles for material binding readability).
|
|
290
|
+
* GltfMaterialIr's `*Texture` fields hold glTF `textures[]` indices; the
|
|
291
|
+
* importer dereferences `textures[i].source` to convert that into an image
|
|
292
|
+
* index when stamping material handles.
|
|
293
|
+
*/
|
|
294
|
+
function buildHandleMaps(
|
|
295
|
+
subAssets: readonly { guid: string; sourceIndex: number; kind: string }[],
|
|
296
|
+
doc: GltfDoc,
|
|
297
|
+
): HandleMaps {
|
|
298
|
+
const meshHandles = new Map<number, Handle<'MeshAsset', 'shared'>>();
|
|
299
|
+
const materialHandles = new Map<number, Handle<'MaterialAsset', 'shared'>>();
|
|
300
|
+
const textureHandles = new Map<number, Handle<'TextureAsset', 'shared'>>();
|
|
301
|
+
const samplerHandles = new Map<number, Handle<'SamplerAsset', 'shared'>>();
|
|
302
|
+
const meshGuidByIndex = new Map<number, string>();
|
|
303
|
+
const materialGuidByIndex = new Map<number, string>();
|
|
304
|
+
const textureGuidByIndex = new Map<number, string>();
|
|
305
|
+
const samplerGuidByIndex = new Map<number, string>();
|
|
306
|
+
// bug-20260610 layer 7c-2: scene's `refs[]` is concatenated in the order
|
|
307
|
+
// [mesh sub-assets..., material sub-assets..., texture sub-assets...] (see
|
|
308
|
+
// gltf-importer scene branch). The synthetic handle values stamped here
|
|
309
|
+
// travel through `MeshFilter.assetHandle` / `MeshRenderer.materials[*]` and
|
|
310
|
+
// are decoded at runtime as **refs[] indices** by parseScenePayload. So the
|
|
311
|
+
// handle value MUST equal the slot offset in the eventual concat, NOT the
|
|
312
|
+
// gltf source-index. The previous keying-by-sourceIndex worked accidentally
|
|
313
|
+
// when there was exactly one mesh + N <= meshCount materials; it broke as
|
|
314
|
+
// soon as materialIndex landed past the mesh-section boundary (Sponza:
|
|
315
|
+
// materials[0]=0 -> refs[0] = mesh GUID, every submesh sampled the mesh
|
|
316
|
+
// asset as its material -> single fallback unlit dispatch entry, no per-
|
|
317
|
+
// primitive draws).
|
|
318
|
+
let meshCursor = 0;
|
|
319
|
+
let materialCursor = 0;
|
|
320
|
+
for (const sub of subAssets) {
|
|
321
|
+
if (sub.kind === 'mesh') {
|
|
322
|
+
meshHandles.set(sub.sourceIndex, toShared<'MeshAsset'>(meshCursor));
|
|
323
|
+
meshGuidByIndex.set(sub.sourceIndex, sub.guid);
|
|
324
|
+
meshCursor += 1;
|
|
325
|
+
} else if (sub.kind === 'material') {
|
|
326
|
+
// material handles come AFTER all mesh refs in the concat.
|
|
327
|
+
// The actual offset (= meshCount + materialCursor) is back-patched
|
|
328
|
+
// below once we know meshCount.
|
|
329
|
+
materialHandles.set(sub.sourceIndex, toShared<'MaterialAsset'>(materialCursor));
|
|
330
|
+
materialGuidByIndex.set(sub.sourceIndex, sub.guid);
|
|
331
|
+
materialCursor += 1;
|
|
332
|
+
} else if (sub.kind === 'texture') {
|
|
333
|
+
textureGuidByIndex.set(sub.sourceIndex, sub.guid);
|
|
334
|
+
} else if (sub.kind === 'sampler') {
|
|
335
|
+
samplerGuidByIndex.set(sub.sourceIndex, sub.guid);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const meshCount = meshCursor;
|
|
339
|
+
if (meshCount > 0) {
|
|
340
|
+
for (const [k, v] of materialHandles) {
|
|
341
|
+
const local = v as unknown as number;
|
|
342
|
+
materialHandles.set(k, toShared<'MaterialAsset'>(local + meshCount));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// For material binding (toMaterialAsset / values.<X>Texture) the handle
|
|
346
|
+
// value is the texture's slot offset within the SAME asset's `refs[]` (which
|
|
347
|
+
// is `materialTextureRefs` order, not the scene-level refs concat).
|
|
348
|
+
// toMaterialAsset only consumes textureHandles to copy a number into the
|
|
349
|
+
// values; the gltf-importer's later 7a fix-up rewrites those values
|
|
350
|
+
// into refs[] indices for the runtime materialLoader. Keying by texIndex
|
|
351
|
+
// and storing `tex.source` here matches the existing 7a path.
|
|
352
|
+
const textures = doc.textures ?? [];
|
|
353
|
+
for (let texIndex = 0; texIndex < textures.length; texIndex++) {
|
|
354
|
+
const tex = textures[texIndex];
|
|
355
|
+
if (tex === undefined) continue;
|
|
356
|
+
if (textureGuidByIndex.has(tex.source)) {
|
|
357
|
+
textureHandles.set(texIndex, toShared<'TextureAsset'>(tex.source));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
for (const [samplerIndex] of samplerGuidByIndex) {
|
|
361
|
+
samplerHandles.set(samplerIndex, toShared<'SamplerAsset'>(samplerIndex));
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
meshHandles,
|
|
365
|
+
materialHandles,
|
|
366
|
+
textureHandles,
|
|
367
|
+
samplerHandles,
|
|
368
|
+
meshGuidByIndex,
|
|
369
|
+
materialGuidByIndex,
|
|
370
|
+
textureGuidByIndex,
|
|
371
|
+
samplerGuidByIndex,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function textureInfo(info: GltfTextureInfoIr | number | undefined): GltfTextureInfoIr | undefined {
|
|
376
|
+
return info === undefined ? undefined : typeof info === 'number' ? { texture: info } : info;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Collect texture and sampler GUID refs for one material (AC-11 cross-edge). */
|
|
380
|
+
export function materialRefsForPack(
|
|
381
|
+
mat: GltfMaterialIr,
|
|
382
|
+
doc: GltfDoc,
|
|
383
|
+
textureGuidByIndex: ReadonlyMap<number, string>,
|
|
384
|
+
samplerGuidByIndex: ReadonlyMap<number, string> = new Map(),
|
|
385
|
+
): readonly AssetRef[] {
|
|
386
|
+
const refs: AssetRef[] = [];
|
|
387
|
+
const textures = doc.textures ?? [];
|
|
388
|
+
function pushRefsForSlot(info: GltfTextureInfoIr | number | undefined, fieldName: string): void {
|
|
389
|
+
const binding = textureInfo(info);
|
|
390
|
+
if (binding === undefined) return;
|
|
391
|
+
const tex = textures[binding.texture];
|
|
392
|
+
if (tex === undefined) return;
|
|
393
|
+
const guid = textureGuidByIndex.get(tex.source);
|
|
394
|
+
if (guid !== undefined) {
|
|
395
|
+
refs.push({
|
|
396
|
+
guid,
|
|
397
|
+
sourceField: { componentName: '<material>', fieldName },
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (binding.sampler !== undefined) {
|
|
401
|
+
const samplerGuid = samplerGuidByIndex.get(binding.sampler);
|
|
402
|
+
if (samplerGuid !== undefined) {
|
|
403
|
+
refs.push({
|
|
404
|
+
guid: samplerGuid,
|
|
405
|
+
sourceField: { componentName: '<material>', fieldName: `${fieldName}.sampler` },
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
pushRefsForSlot(mat.baseColorTexture, 'baseColorTexture');
|
|
411
|
+
pushRefsForSlot(mat.metallicRoughnessTexture, 'metallicRoughnessTexture');
|
|
412
|
+
pushRefsForSlot(mat.normalTexture, 'normalTexture');
|
|
413
|
+
pushRefsForSlot(mat.occlusionTexture, 'occlusionTexture');
|
|
414
|
+
pushRefsForSlot(mat.emissiveTexture, 'emissiveTexture');
|
|
415
|
+
return refs;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function availableUvSets(mesh: GltfDoc['meshes'][number]): readonly number[] {
|
|
419
|
+
const sets: number[] = [];
|
|
420
|
+
for (let set = 0; set <= 7; set++) {
|
|
421
|
+
const field = `texcoord${set}` as keyof typeof mesh;
|
|
422
|
+
if (mesh[field] !== undefined) sets.push(set);
|
|
423
|
+
}
|
|
424
|
+
return sets;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function rewriteMaterialAssetRefs(
|
|
428
|
+
matAsset: ReturnType<typeof toMaterialAsset>,
|
|
429
|
+
mat: GltfMaterialIr,
|
|
430
|
+
doc: GltfDoc,
|
|
431
|
+
maps: HandleMaps,
|
|
432
|
+
): ReturnType<typeof toMaterialAsset> {
|
|
433
|
+
const values = { ...(matAsset.values ?? {}) } as Record<string, MaterialValue | null>;
|
|
434
|
+
const textures = doc.textures ?? [];
|
|
435
|
+
const slots: readonly [
|
|
436
|
+
(
|
|
437
|
+
| 'baseColorTexture'
|
|
438
|
+
| 'metallicRoughnessTexture'
|
|
439
|
+
| 'normalTexture'
|
|
440
|
+
| 'occlusionTexture'
|
|
441
|
+
| 'emissiveTexture'
|
|
442
|
+
),
|
|
443
|
+
GltfTextureInfoIr | number | undefined,
|
|
444
|
+
][] = [
|
|
445
|
+
['baseColorTexture', mat.baseColorTexture],
|
|
446
|
+
['metallicRoughnessTexture', mat.metallicRoughnessTexture],
|
|
447
|
+
['normalTexture', mat.normalTexture],
|
|
448
|
+
['occlusionTexture', mat.occlusionTexture],
|
|
449
|
+
['emissiveTexture', mat.emissiveTexture],
|
|
450
|
+
];
|
|
451
|
+
let cursor = 0;
|
|
452
|
+
for (const [slot, rawBinding] of slots) {
|
|
453
|
+
const binding = textureInfo(rawBinding);
|
|
454
|
+
if (binding === undefined) continue;
|
|
455
|
+
const texture = textures[binding.texture];
|
|
456
|
+
const textureGuid =
|
|
457
|
+
texture === undefined ? undefined : maps.textureGuidByIndex.get(texture.source);
|
|
458
|
+
const value = values[slot];
|
|
459
|
+
if (textureGuid === undefined || typeof value !== 'object' || value === null) {
|
|
460
|
+
delete values[slot];
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const textureValue = value as MaterialTextureValue;
|
|
464
|
+
const textureRef = cursor as unknown as MaterialTextureValue['texture'];
|
|
465
|
+
cursor++;
|
|
466
|
+
const rewritten =
|
|
467
|
+
binding.sampler !== undefined && maps.samplerGuidByIndex.has(binding.sampler)
|
|
468
|
+
? {
|
|
469
|
+
...textureValue,
|
|
470
|
+
texture: textureRef,
|
|
471
|
+
sampler: cursor as unknown as NonNullable<MaterialTextureValue['sampler']>,
|
|
472
|
+
}
|
|
473
|
+
: { ...textureValue, texture: textureRef };
|
|
474
|
+
if (binding.sampler !== undefined && maps.samplerGuidByIndex.has(binding.sampler)) cursor++;
|
|
475
|
+
values[slot] = rewritten;
|
|
476
|
+
}
|
|
477
|
+
return { ...matAsset, values };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function importGltf(
|
|
481
|
+
ctx: ImportContext,
|
|
482
|
+
meshopt?: GltfBufferViewDecodeCapability,
|
|
483
|
+
): Promise<ImportResult> {
|
|
484
|
+
const read = await ctx.readSource();
|
|
485
|
+
if (!read.ok) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
`gltfImporter: readSource failed: ${read.error instanceof Error ? read.error.message : String(read.error)}`,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
const parsed = await parseDoc(ctx.source, read.value, ctx, meshopt);
|
|
491
|
+
if (!parsed.ok) return parsed;
|
|
492
|
+
const doc = parsed.value;
|
|
493
|
+
const maps = buildHandleMaps(ctx.subAssets, doc);
|
|
494
|
+
|
|
495
|
+
// Pre-derive each images[] row's colorSpace from material slot bindings
|
|
496
|
+
// (D-3 / AC-08). orphan images default to 'linear' (AC-13).
|
|
497
|
+
const imageColorSpaces = deriveTextureColorSpace({
|
|
498
|
+
imageCount: (doc.images ?? []).length,
|
|
499
|
+
textures: doc.textures,
|
|
500
|
+
materials: doc.materials,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Extract image bytes (3 source paths) once. Failures here are surfaced
|
|
504
|
+
// per-image so a single bad row does not abort the whole importer.
|
|
505
|
+
const declaredImageIndices = new Set<number>();
|
|
506
|
+
for (const sub of ctx.subAssets) {
|
|
507
|
+
if (sub.kind === 'texture') declaredImageIndices.add(sub.sourceIndex);
|
|
508
|
+
}
|
|
509
|
+
const extraction =
|
|
510
|
+
declaredImageIndices.size > 0
|
|
511
|
+
? await extractImageBytes(read.value, ctx.source, ctx)
|
|
512
|
+
: {
|
|
513
|
+
extracted: new Map(),
|
|
514
|
+
failures: [] as readonly {
|
|
515
|
+
imageIndex: number;
|
|
516
|
+
source: 'bufferView' | 'data-uri' | 'external-uri';
|
|
517
|
+
reason: string;
|
|
518
|
+
}[],
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// M4 (tweak-20260611-skin-fox-3clip-and-kb-sample-assets): SkinAsset.refs[]
|
|
522
|
+
// carries the skeleton GUID (each skin binds 1:1 to a SkeletonAsset; the
|
|
523
|
+
// skeletonGuid field is the on-asset cross-reference; refs[] is the runner-
|
|
524
|
+
// visible cross-edge used for pack-index ordering and integrity checks).
|
|
525
|
+
// Build the index now so the skin emit branch below can stamp it.
|
|
526
|
+
const skeletonGuidBySourceIndex = new Map<number, string>();
|
|
527
|
+
for (const sub of ctx.subAssets) {
|
|
528
|
+
if (sub.kind === 'skeleton') skeletonGuidBySourceIndex.set(sub.sourceIndex, sub.guid);
|
|
529
|
+
}
|
|
530
|
+
// feat-20260612 M2 fixup: parallel skin GUID index. Skins and skeletons
|
|
531
|
+
// share `sourceIndex` (toAssetPack emits 1:1 per GltfSkeletonRecord) but live
|
|
532
|
+
// as distinct `kind` sub-assets, so the SkinAsset GUIDs differ from the
|
|
533
|
+
// SkeletonAsset GUIDs. The scene branch below appends these GUIDs to both
|
|
534
|
+
// its refs[] (the runtime recursion source: loadByGuid<SceneAsset> walks
|
|
535
|
+
// envelope.refs to recursively pull every SkinAsset before instantiate) and
|
|
536
|
+
// its payload.skinGuids (the reverse-decode hint) -- without the refs[] edge,
|
|
537
|
+
// browser-async-pack-fetch never loads SkinAssets and Skin.joints stays
|
|
538
|
+
// length=0).
|
|
539
|
+
const skinGuidBySourceIndex = new Map<number, string>();
|
|
540
|
+
for (const sub of ctx.subAssets) {
|
|
541
|
+
if (sub.kind === 'skin') skinGuidBySourceIndex.set(sub.sourceIndex, sub.guid);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const out: ImportedAsset[] = [];
|
|
545
|
+
const isMultiAsset = ctx.subAssets.length > 1;
|
|
546
|
+
for (const sub of ctx.subAssets) {
|
|
547
|
+
if (sub.kind === 'mesh') {
|
|
548
|
+
// sub.sourceIndex now indexes glTF mesh-index (not flat GltfMeshIr index).
|
|
549
|
+
// parseGltf flattens N glTF meshes with M_i primitives into sum(M_i)
|
|
550
|
+
// GltfMeshIr rows, all sharing meshIndex; gather all rows whose
|
|
551
|
+
// meshIndex === sub.sourceIndex (preserving doc.meshes order so the
|
|
552
|
+
// bridge's positional materials[i] <-> submeshes[i] pairing aligns)
|
|
553
|
+
// and let meshIrToMeshAsset interleave them into one MeshAsset with
|
|
554
|
+
// N Submesh entries.
|
|
555
|
+
const prims = doc.meshes.filter((m) => m.meshIndex === sub.sourceIndex);
|
|
556
|
+
if (prims.length === 0) continue;
|
|
557
|
+
const meshName = isMultiAsset ? prims[0]?.name : undefined;
|
|
558
|
+
const materialNameByIndex = new Map<number, string>();
|
|
559
|
+
const materialSourceKeyByIndex = new Map<number, string>();
|
|
560
|
+
for (let materialIndex = 0; materialIndex < doc.materials.length; materialIndex++) {
|
|
561
|
+
const name = doc.materials[materialIndex]?.name;
|
|
562
|
+
if (typeof name === 'string') materialNameByIndex.set(materialIndex, name);
|
|
563
|
+
}
|
|
564
|
+
for (const material of ctx.subAssets) {
|
|
565
|
+
if (material.kind === 'material' && material.sourceKey !== undefined) {
|
|
566
|
+
materialSourceKeyByIndex.set(material.sourceIndex, material.sourceKey);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const bridged = meshIrToMeshAsset(prims, {
|
|
570
|
+
guidByIndex: maps.materialGuidByIndex,
|
|
571
|
+
nameByIndex: materialNameByIndex,
|
|
572
|
+
sourceKeyByIndex: materialSourceKeyByIndex,
|
|
573
|
+
});
|
|
574
|
+
if (!bridged.ok) {
|
|
575
|
+
return {
|
|
576
|
+
ok: false,
|
|
577
|
+
error: new ImportError({
|
|
578
|
+
code: 'import-internal-error',
|
|
579
|
+
expected: 'gltf mesh bridge to produce a canonical MeshAsset',
|
|
580
|
+
actual: bridged.error.code,
|
|
581
|
+
hint: 'repair the source primitive and re-run the glTF importer',
|
|
582
|
+
detail: {
|
|
583
|
+
reason: `gltf mesh bridge rejected mesh ${sub.sourceIndex}; inspect the bridge error detail`,
|
|
584
|
+
},
|
|
585
|
+
}),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
const meshPayload = stabilizeMeshMaterialSlots(bridged.value, ctx, sub.guid, sub.sourceKey);
|
|
589
|
+
const materialRefs: AssetRef[] = [];
|
|
590
|
+
const seenMaterialGuids = new Set<string>();
|
|
591
|
+
for (let slotIndex = 0; slotIndex < meshPayload.materialSlots.length; slotIndex++) {
|
|
592
|
+
const defaultMaterial = meshPayload.materialSlots[slotIndex]?.defaultMaterial;
|
|
593
|
+
const guid =
|
|
594
|
+
defaultMaterial === undefined ? undefined : AssetGuidCodec.format(defaultMaterial);
|
|
595
|
+
if (guid !== undefined && !seenMaterialGuids.has(guid.toLowerCase())) {
|
|
596
|
+
seenMaterialGuids.add(guid.toLowerCase());
|
|
597
|
+
materialRefs.push({
|
|
598
|
+
guid,
|
|
599
|
+
sourceField: { fieldName: 'materialSlots', arrayIndex: slotIndex },
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
out.push({
|
|
604
|
+
guid: sub.guid,
|
|
605
|
+
kind: 'mesh',
|
|
606
|
+
...(meshName !== undefined ? { name: meshName } : {}),
|
|
607
|
+
payload: meshPayload,
|
|
608
|
+
refs: materialRefs,
|
|
609
|
+
artifacts: {
|
|
610
|
+
body: {
|
|
611
|
+
mediaType: 'application/x-forgeax-mesh',
|
|
612
|
+
assetCodec: { name: 'mesh-binary', version: '4' },
|
|
613
|
+
bytes: (() => {
|
|
614
|
+
const packed = packMeshBinV4(
|
|
615
|
+
meshPayload as never,
|
|
616
|
+
sub.sourceKey ?? ctx.source,
|
|
617
|
+
materialRefs.map((ref) => ref.guid),
|
|
618
|
+
);
|
|
619
|
+
if (!packed.ok) {
|
|
620
|
+
throw new ImportError({
|
|
621
|
+
code: 'import-internal-error',
|
|
622
|
+
expected: 'mesh-bin v4 producer to accept the canonical mesh projection',
|
|
623
|
+
hint: 're-cook the source with its Meta sidecar after fixing the mesh payload',
|
|
624
|
+
detail: { reason: `${packed.error.code}: ${packed.error.actual}` },
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
return packed.value;
|
|
628
|
+
})(),
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
} else if (sub.kind === 'material') {
|
|
633
|
+
const mat = doc.materials[sub.sourceIndex];
|
|
634
|
+
if (mat === undefined) continue;
|
|
635
|
+
// feat-20260611 w17-a: scan doc.meshes[] for any primitive that (a)
|
|
636
|
+
// references this material and (b) carries JOINTS_0 + WEIGHTS_0. If
|
|
637
|
+
// found, route the emitted MaterialAsset to `forgeax::pbr-skin`.
|
|
638
|
+
// Mirrors the per-MeshAsset 18F-stride decision in meshIrToMeshAsset
|
|
639
|
+
// (D-2): a material consumed by any skinned primitive must use the
|
|
640
|
+
// skin shader so the runtime PSO chain (LayoutKind='pbr-skin' +
|
|
641
|
+
// 6-attribute deriveVertexBufferLayout) is exercised.
|
|
642
|
+
let skinned = false;
|
|
643
|
+
for (const meshIr of doc.meshes) {
|
|
644
|
+
if (meshIr.materialIndex !== sub.sourceIndex) continue;
|
|
645
|
+
if (meshIr.joints0 !== undefined && meshIr.weights0 !== undefined) {
|
|
646
|
+
skinned = true;
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
for (let primitiveIndex = 0; primitiveIndex < doc.meshes.length; primitiveIndex++) {
|
|
651
|
+
const meshIr = doc.meshes[primitiveIndex];
|
|
652
|
+
if (meshIr?.materialIndex !== sub.sourceIndex) continue;
|
|
653
|
+
const uvResult = validateMaterialUvSets(
|
|
654
|
+
mat,
|
|
655
|
+
`primitive-${primitiveIndex}`,
|
|
656
|
+
availableUvSets(meshIr),
|
|
657
|
+
);
|
|
658
|
+
if (!uvResult.ok) {
|
|
659
|
+
throw Object.assign(new Error(uvResult.error.message), uvResult.error);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const matAsset = toMaterialAsset(mat, {
|
|
663
|
+
textureHandles: maps.textureHandles,
|
|
664
|
+
samplerHandles: maps.samplerHandles,
|
|
665
|
+
skinned,
|
|
666
|
+
...(typeof ctx.importSettings.standardMaterialGuid === 'string'
|
|
667
|
+
? { standardRootGuid: ctx.importSettings.standardMaterialGuid as unknown as AssetGuid }
|
|
668
|
+
: {}),
|
|
669
|
+
});
|
|
670
|
+
const refs = materialRefsForPack(mat, doc, maps.textureGuidByIndex, maps.samplerGuidByIndex);
|
|
671
|
+
// D-8: if material has a parent, add parent edge to refs
|
|
672
|
+
const materialRefs: AssetRef[] = [...refs];
|
|
673
|
+
if (matAsset.parent !== undefined) {
|
|
674
|
+
materialRefs.push({
|
|
675
|
+
guid: matAsset.parent as unknown as string,
|
|
676
|
+
sourceField: { fieldName: 'parent' },
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
const rewrittenAsset = rewriteMaterialAssetRefs(matAsset, mat, doc, maps);
|
|
680
|
+
const matName = isMultiAsset ? mat.name : undefined;
|
|
681
|
+
out.push({
|
|
682
|
+
guid: sub.guid,
|
|
683
|
+
kind: 'material',
|
|
684
|
+
...(matName !== undefined ? { name: matName } : {}),
|
|
685
|
+
payload: rewrittenAsset,
|
|
686
|
+
refs: materialRefs,
|
|
687
|
+
artifacts: {},
|
|
688
|
+
});
|
|
689
|
+
} else if (sub.kind === 'texture') {
|
|
690
|
+
const imageIndex = sub.sourceIndex;
|
|
691
|
+
const extracted = extraction.extracted.get(imageIndex);
|
|
692
|
+
if (extracted === undefined) {
|
|
693
|
+
const failure = extraction.failures.find((f) => f.imageIndex === imageIndex);
|
|
694
|
+
const detail = failure ?? {
|
|
695
|
+
imageIndex,
|
|
696
|
+
source: 'bufferView' as const,
|
|
697
|
+
reason: 'image row missing from extraction map (no images[] entry?)',
|
|
698
|
+
};
|
|
699
|
+
const error = gltfErr('gltf-image-extract-failed', detail);
|
|
700
|
+
throw new Error(
|
|
701
|
+
`gltfImporter: ${error.code} on image ${imageIndex} (${detail.source}): ${detail.reason}`,
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
const colorSpace = imageColorSpaces.get(imageIndex) ?? 'linear';
|
|
705
|
+
// Carry colorSpace + mipmap settings into decodeImage via a per-image
|
|
706
|
+
// settings record (mirror of importImageSettings — but here the importer
|
|
707
|
+
// owns the decision because the seam is in-bounds for AC-12 (c)).
|
|
708
|
+
const decodeSettings = {
|
|
709
|
+
...ctx.importSettings,
|
|
710
|
+
colorSpace,
|
|
711
|
+
mipmap: ctx.importSettings.mipmap ?? true,
|
|
712
|
+
};
|
|
713
|
+
const decoded = await ctx.decodeImage(extracted.bytes, extracted.mimeType, decodeSettings);
|
|
714
|
+
if (!decoded.ok) {
|
|
715
|
+
const reason = `decodeImage failed: ${decoded.error.code}`;
|
|
716
|
+
const error = gltfErr('gltf-image-extract-failed', {
|
|
717
|
+
imageIndex,
|
|
718
|
+
source: extracted.source,
|
|
719
|
+
reason,
|
|
720
|
+
});
|
|
721
|
+
throw new Error(
|
|
722
|
+
`gltfImporter: ${error.code} on image ${imageIndex} (${extracted.source}): ${reason}`,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
const imageItem = (doc.images ?? [])[imageIndex];
|
|
726
|
+
const texName = isMultiAsset ? imageItem?.name : undefined;
|
|
727
|
+
out.push({
|
|
728
|
+
guid: sub.guid,
|
|
729
|
+
kind: 'texture',
|
|
730
|
+
...(texName !== undefined ? { name: texName } : {}),
|
|
731
|
+
payload: decoded.value.texture,
|
|
732
|
+
refs: [],
|
|
733
|
+
artifacts: {
|
|
734
|
+
body: {
|
|
735
|
+
mediaType: decoded.value.mediaType ?? extracted.mimeType,
|
|
736
|
+
assetCodec: decoded.value.assetCodec ?? { name: 'rgba8', version: '1' },
|
|
737
|
+
bytes: decoded.value.bytes,
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
} else if (sub.kind === 'sampler') {
|
|
742
|
+
const sampler = doc.samplers?.[sub.sourceIndex];
|
|
743
|
+
if (sampler === undefined) continue;
|
|
744
|
+
const filter = (value: number | undefined): 'nearest' | 'linear' | undefined => {
|
|
745
|
+
if (value === undefined) return undefined;
|
|
746
|
+
return value === 9728 || value === 9984 || value === 9986 || value === 9988
|
|
747
|
+
? 'nearest'
|
|
748
|
+
: 'linear';
|
|
749
|
+
};
|
|
750
|
+
const mipmapFilter = (value: number | undefined): 'nearest' | 'linear' | undefined => {
|
|
751
|
+
if (value === undefined) return undefined;
|
|
752
|
+
return value === 9984 || value === 9985 ? 'nearest' : 'linear';
|
|
753
|
+
};
|
|
754
|
+
const addressMode = (value: number): 'repeat' | 'mirror-repeat' | 'clamp-to-edge' => {
|
|
755
|
+
if (value === 33071) return 'clamp-to-edge';
|
|
756
|
+
if (value === 33648) return 'mirror-repeat';
|
|
757
|
+
return 'repeat';
|
|
758
|
+
};
|
|
759
|
+
const magFilter = filter(sampler.magFilter);
|
|
760
|
+
const minFilter = filter(sampler.minFilter);
|
|
761
|
+
const mipmap = mipmapFilter(sampler.minFilter);
|
|
762
|
+
const payload = {
|
|
763
|
+
kind: 'sampler' as const,
|
|
764
|
+
...(magFilter === undefined ? {} : { magFilter }),
|
|
765
|
+
...(minFilter === undefined ? {} : { minFilter }),
|
|
766
|
+
...(mipmap === undefined ? {} : { mipmapFilter: mipmap }),
|
|
767
|
+
addressModeU: addressMode(sampler.wrapS),
|
|
768
|
+
addressModeV: addressMode(sampler.wrapT),
|
|
769
|
+
};
|
|
770
|
+
out.push({ guid: sub.guid, kind: 'sampler', payload, refs: [], artifacts: {} });
|
|
771
|
+
} else if (sub.kind === 'scene') {
|
|
772
|
+
// #317 multi-material design: bridge accepts glTF mesh-index keyed
|
|
773
|
+
// meshHandles + materialHandles only; primitive merge happens via
|
|
774
|
+
// doc.meshes[] filtering on meshIndex inside the bridge.
|
|
775
|
+
// tweak-20260611 M6: also pass skeletonGuidBySkinIndex so the bridge
|
|
776
|
+
// can stamp Skin component (skeleton GUID string) onto skinned mesh
|
|
777
|
+
// entities. Skins are 1:1 with skeletons in toAssetPack so the same
|
|
778
|
+
// sourceIndex map serves both purposes (the field name says
|
|
779
|
+
// "BySkinIndex" because the bridge keys by GltfNodeIr.skinIndex).
|
|
780
|
+
const scene = gltfDocToSceneAsset(doc, {
|
|
781
|
+
meshHandles: maps.meshHandles,
|
|
782
|
+
materialHandles: maps.materialHandles,
|
|
783
|
+
skeletonGuidBySkinIndex: skeletonGuidBySourceIndex,
|
|
784
|
+
});
|
|
785
|
+
// Scene refs contain only dependencies authored directly by the scene:
|
|
786
|
+
// mesh, skeleton, and skin assets. Imported material defaults belong to
|
|
787
|
+
// MeshAsset.materialSlots, and textures belong to MaterialAsset refs;
|
|
788
|
+
// repeating either here would create a second dependency owner.
|
|
789
|
+
// Skeleton GUIDs are appended so the runtime asset graph sees the
|
|
790
|
+
// cross-edge when a skinned mesh node carries Skin { skeleton:
|
|
791
|
+
// <guid-string> }; skin GUIDs (feat-20260612 M2 fixup) carry the
|
|
792
|
+
// SkinAsset cross-edge that has no entity-component representation but
|
|
793
|
+
// is required for postSpawnResolveJoints to resolve Skin.joints[] via
|
|
794
|
+
// SkinAsset.jointPaths.
|
|
795
|
+
//
|
|
796
|
+
// D-2 / D-3: refs carries structured edge metadata (AssetRef[]).
|
|
797
|
+
// Walk scene entities to build a handle-value -> (entityLocalId,
|
|
798
|
+
// componentName, fieldName, arrayIndex?) provenance map, then
|
|
799
|
+
// produce AssetRef[] with sourceField / sceneEntityId filled for mesh
|
|
800
|
+
// handle-field edges. Skeleton edges: sourceField from Skin.skeleton if entity
|
|
801
|
+
// carries that GUID. Skin edges: sourceField=undefined (cross-edge
|
|
802
|
+
// with no entity-component representation).
|
|
803
|
+
const handleValueProvenance = new Map<
|
|
804
|
+
number,
|
|
805
|
+
{ sceneEntityId: number; componentName: string; fieldName: string; arrayIndex?: number }
|
|
806
|
+
>();
|
|
807
|
+
const skeletonGuidProvenance = new Map<string, { sceneEntityId: number }>();
|
|
808
|
+
for (const entity of scene.entities) {
|
|
809
|
+
const comps = entity.components as Record<string, Record<string, unknown>>;
|
|
810
|
+
const mf = comps.MeshFilter;
|
|
811
|
+
if (mf !== undefined && typeof mf.assetHandle === 'number') {
|
|
812
|
+
handleValueProvenance.set(mf.assetHandle, {
|
|
813
|
+
sceneEntityId: entity.localId,
|
|
814
|
+
componentName: 'MeshFilter',
|
|
815
|
+
fieldName: 'assetHandle',
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
const skin = comps.Skin;
|
|
819
|
+
if (skin !== undefined && typeof skin.skeleton === 'string') {
|
|
820
|
+
skeletonGuidProvenance.set(skin.skeleton, { sceneEntityId: entity.localId });
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const meshGuidList = [...maps.meshGuidByIndex.values()];
|
|
825
|
+
|
|
826
|
+
function makeRef(guid: string, idx: number): AssetRef {
|
|
827
|
+
const prov = handleValueProvenance.get(idx);
|
|
828
|
+
if (prov !== undefined) {
|
|
829
|
+
return {
|
|
830
|
+
guid,
|
|
831
|
+
sourceField: {
|
|
832
|
+
componentName: prov.componentName,
|
|
833
|
+
fieldName: prov.fieldName,
|
|
834
|
+
...(prov.arrayIndex !== undefined ? { arrayIndex: prov.arrayIndex } : {}),
|
|
835
|
+
},
|
|
836
|
+
sceneEntityId: prov.sceneEntityId,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
return { guid };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const refs: AssetRef[] = [];
|
|
843
|
+
let cursor = 0;
|
|
844
|
+
for (const guid of meshGuidList) {
|
|
845
|
+
refs.push(makeRef(guid, cursor));
|
|
846
|
+
cursor++;
|
|
847
|
+
}
|
|
848
|
+
{
|
|
849
|
+
const skeletonGuidList = [...skeletonGuidBySourceIndex.values()];
|
|
850
|
+
for (const guid of skeletonGuidList) {
|
|
851
|
+
const skProv = skeletonGuidProvenance.get(guid);
|
|
852
|
+
refs.push(
|
|
853
|
+
skProv !== undefined
|
|
854
|
+
? {
|
|
855
|
+
guid,
|
|
856
|
+
sourceField: { componentName: 'Skin', fieldName: 'skeleton' },
|
|
857
|
+
sceneEntityId: skProv.sceneEntityId,
|
|
858
|
+
}
|
|
859
|
+
: { guid },
|
|
860
|
+
);
|
|
861
|
+
cursor++;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
{
|
|
865
|
+
const skinGuidList = [...skinGuidBySourceIndex.values()];
|
|
866
|
+
for (const guid of skinGuidList) {
|
|
867
|
+
refs.push({ guid });
|
|
868
|
+
cursor++;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
// Inject skinGuids into the scene payload as the reverse-decode hint; the
|
|
872
|
+
// SkinAsset GUIDs are already in the scene envelope's refs[] above, which
|
|
873
|
+
// is the runtime recursion source for the browser-async-pack-fetch path.
|
|
874
|
+
// Stored as inline GUID strings (in-memory dawn smoke path); the on-disk
|
|
875
|
+
// .pack.json round-trip preserves them as strings -- parseScenePayload's
|
|
876
|
+
// resolveSkinGuids accepts both string and refs[]-index shapes.
|
|
877
|
+
const skinGuidList = [...skinGuidBySourceIndex.values()];
|
|
878
|
+
const sceneWithSkinGuids =
|
|
879
|
+
skinGuidList.length > 0 ? { ...scene, skinGuids: skinGuidList } : scene;
|
|
880
|
+
const sceneName = isMultiAsset ? doc.scenes[sub.sourceIndex]?.name : undefined;
|
|
881
|
+
out.push({
|
|
882
|
+
guid: sub.guid,
|
|
883
|
+
kind: 'scene',
|
|
884
|
+
...(sceneName !== undefined ? { name: sceneName } : {}),
|
|
885
|
+
payload: sceneWithSkinGuids,
|
|
886
|
+
refs,
|
|
887
|
+
artifacts: {},
|
|
888
|
+
});
|
|
889
|
+
} else if (sub.kind === 'skeleton') {
|
|
890
|
+
// tweak-20260611 M4: skeleton sub-asset POD emit. GltfSkeletonRecord (the
|
|
891
|
+
// gltf IR shape from parse-skin) carries inverseBindMatrices + jointCount
|
|
892
|
+
// + jointPaths; SkeletonAsset (the runtime POD) carries IBM + jointCount
|
|
893
|
+
// only — jointPaths live on the parallel SkinAsset (1:1 mapping per
|
|
894
|
+
// toAssetPack's emit policy). refs[] is empty (skeleton is leaf data).
|
|
895
|
+
const rec = doc.skeletons[sub.sourceIndex];
|
|
896
|
+
if (rec === undefined) continue;
|
|
897
|
+
const payload = {
|
|
898
|
+
kind: 'skeleton' as const,
|
|
899
|
+
inverseBindMatrices: rec.inverseBindMatrices,
|
|
900
|
+
jointCount: rec.jointCount,
|
|
901
|
+
};
|
|
902
|
+
out.push({ guid: sub.guid, kind: 'skeleton', payload, refs: [], artifacts: {} });
|
|
903
|
+
} else if (sub.kind === 'skin') {
|
|
904
|
+
// tweak-20260611 M4: skin sub-asset POD emit. The 1:1 mapping with
|
|
905
|
+
// skeleton (toAssetPack emits one skin per GltfSkeletonRecord at the same
|
|
906
|
+
// sourceIndex) lets us pull the skeleton GUID out of the pre-built
|
|
907
|
+
// skeletonGuidBySourceIndex map. SkinAsset is the runtime POD, distinct
|
|
908
|
+
// from GltfSkeletonRecord: skeletonGuid + jointPaths only (zero entity refs
|
|
909
|
+
// at the asset layer per AC-06). refs[] carries the skeletonGuid so the
|
|
910
|
+
// runner sees the cross-edge.
|
|
911
|
+
const rec = doc.skeletons[sub.sourceIndex];
|
|
912
|
+
if (rec === undefined) continue;
|
|
913
|
+
const skeletonGuid = skeletonGuidBySourceIndex.get(sub.sourceIndex);
|
|
914
|
+
if (skeletonGuid === undefined) continue;
|
|
915
|
+
const payload = {
|
|
916
|
+
kind: 'skin' as const,
|
|
917
|
+
skeletonGuid,
|
|
918
|
+
jointPaths: rec.jointPaths,
|
|
919
|
+
};
|
|
920
|
+
out.push({
|
|
921
|
+
guid: sub.guid,
|
|
922
|
+
kind: 'skin',
|
|
923
|
+
payload,
|
|
924
|
+
refs: [{ guid: skeletonGuid, sourceField: { fieldName: 'skeleton' } }],
|
|
925
|
+
artifacts: {},
|
|
926
|
+
});
|
|
927
|
+
} else if (sub.kind === 'animation-clip') {
|
|
928
|
+
// tweak-20260611 M4: animation-clip sub-asset POD emit. GltfAnimationClipRecord
|
|
929
|
+
// (gltf IR) and AnimationClip (runtime POD) are structurally compatible
|
|
930
|
+
// — both carry duration + channels[]; channels' inner shape matches
|
|
931
|
+
// (targetId / property / sampler). property is narrowed to the runtime
|
|
932
|
+
// closed union by the parser. refs[] is empty (animation clips reference joints by name path
|
|
933
|
+
// resolved at post-spawn time, not by sub-asset cross-edge).
|
|
934
|
+
const rec = doc.animationClips[sub.sourceIndex];
|
|
935
|
+
if (rec === undefined) continue;
|
|
936
|
+
const payload = {
|
|
937
|
+
kind: 'animation-clip' as const,
|
|
938
|
+
duration: rec.duration,
|
|
939
|
+
channels: rec.channels.map((ch) => ({
|
|
940
|
+
targetId: ch.targetId,
|
|
941
|
+
property: ch.property,
|
|
942
|
+
sampler: {
|
|
943
|
+
input: ch.sampler.input,
|
|
944
|
+
output: ch.sampler.output,
|
|
945
|
+
interpolation: ch.sampler.interpolation,
|
|
946
|
+
},
|
|
947
|
+
})),
|
|
948
|
+
};
|
|
949
|
+
out.push({ guid: sub.guid, kind: 'animation-clip', payload, refs: [], artifacts: {} });
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return { ok: true, value: { assets: out, sourceDependencies: [] } };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* The gltf {@link Importer}. Register it into an `ImporterRegistry` so the
|
|
957
|
+
* import runner dispatches `meta.importer === 'gltf'` sidecars here.
|
|
958
|
+
*
|
|
959
|
+
* @example
|
|
960
|
+
* ```ts
|
|
961
|
+
* import { ImporterRegistry } from '@forgeax/engine-import';
|
|
962
|
+
* import { gltfImporter } from '@forgeax/engine-gltf';
|
|
963
|
+
* const importers = new ImporterRegistry();
|
|
964
|
+
* importers.register(gltfImporter);
|
|
965
|
+
* ```
|
|
966
|
+
*/
|
|
967
|
+
export function createGltfImporter(meshopt?: GltfBufferViewDecodeCapability): Importer {
|
|
968
|
+
return {
|
|
969
|
+
key: 'gltf',
|
|
970
|
+
import: (ctx) => importGltf(ctx, meshopt),
|
|
971
|
+
capabilities: { catalog: { publish: publishesCatalogProduct } },
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** Importer for hosts that provide an optional build-only Meshopt decoder. */
|
|
976
|
+
export const gltfImporter: Importer = createGltfImporter();
|