@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,1731 @@
|
|
|
1
|
+
// parse-gltf.ts - JSON-side glTF importer entry (w17).
|
|
2
|
+
//
|
|
3
|
+
// w17 lands the full parseGltf main function plus parseGlb / toAssetPack
|
|
4
|
+
// siblings. The helpers exported here include both the surface API
|
|
5
|
+
// (parseGltf / parseGlb / toAssetPack) and a few mesh / scene utilities
|
|
6
|
+
// the main path uses internally.
|
|
7
|
+
|
|
8
|
+
import { mat4, quat, vec3 } from '@forgeax/engine-math';
|
|
9
|
+
import {
|
|
10
|
+
IMPORT_ERROR_HINTS,
|
|
11
|
+
ImportError,
|
|
12
|
+
MESH_MATERIAL_SLOT_SOURCE_OVERRIDE_PAYLOAD_SCHEMA,
|
|
13
|
+
type MeshMaterialSlotTopologyEntry,
|
|
14
|
+
reconcileMeshMaterialSlotTopology,
|
|
15
|
+
} from '@forgeax/engine-types';
|
|
16
|
+
import {
|
|
17
|
+
type AccessorJson,
|
|
18
|
+
type BufferViewJson,
|
|
19
|
+
COMPONENT_TYPE,
|
|
20
|
+
decodeAccessor,
|
|
21
|
+
} from './accessor/decode-accessor.js';
|
|
22
|
+
import { decodeColorAccessor } from './accessor/decode-color.js';
|
|
23
|
+
import { checkExtensions, type GltfExtensionsJson } from './check-extensions.js';
|
|
24
|
+
import { Base64DecodeError, dataUriBase64Payload, decodeBase64 } from './data-uri.js';
|
|
25
|
+
import { err, type GltfError, gltfErr, ok, type Result } from './errors.js';
|
|
26
|
+
import {
|
|
27
|
+
type GltfBufferViewDecodeCapability,
|
|
28
|
+
type MeshoptBufferViewJson,
|
|
29
|
+
projectMeshoptBufferViews,
|
|
30
|
+
} from './meshopt-decode.js';
|
|
31
|
+
import { type GltfAnimationClipRecord, parseAnimation } from './parse-animation.js';
|
|
32
|
+
import { parseGlbChunks } from './parse-glb-chunks.js';
|
|
33
|
+
import { parseGltfHeader } from './parse-gltf-header.js';
|
|
34
|
+
import { type GltfSkeletonRecord, parseSkin } from './parse-skin.js';
|
|
35
|
+
import {
|
|
36
|
+
type GltfDocItem,
|
|
37
|
+
type GltfMetaJson,
|
|
38
|
+
type GltfSubAssetEntry,
|
|
39
|
+
reimportReuseMeta,
|
|
40
|
+
} from './reimport-reuse-meta.js';
|
|
41
|
+
import type { GltfSourceKeyError } from './source-key.js';
|
|
42
|
+
import { type DecomposedTransform, decomposeNodeTransform } from './transform.js';
|
|
43
|
+
|
|
44
|
+
export interface MeshPrimitiveJson {
|
|
45
|
+
readonly attributes?: Record<string, number>;
|
|
46
|
+
readonly indices?: number;
|
|
47
|
+
readonly material?: number;
|
|
48
|
+
readonly mode?: number;
|
|
49
|
+
readonly targets?: ReadonlyArray<Record<string, number>>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface MeshJson {
|
|
53
|
+
readonly name?: string;
|
|
54
|
+
readonly weights?: readonly number[];
|
|
55
|
+
readonly primitives: readonly MeshPrimitiveJson[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// === Tier-B v1 IR (GltfDoc) ===
|
|
59
|
+
//
|
|
60
|
+
// Shape: a denormalised view of the parsed glTF JSON, post-validation,
|
|
61
|
+
// suitable for downstream `toAssetPack` and runtime AssetRegistry
|
|
62
|
+
// hand-off. Math types are POD (number tuples), no Vec3/Quat brand at
|
|
63
|
+
// the boundary (charter proposition 5).
|
|
64
|
+
|
|
65
|
+
interface InstancingAttributes {
|
|
66
|
+
readonly TRANSLATION?: number;
|
|
67
|
+
readonly ROTATION?: number;
|
|
68
|
+
readonly SCALE?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Decode EXT_mesh_gpu_instancing attributes for a single node.
|
|
73
|
+
*
|
|
74
|
+
* Cross-validates that all present TRS accessor counts agree (spec
|
|
75
|
+
* MUST clause "Extending nodes with instance attributes"). Missing
|
|
76
|
+
* accessors fall back to identity (translation=(0,0,0),
|
|
77
|
+
* rotation=(0,0,0,1), scale=(1,1,1)). Composes N column-major mat4 via
|
|
78
|
+
* @forgeax/engine-math `mat4.compose`, packing them into a single
|
|
79
|
+
* Float32Array of length N*16. Pure function: no fs / fetch / throw.
|
|
80
|
+
*/
|
|
81
|
+
function decodeNodeInstancing(
|
|
82
|
+
nodeIndex: number,
|
|
83
|
+
attributes: InstancingAttributes,
|
|
84
|
+
accessors: readonly AccessorJson[],
|
|
85
|
+
bufferViews: readonly BufferViewJson[],
|
|
86
|
+
buffers: readonly Uint8Array[],
|
|
87
|
+
): Result<NodeInstancingIr, GltfError> {
|
|
88
|
+
const tIdx = attributes.TRANSLATION;
|
|
89
|
+
const rIdx = attributes.ROTATION;
|
|
90
|
+
const sIdx = attributes.SCALE;
|
|
91
|
+
|
|
92
|
+
let count: number | undefined;
|
|
93
|
+
const setOrCheck = (label: 'TRANSLATION' | 'ROTATION' | 'SCALE', n: number): GltfError | null => {
|
|
94
|
+
if (count === undefined) {
|
|
95
|
+
count = n;
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (n !== count) {
|
|
99
|
+
return gltfErr('gltf-instancing-count-mismatch', {
|
|
100
|
+
nodeIndex,
|
|
101
|
+
accessor: label,
|
|
102
|
+
expectedCount: count,
|
|
103
|
+
actualCount: n,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let tValues: Float32Array | undefined;
|
|
110
|
+
if (tIdx !== undefined) {
|
|
111
|
+
const acc = accessors[tIdx];
|
|
112
|
+
if (acc === undefined) return err(unknownAccessor(tIdx));
|
|
113
|
+
const e = setOrCheck('TRANSLATION', acc.count);
|
|
114
|
+
if (e !== null) return err(e);
|
|
115
|
+
const decoded = decodeAttributeAccessor(tIdx, acc, bufferViews, buffers);
|
|
116
|
+
if (!decoded.ok) return err(decoded.error);
|
|
117
|
+
tValues = decoded.value;
|
|
118
|
+
}
|
|
119
|
+
let rValues: Float32Array | undefined;
|
|
120
|
+
if (rIdx !== undefined) {
|
|
121
|
+
const acc = accessors[rIdx];
|
|
122
|
+
if (acc === undefined) return err(unknownAccessor(rIdx));
|
|
123
|
+
const e = setOrCheck('ROTATION', acc.count);
|
|
124
|
+
if (e !== null) return err(e);
|
|
125
|
+
const decoded = decodeAttributeAccessor(rIdx, acc, bufferViews, buffers);
|
|
126
|
+
if (!decoded.ok) return err(decoded.error);
|
|
127
|
+
rValues = decoded.value;
|
|
128
|
+
}
|
|
129
|
+
let sValues: Float32Array | undefined;
|
|
130
|
+
if (sIdx !== undefined) {
|
|
131
|
+
const acc = accessors[sIdx];
|
|
132
|
+
if (acc === undefined) return err(unknownAccessor(sIdx));
|
|
133
|
+
const e = setOrCheck('SCALE', acc.count);
|
|
134
|
+
if (e !== null) return err(e);
|
|
135
|
+
const decoded = decodeAttributeAccessor(sIdx, acc, bufferViews, buffers);
|
|
136
|
+
if (!decoded.ok) return err(decoded.error);
|
|
137
|
+
sValues = decoded.value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const n = count ?? 0;
|
|
141
|
+
const transforms = new Float32Array(n * 16);
|
|
142
|
+
const tmp = mat4.create();
|
|
143
|
+
const tv = vec3.create();
|
|
144
|
+
const rv = quat.create();
|
|
145
|
+
rv[3] = 1;
|
|
146
|
+
const sv = vec3.create(1, 1, 1);
|
|
147
|
+
for (let i = 0; i < n; i++) {
|
|
148
|
+
if (tValues !== undefined) {
|
|
149
|
+
tv[0] = tValues[i * 3] ?? 0;
|
|
150
|
+
tv[1] = tValues[i * 3 + 1] ?? 0;
|
|
151
|
+
tv[2] = tValues[i * 3 + 2] ?? 0;
|
|
152
|
+
} else {
|
|
153
|
+
tv[0] = 0;
|
|
154
|
+
tv[1] = 0;
|
|
155
|
+
tv[2] = 0;
|
|
156
|
+
}
|
|
157
|
+
if (rValues !== undefined) {
|
|
158
|
+
rv[0] = rValues[i * 4] ?? 0;
|
|
159
|
+
rv[1] = rValues[i * 4 + 1] ?? 0;
|
|
160
|
+
rv[2] = rValues[i * 4 + 2] ?? 0;
|
|
161
|
+
rv[3] = rValues[i * 4 + 3] ?? 1;
|
|
162
|
+
} else {
|
|
163
|
+
rv[0] = 0;
|
|
164
|
+
rv[1] = 0;
|
|
165
|
+
rv[2] = 0;
|
|
166
|
+
rv[3] = 1;
|
|
167
|
+
}
|
|
168
|
+
if (sValues !== undefined) {
|
|
169
|
+
sv[0] = sValues[i * 3] ?? 1;
|
|
170
|
+
sv[1] = sValues[i * 3 + 1] ?? 1;
|
|
171
|
+
sv[2] = sValues[i * 3 + 2] ?? 1;
|
|
172
|
+
} else {
|
|
173
|
+
sv[0] = 1;
|
|
174
|
+
sv[1] = 1;
|
|
175
|
+
sv[2] = 1;
|
|
176
|
+
}
|
|
177
|
+
mat4.compose(tmp, tv, rv, sv);
|
|
178
|
+
for (let k = 0; k < 16; k++) {
|
|
179
|
+
transforms[i * 16 + k] = tmp[k] ?? 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return ok({ count: n, transforms });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function unknownAccessor(accessorIndex: number): GltfError {
|
|
187
|
+
return gltfErr('gltf-accessor-type-mismatch', {
|
|
188
|
+
accessorIndex,
|
|
189
|
+
reason: 'unknownComponentType',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function decodeAttributeAccessor(
|
|
194
|
+
accessorIndex: number,
|
|
195
|
+
accessor: AccessorJson,
|
|
196
|
+
bufferViews: readonly BufferViewJson[],
|
|
197
|
+
buffers: readonly Uint8Array[],
|
|
198
|
+
): Result<Float32Array, GltfError> {
|
|
199
|
+
const view = bufferViews[accessor.bufferView ?? -1];
|
|
200
|
+
if (view === undefined) return err(unknownAccessor(accessorIndex));
|
|
201
|
+
const buf = buffers[view.buffer];
|
|
202
|
+
if (buf === undefined) return err(unknownAccessor(accessorIndex));
|
|
203
|
+
const decoded = decodeAccessor({
|
|
204
|
+
accessorIndex,
|
|
205
|
+
accessor,
|
|
206
|
+
bufferView: view,
|
|
207
|
+
buffer: buf,
|
|
208
|
+
role: 'attribute',
|
|
209
|
+
});
|
|
210
|
+
if (!decoded.ok) return err(decoded.error);
|
|
211
|
+
if (decoded.value.kind !== 'f32') return err(unknownAccessor(accessorIndex));
|
|
212
|
+
return ok(decoded.value.data);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface GltfMeshIr {
|
|
216
|
+
readonly name?: string;
|
|
217
|
+
readonly positions: Float32Array;
|
|
218
|
+
readonly normals?: Float32Array;
|
|
219
|
+
readonly texcoord0?: Float32Array;
|
|
220
|
+
/** TEXCOORD_1 per-vertex UV set 1 (Float32Array, 2 per vertex). feat-20260629-multi-uv-set-support m1-w2. */
|
|
221
|
+
readonly texcoord1?: Float32Array;
|
|
222
|
+
/** TEXCOORD_2 per-vertex UV set 2 (Float32Array, 2 per vertex). */
|
|
223
|
+
readonly texcoord2?: Float32Array;
|
|
224
|
+
/** TEXCOORD_3 per-vertex UV set 3 (Float32Array, 2 per vertex). */
|
|
225
|
+
readonly texcoord3?: Float32Array;
|
|
226
|
+
/** TEXCOORD_4 per-vertex UV set 4 (Float32Array, 2 per vertex). */
|
|
227
|
+
readonly texcoord4?: Float32Array;
|
|
228
|
+
/** TEXCOORD_5 per-vertex UV set 5 (Float32Array, 2 per vertex). */
|
|
229
|
+
readonly texcoord5?: Float32Array;
|
|
230
|
+
/** TEXCOORD_6 per-vertex UV set 6 (Float32Array, 2 per vertex). */
|
|
231
|
+
readonly texcoord6?: Float32Array;
|
|
232
|
+
/** TEXCOORD_7 per-vertex UV set 7 (Float32Array, 2 per vertex). */
|
|
233
|
+
readonly texcoord7?: Float32Array;
|
|
234
|
+
readonly tangents?: Float32Array;
|
|
235
|
+
/** COLOR_0 importer carrier; bridge projects it to the canonical color attribute. */
|
|
236
|
+
readonly colors0?: Float32Array;
|
|
237
|
+
/** JOINTS_0 per-vertex joint indices (Uint16Array, 4 per vertex). UBYTE source is width-converted to U16 at parse time (D-3). */
|
|
238
|
+
readonly joints0?: Uint16Array;
|
|
239
|
+
/** WEIGHTS_0 per-vertex skin weights (Float32Array, 4 per vertex). */
|
|
240
|
+
readonly weights0?: Float32Array;
|
|
241
|
+
readonly morphTargets?: readonly {
|
|
242
|
+
readonly position?: Float32Array;
|
|
243
|
+
readonly normal?: Float32Array;
|
|
244
|
+
readonly tangent?: Float32Array;
|
|
245
|
+
}[];
|
|
246
|
+
readonly morphWeights?: Float32Array;
|
|
247
|
+
/**
|
|
248
|
+
* Optional per-glTF-spec: when omitted, primitive declares non-indexed
|
|
249
|
+
* geometry (vertex buffer is consumed in vertex order, every 3 verts =
|
|
250
|
+
* 1 triangle for triangle-list). bridge.ts handles undefined by routing
|
|
251
|
+
* MeshAsset.indices to undefined and submesh.indexCount=0 (vertexCount
|
|
252
|
+
* carries the draw count for `pass.draw(vertexCount)`).
|
|
253
|
+
* bug-20260612 hello-skin visual layered gate: prior shape coerced
|
|
254
|
+
* undefined into Uint16Array(0) and tripped MeshAsset.indices !==
|
|
255
|
+
* undefined branches downstream into drawIndexed(0).
|
|
256
|
+
* U16 (incl. widened U8) or U32 — width preserved from the source accessor;
|
|
257
|
+
* bridge.ts narrows U32 to U16 when the merged maxIndex fits.
|
|
258
|
+
*/
|
|
259
|
+
readonly indices?: Uint16Array | Uint32Array;
|
|
260
|
+
readonly materialIndex: number | null;
|
|
261
|
+
/**
|
|
262
|
+
* Owning glTF mesh index in the original document (`gltf.meshes[meshIndex]`).
|
|
263
|
+
* After OOS-7 flattening, multiple GltfMeshIr entries may share the same
|
|
264
|
+
* meshIndex (one per primitive). Bridge layer uses this to filter material
|
|
265
|
+
* collection per node, fixing the verify-r1 charter-defect where multi
|
|
266
|
+
* glTF-mesh documents would silently misalign materials across nodes.
|
|
267
|
+
*/
|
|
268
|
+
readonly meshIndex: number;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface GltfTextureIr {
|
|
272
|
+
readonly sampler?: number;
|
|
273
|
+
readonly source: number;
|
|
274
|
+
readonly name?: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface GltfTextureTransformIr {
|
|
278
|
+
readonly offset?: readonly [number, number];
|
|
279
|
+
readonly rotation?: number;
|
|
280
|
+
readonly scale?: readonly [number, number];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface GltfTextureInfoIr {
|
|
284
|
+
readonly texture: number;
|
|
285
|
+
readonly sampler?: number;
|
|
286
|
+
readonly texCoord?: number;
|
|
287
|
+
readonly transform?: GltfTextureTransformIr;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface GltfNormalTextureInfoIr extends GltfTextureInfoIr {
|
|
291
|
+
readonly scale?: number;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export interface GltfOcclusionTextureInfoIr extends GltfTextureInfoIr {
|
|
295
|
+
readonly strength?: number;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface GltfImageIr {
|
|
299
|
+
readonly uri?: string;
|
|
300
|
+
readonly mimeType?: string;
|
|
301
|
+
readonly bufferView?: number;
|
|
302
|
+
readonly name?: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface GltfSamplerIr {
|
|
306
|
+
readonly magFilter?: number;
|
|
307
|
+
readonly minFilter?: number;
|
|
308
|
+
readonly wrapS: number;
|
|
309
|
+
readonly wrapT: number;
|
|
310
|
+
readonly name?: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface GltfMaterialIr {
|
|
314
|
+
readonly name?: string;
|
|
315
|
+
readonly baseColorFactor: readonly [number, number, number, number];
|
|
316
|
+
/** glTF emissiveFactor; omitted means the glTF default [0, 0, 0]. */
|
|
317
|
+
readonly emissiveFactor?: readonly [number, number, number];
|
|
318
|
+
readonly baseColorTexture?: GltfTextureInfoIr | number;
|
|
319
|
+
/** Texture binding for the emissive map. */
|
|
320
|
+
readonly emissiveTexture?: GltfTextureInfoIr | number;
|
|
321
|
+
readonly metallicFactor: number;
|
|
322
|
+
readonly roughnessFactor: number;
|
|
323
|
+
readonly metallicRoughnessTexture?: GltfTextureInfoIr | number;
|
|
324
|
+
readonly normalTexture?: GltfNormalTextureInfoIr | number;
|
|
325
|
+
readonly occlusionTexture?: GltfOcclusionTextureInfoIr | number;
|
|
326
|
+
/**
|
|
327
|
+
* glTF `alphaMode` (`'OPAQUE'` | `'MASK'` | `'BLEND'`). Absent -> `'OPAQUE'`
|
|
328
|
+
* (glTF spec default). Drives the bridge's transparent / alpha-test routing.
|
|
329
|
+
*/
|
|
330
|
+
readonly alphaMode?: 'OPAQUE' | 'MASK' | 'BLEND';
|
|
331
|
+
/** glTF `alphaCutoff` (MASK-mode threshold; glTF spec default 0.5). */
|
|
332
|
+
readonly alphaCutoff?: number;
|
|
333
|
+
/**
|
|
334
|
+
* glTF `doubleSided`. Absent -> false (the glTF spec default). The bridge
|
|
335
|
+
* maps this to a no-cull material pass so reverse-facing glass and cards
|
|
336
|
+
* remain visible without changing their authored winding.
|
|
337
|
+
*/
|
|
338
|
+
readonly doubleSided?: boolean;
|
|
339
|
+
/**
|
|
340
|
+
* baseColorTexture UV-set index (glTF `baseColorTexture.texCoord`). Absent
|
|
341
|
+
* -> 0. Selects which interleaved UV set the base-color sampler reads.
|
|
342
|
+
*/
|
|
343
|
+
readonly baseColorTexCoord?: number;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export interface NodeInstancingIr {
|
|
347
|
+
/** Number of instances. All TRS attribute accessors share this count. */
|
|
348
|
+
readonly count: number;
|
|
349
|
+
/** N column-major mat4 transforms packed into one Float32Array (length = N*16). */
|
|
350
|
+
readonly transforms: Float32Array;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export type GltfPunctualLightType = 'directional' | 'point' | 'spot';
|
|
354
|
+
|
|
355
|
+
export interface GltfPunctualLightIr {
|
|
356
|
+
readonly type: GltfPunctualLightType;
|
|
357
|
+
readonly color: readonly [number, number, number];
|
|
358
|
+
readonly intensity: number;
|
|
359
|
+
readonly range?: number;
|
|
360
|
+
readonly spot?: {
|
|
361
|
+
readonly innerConeAngle: number;
|
|
362
|
+
readonly outerConeAngle: number;
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export interface GltfNodeIr {
|
|
367
|
+
readonly name?: string;
|
|
368
|
+
readonly transform: DecomposedTransform;
|
|
369
|
+
readonly meshIndex: number | null;
|
|
370
|
+
/** Node-authored morph weights override mesh defaults at instantiation. */
|
|
371
|
+
readonly morphWeights?: Float32Array;
|
|
372
|
+
/** Valid when node carries `skin` reference. Null when no skin. */
|
|
373
|
+
readonly skinIndex: number | null;
|
|
374
|
+
readonly children: readonly number[];
|
|
375
|
+
/** Present when node carries EXT_mesh_gpu_instancing (feat-20260518). */
|
|
376
|
+
readonly instancing?: NodeInstancingIr;
|
|
377
|
+
/** glTF camera index. Null when the node does not reference a camera. */
|
|
378
|
+
readonly camera: number | null;
|
|
379
|
+
/** KHR_lights_punctual light index. Null when the node has no light. */
|
|
380
|
+
readonly lightIndex?: number | null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export interface GltfSceneIr {
|
|
384
|
+
readonly name?: string;
|
|
385
|
+
readonly nodes: readonly number[];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export interface GltfDiagnosticsIr {
|
|
389
|
+
readonly nodeNames: readonly string[];
|
|
390
|
+
readonly unsupportedExtensions: readonly string[];
|
|
391
|
+
readonly matrixTrsCoexistNodes: readonly number[];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export interface GltfDoc {
|
|
395
|
+
readonly meshes: readonly GltfMeshIr[];
|
|
396
|
+
readonly materials: readonly GltfMaterialIr[];
|
|
397
|
+
readonly nodes: readonly GltfNodeIr[];
|
|
398
|
+
readonly scenes: readonly GltfSceneIr[];
|
|
399
|
+
readonly textures: readonly GltfTextureIr[] | undefined;
|
|
400
|
+
readonly images: readonly GltfImageIr[] | undefined;
|
|
401
|
+
readonly samplers: readonly GltfSamplerIr[] | undefined;
|
|
402
|
+
readonly skeletons: readonly GltfSkeletonRecord[];
|
|
403
|
+
readonly animationClips: readonly GltfAnimationClipRecord[];
|
|
404
|
+
readonly defaultSceneIndex: number;
|
|
405
|
+
readonly diagnostics: GltfDiagnosticsIr;
|
|
406
|
+
/** KHR_lights_punctual author facts, kept in source units for the bridge. */
|
|
407
|
+
readonly lights?: readonly GltfPunctualLightIr[];
|
|
408
|
+
/** Raw extension projection retained for bridge-only hand-constructed docs. */
|
|
409
|
+
readonly extensions?: {
|
|
410
|
+
readonly KHR_lights_punctual?: {
|
|
411
|
+
readonly lights?: readonly GltfPunctualLightIr[];
|
|
412
|
+
};
|
|
413
|
+
};
|
|
414
|
+
/**
|
|
415
|
+
* Original glTF-mesh-index -> number of primitives that mesh expanded into
|
|
416
|
+
* (feat-20260608 round-2). parseGltf flattens N glTF meshes with M_i
|
|
417
|
+
* primitives into sum(M_i) GltfMeshIr entries; this map preserves the M_i so
|
|
418
|
+
* downstream consumers (gltfDocToSceneAsset's `meshPrimitiveCount` param)
|
|
419
|
+
* can map a glTF mesh index back to the flat-GltfMeshIr offset range without
|
|
420
|
+
* re-parsing JSON.
|
|
421
|
+
*
|
|
422
|
+
* Optional for backwards compatibility with hand-constructed test
|
|
423
|
+
* fixtures; producers from parseGltf / parseGlb always populate it.
|
|
424
|
+
*/
|
|
425
|
+
readonly meshPrimitiveCount?: ReadonlyMap<number, number>;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
interface BuffersJson {
|
|
429
|
+
readonly byteLength: number;
|
|
430
|
+
readonly uri?: string;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
interface TextureTransformJson {
|
|
434
|
+
readonly offset?: readonly number[];
|
|
435
|
+
readonly rotation?: number;
|
|
436
|
+
readonly scale?: readonly number[];
|
|
437
|
+
readonly texCoord?: number;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
interface TextureInfoJson {
|
|
441
|
+
readonly index: number;
|
|
442
|
+
readonly texCoord?: number;
|
|
443
|
+
readonly extensions?: {
|
|
444
|
+
readonly KHR_texture_transform?: TextureTransformJson;
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
interface NormalTextureInfoJson extends TextureInfoJson {
|
|
449
|
+
readonly scale?: number;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
interface OcclusionTextureInfoJson extends TextureInfoJson {
|
|
453
|
+
readonly strength?: number;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
interface RootGltfJson extends GltfExtensionsJson {
|
|
457
|
+
readonly asset?: { readonly version?: string };
|
|
458
|
+
readonly scene?: number;
|
|
459
|
+
readonly scenes?: ReadonlyArray<{ readonly name?: string; readonly nodes?: readonly number[] }>;
|
|
460
|
+
readonly nodes?: ReadonlyArray<{
|
|
461
|
+
readonly name?: string;
|
|
462
|
+
readonly mesh?: number;
|
|
463
|
+
readonly skin?: number;
|
|
464
|
+
readonly camera?: number;
|
|
465
|
+
readonly children?: readonly number[];
|
|
466
|
+
readonly matrix?: readonly number[];
|
|
467
|
+
readonly translation?: readonly number[];
|
|
468
|
+
readonly rotation?: readonly number[];
|
|
469
|
+
readonly scale?: readonly number[];
|
|
470
|
+
readonly weights?: readonly number[];
|
|
471
|
+
readonly extensions?: {
|
|
472
|
+
readonly KHR_lights_punctual?: {
|
|
473
|
+
readonly light?: number;
|
|
474
|
+
};
|
|
475
|
+
readonly EXT_mesh_gpu_instancing?: {
|
|
476
|
+
readonly attributes?: {
|
|
477
|
+
readonly TRANSLATION?: number;
|
|
478
|
+
readonly ROTATION?: number;
|
|
479
|
+
readonly SCALE?: number;
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
}>;
|
|
484
|
+
readonly extensions?: {
|
|
485
|
+
readonly KHR_lights_punctual?: {
|
|
486
|
+
readonly lights?: ReadonlyArray<{
|
|
487
|
+
readonly type?: string;
|
|
488
|
+
readonly color?: readonly number[];
|
|
489
|
+
readonly intensity?: number;
|
|
490
|
+
readonly range?: number;
|
|
491
|
+
readonly spot?: {
|
|
492
|
+
readonly innerConeAngle?: number;
|
|
493
|
+
readonly outerConeAngle?: number;
|
|
494
|
+
};
|
|
495
|
+
}>;
|
|
496
|
+
};
|
|
497
|
+
};
|
|
498
|
+
readonly skins?: ReadonlyArray<{
|
|
499
|
+
readonly name?: string;
|
|
500
|
+
readonly joints: readonly number[];
|
|
501
|
+
readonly inverseBindMatrices?: number;
|
|
502
|
+
}>;
|
|
503
|
+
readonly meshes?: readonly MeshJson[];
|
|
504
|
+
readonly materials?: ReadonlyArray<{
|
|
505
|
+
readonly name?: string;
|
|
506
|
+
readonly pbrMetallicRoughness?: {
|
|
507
|
+
readonly baseColorFactor?: readonly number[];
|
|
508
|
+
readonly baseColorTexture?: TextureInfoJson;
|
|
509
|
+
readonly metallicFactor?: number;
|
|
510
|
+
readonly roughnessFactor?: number;
|
|
511
|
+
readonly metallicRoughnessTexture?: TextureInfoJson;
|
|
512
|
+
};
|
|
513
|
+
readonly normalTexture?: NormalTextureInfoJson;
|
|
514
|
+
readonly emissiveFactor?: readonly number[];
|
|
515
|
+
readonly emissiveTexture?: TextureInfoJson;
|
|
516
|
+
readonly occlusionTexture?: OcclusionTextureInfoJson;
|
|
517
|
+
readonly alphaMode?: string;
|
|
518
|
+
readonly alphaCutoff?: number;
|
|
519
|
+
readonly doubleSided?: boolean;
|
|
520
|
+
}>;
|
|
521
|
+
readonly textures?: ReadonlyArray<{
|
|
522
|
+
readonly sampler?: number;
|
|
523
|
+
readonly source?: number;
|
|
524
|
+
readonly name?: string;
|
|
525
|
+
}>;
|
|
526
|
+
readonly images?: ReadonlyArray<{
|
|
527
|
+
readonly uri?: string;
|
|
528
|
+
readonly mimeType?: string;
|
|
529
|
+
readonly bufferView?: number;
|
|
530
|
+
readonly name?: string;
|
|
531
|
+
}>;
|
|
532
|
+
readonly samplers?: ReadonlyArray<{
|
|
533
|
+
readonly magFilter?: number;
|
|
534
|
+
readonly minFilter?: number;
|
|
535
|
+
readonly wrapS?: number;
|
|
536
|
+
readonly wrapT?: number;
|
|
537
|
+
readonly name?: string;
|
|
538
|
+
}>;
|
|
539
|
+
readonly accessors?: readonly AccessorJson[];
|
|
540
|
+
readonly bufferViews?: readonly BufferViewJson[];
|
|
541
|
+
readonly buffers?: readonly BuffersJson[];
|
|
542
|
+
readonly animations?: ReadonlyArray<{
|
|
543
|
+
readonly name?: string;
|
|
544
|
+
readonly channels: ReadonlyArray<{
|
|
545
|
+
readonly sampler: number;
|
|
546
|
+
readonly target: {
|
|
547
|
+
readonly node?: number;
|
|
548
|
+
readonly path: string;
|
|
549
|
+
};
|
|
550
|
+
}>;
|
|
551
|
+
readonly samplers: ReadonlyArray<{
|
|
552
|
+
readonly input: number;
|
|
553
|
+
readonly output: number;
|
|
554
|
+
readonly interpolation?: string;
|
|
555
|
+
}>;
|
|
556
|
+
}>;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function tuple2(values: readonly number[] | undefined): readonly [number, number] | undefined {
|
|
560
|
+
if (values === undefined || values.length < 2) return undefined;
|
|
561
|
+
return [values[0] ?? 0, values[1] ?? 0];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function parseTextureInfo(
|
|
565
|
+
info: TextureInfoJson,
|
|
566
|
+
textures: readonly GltfTextureIr[],
|
|
567
|
+
): GltfTextureInfoIr {
|
|
568
|
+
const transformJson = info.extensions?.KHR_texture_transform;
|
|
569
|
+
const offset = tuple2(transformJson?.offset);
|
|
570
|
+
const scale = tuple2(transformJson?.scale);
|
|
571
|
+
const transform =
|
|
572
|
+
offset === undefined && transformJson?.rotation === undefined && scale === undefined
|
|
573
|
+
? undefined
|
|
574
|
+
: {
|
|
575
|
+
...(offset === undefined ? {} : { offset }),
|
|
576
|
+
...(transformJson?.rotation === undefined ? {} : { rotation: transformJson.rotation }),
|
|
577
|
+
...(scale === undefined ? {} : { scale }),
|
|
578
|
+
};
|
|
579
|
+
const sampler = textures[info.index]?.sampler;
|
|
580
|
+
return {
|
|
581
|
+
texture: info.index,
|
|
582
|
+
...(sampler === undefined ? {} : { sampler }),
|
|
583
|
+
...(info.texCoord === undefined && transformJson?.texCoord === undefined
|
|
584
|
+
? {}
|
|
585
|
+
: { texCoord: transformJson?.texCoord ?? info.texCoord }),
|
|
586
|
+
...(transform === undefined ? {} : { transform }),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export type ExternalLoader = (uri: string) => Promise<ArrayBuffer>;
|
|
591
|
+
|
|
592
|
+
async function resolveBuffer(
|
|
593
|
+
buf: BuffersJson,
|
|
594
|
+
externalLoader: ExternalLoader,
|
|
595
|
+
binChunk: Uint8Array | undefined,
|
|
596
|
+
): Promise<Uint8Array> {
|
|
597
|
+
if (buf.uri === undefined) {
|
|
598
|
+
// GLB BIN chunk slot.
|
|
599
|
+
if (binChunk === undefined) {
|
|
600
|
+
throw new Error('parseGltf: buffer 0 has no uri and no GLB BIN chunk available');
|
|
601
|
+
}
|
|
602
|
+
return binChunk;
|
|
603
|
+
}
|
|
604
|
+
const dataPayload = dataUriBase64Payload(buf.uri);
|
|
605
|
+
if (dataPayload !== undefined) {
|
|
606
|
+
return decodeBase64(dataPayload);
|
|
607
|
+
}
|
|
608
|
+
const arrayBuffer = await externalLoader(buf.uri);
|
|
609
|
+
return new Uint8Array(arrayBuffer);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
interface ParseGltfInternalsContext {
|
|
613
|
+
readonly externalLoader: ExternalLoader;
|
|
614
|
+
readonly binChunk?: Uint8Array;
|
|
615
|
+
readonly filePath: string;
|
|
616
|
+
readonly meshopt?: GltfBufferViewDecodeCapability;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
type GltfParseError = GltfError | ImportError;
|
|
620
|
+
|
|
621
|
+
function invalidBufferDataUriError(
|
|
622
|
+
filePath: string,
|
|
623
|
+
bufferIndex: number,
|
|
624
|
+
cause: Base64DecodeError,
|
|
625
|
+
): ImportError {
|
|
626
|
+
return new ImportError({
|
|
627
|
+
code: 'source-validation-failed',
|
|
628
|
+
expected: `glTF buffer ${bufferIndex} data URI to contain a valid base64 payload`,
|
|
629
|
+
hint: IMPORT_ERROR_HINTS['source-validation-failed'],
|
|
630
|
+
detail: {
|
|
631
|
+
diagnostics: [
|
|
632
|
+
{
|
|
633
|
+
code: 'gltf-buffer-data-uri-invalid',
|
|
634
|
+
severity: 'error',
|
|
635
|
+
sourcePath: filePath,
|
|
636
|
+
sourceRange: { start: 0, end: 0, line: 1, column: 1 },
|
|
637
|
+
rule: 'gltf-buffer-data-uri-base64',
|
|
638
|
+
expected: 'a valid base64 payload after ;base64,',
|
|
639
|
+
actual: `buffer ${bufferIndex}: ${cause.message}`,
|
|
640
|
+
hint: 'repair the buffer data URI or provide a valid external .bin sibling',
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export interface GltfParseOptions {
|
|
648
|
+
readonly meshopt?: GltfBufferViewDecodeCapability;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function parsePunctualLights(
|
|
652
|
+
json: RootGltfJson,
|
|
653
|
+
filePath: string,
|
|
654
|
+
): Result<readonly GltfPunctualLightIr[], GltfError> {
|
|
655
|
+
const source = json.extensions?.KHR_lights_punctual?.lights ?? [];
|
|
656
|
+
const lights: GltfPunctualLightIr[] = [];
|
|
657
|
+
for (const light of source) {
|
|
658
|
+
const type = light?.type;
|
|
659
|
+
if (type !== 'directional' && type !== 'point' && type !== 'spot') {
|
|
660
|
+
return err(gltfErr('gltf-malformed-header', { filePath, byteOffset: 0 }));
|
|
661
|
+
}
|
|
662
|
+
const color = light.color ?? [1, 1, 1];
|
|
663
|
+
if (
|
|
664
|
+
color.length < 3 ||
|
|
665
|
+
color
|
|
666
|
+
.slice(0, 3)
|
|
667
|
+
.some(
|
|
668
|
+
(value) => typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1,
|
|
669
|
+
)
|
|
670
|
+
) {
|
|
671
|
+
return err(gltfErr('gltf-malformed-header', { filePath, byteOffset: 0 }));
|
|
672
|
+
}
|
|
673
|
+
const intensity = light.intensity ?? 1;
|
|
674
|
+
if (typeof intensity !== 'number' || !Number.isFinite(intensity) || intensity < 0) {
|
|
675
|
+
return err(gltfErr('gltf-malformed-header', { filePath, byteOffset: 0 }));
|
|
676
|
+
}
|
|
677
|
+
const range = light.range;
|
|
678
|
+
if (
|
|
679
|
+
range !== undefined &&
|
|
680
|
+
(typeof range !== 'number' || !Number.isFinite(range) || range < 0)
|
|
681
|
+
) {
|
|
682
|
+
return err(gltfErr('gltf-malformed-header', { filePath, byteOffset: 0 }));
|
|
683
|
+
}
|
|
684
|
+
let spot: GltfPunctualLightIr['spot'];
|
|
685
|
+
if (type === 'spot') {
|
|
686
|
+
const innerConeAngle = light.spot?.innerConeAngle ?? 0;
|
|
687
|
+
const outerConeAngle = light.spot?.outerConeAngle ?? Math.PI / 4;
|
|
688
|
+
if (
|
|
689
|
+
!Number.isFinite(innerConeAngle) ||
|
|
690
|
+
!Number.isFinite(outerConeAngle) ||
|
|
691
|
+
innerConeAngle < 0 ||
|
|
692
|
+
innerConeAngle >= outerConeAngle ||
|
|
693
|
+
outerConeAngle > Math.PI / 2
|
|
694
|
+
) {
|
|
695
|
+
return err(gltfErr('gltf-malformed-header', { filePath, byteOffset: 0 }));
|
|
696
|
+
}
|
|
697
|
+
spot = { innerConeAngle, outerConeAngle };
|
|
698
|
+
}
|
|
699
|
+
lights.push({
|
|
700
|
+
type,
|
|
701
|
+
color: [color[0] ?? 1, color[1] ?? 1, color[2] ?? 1],
|
|
702
|
+
intensity,
|
|
703
|
+
...(range === undefined || range === 0 ? {} : { range }),
|
|
704
|
+
...(spot === undefined ? {} : { spot }),
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
return ok(lights);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function parseGltfWithBin(
|
|
711
|
+
json: RootGltfJson,
|
|
712
|
+
ctx: ParseGltfInternalsContext,
|
|
713
|
+
): Promise<Result<GltfDoc, GltfParseError>> {
|
|
714
|
+
const headerResult = parseGltfHeader(json, ctx.filePath);
|
|
715
|
+
if (!headerResult.ok) return err(headerResult.error);
|
|
716
|
+
|
|
717
|
+
const extResult = checkExtensions(json);
|
|
718
|
+
if (!extResult.ok) return err(extResult.error);
|
|
719
|
+
const unsupportedExtensions = extResult.value.unsupportedUsed;
|
|
720
|
+
const lightsResult = parsePunctualLights(json, ctx.filePath);
|
|
721
|
+
if (!lightsResult.ok) return err(lightsResult.error);
|
|
722
|
+
|
|
723
|
+
const meshesJson = json.meshes ?? [];
|
|
724
|
+
|
|
725
|
+
// Resolve buffers (data: URI / external / GLB BIN chunk).
|
|
726
|
+
const buffersJson = json.buffers ?? [];
|
|
727
|
+
const buffers: Uint8Array[] = [];
|
|
728
|
+
for (let i = 0; i < buffersJson.length; i++) {
|
|
729
|
+
const bufJson = buffersJson[i];
|
|
730
|
+
if (bufJson === undefined) continue;
|
|
731
|
+
try {
|
|
732
|
+
const bytes = await resolveBuffer(bufJson, ctx.externalLoader, ctx.binChunk);
|
|
733
|
+
buffers.push(bytes);
|
|
734
|
+
} catch (e) {
|
|
735
|
+
if (e instanceof Base64DecodeError) {
|
|
736
|
+
return err(invalidBufferDataUriError(ctx.filePath, i, e));
|
|
737
|
+
}
|
|
738
|
+
return err(
|
|
739
|
+
gltfErr('gltf-malformed-header', {
|
|
740
|
+
filePath: ctx.filePath,
|
|
741
|
+
byteOffset: 0,
|
|
742
|
+
}),
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const accessors = json.accessors ?? [];
|
|
748
|
+
const rawBufferViews = json.bufferViews ?? [];
|
|
749
|
+
const projected = await projectMeshoptBufferViews(
|
|
750
|
+
rawBufferViews as readonly MeshoptBufferViewJson[],
|
|
751
|
+
buffers,
|
|
752
|
+
json.extensionsRequired ?? [],
|
|
753
|
+
ctx.meshopt,
|
|
754
|
+
);
|
|
755
|
+
if (!projected.ok) return err(projected.error);
|
|
756
|
+
const bufferViews = projected.value.bufferViews;
|
|
757
|
+
buffers.splice(0, buffers.length, ...projected.value.buffers);
|
|
758
|
+
|
|
759
|
+
const meshes: GltfMeshIr[] = [];
|
|
760
|
+
// feat-20260608 round-2: surface the original-glTF-mesh primitive counts so
|
|
761
|
+
// build-time consumers (gltfImporter scene arm) can hand them to
|
|
762
|
+
// gltfDocToSceneAsset's `meshPrimitiveCount` parameter without re-parsing
|
|
763
|
+
// the JSON. Without this, importGltf at gltf-importer.ts ~272 walks the
|
|
764
|
+
// already-flattened doc.meshes (one entry per primitive) and cannot
|
|
765
|
+
// reconstruct the gltfMesh -> primitiveCount map; the bridge then uses
|
|
766
|
+
// identity (gltfMeshIdx == flatIdx) and any multi-primitive .gltf imports
|
|
767
|
+
// misindex meshes after the first primitive.
|
|
768
|
+
const meshPrimitiveCount = new Map<number, number>();
|
|
769
|
+
for (let meshIndex = 0; meshIndex < meshesJson.length; meshIndex++) {
|
|
770
|
+
const meshJson = meshesJson[meshIndex];
|
|
771
|
+
if (meshJson === undefined) continue;
|
|
772
|
+
meshPrimitiveCount.set(meshIndex, meshJson.primitives.length);
|
|
773
|
+
for (const prim of meshJson.primitives) {
|
|
774
|
+
if (prim === undefined) continue;
|
|
775
|
+
const positionAccessorIndex = prim.attributes?.POSITION;
|
|
776
|
+
if (positionAccessorIndex === undefined) {
|
|
777
|
+
return err(
|
|
778
|
+
gltfErr('gltf-accessor-type-mismatch', {
|
|
779
|
+
accessorIndex: -1,
|
|
780
|
+
reason: 'unknownComponentType',
|
|
781
|
+
}),
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
const positionAccessor = accessors[positionAccessorIndex];
|
|
785
|
+
const positionBufferView = bufferViews[positionAccessor?.bufferView ?? -1];
|
|
786
|
+
if (positionAccessor === undefined || positionBufferView === undefined) {
|
|
787
|
+
return err(
|
|
788
|
+
gltfErr('gltf-buffer-out-of-bounds', {
|
|
789
|
+
accessor: positionAccessorIndex,
|
|
790
|
+
byteOffset: 0,
|
|
791
|
+
byteLength: 0,
|
|
792
|
+
bufferIndex: positionBufferView?.buffer ?? 0,
|
|
793
|
+
}),
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
const positionBuffer = buffers[positionBufferView.buffer];
|
|
797
|
+
if (positionBuffer === undefined) {
|
|
798
|
+
return err(
|
|
799
|
+
gltfErr('gltf-buffer-out-of-bounds', {
|
|
800
|
+
accessor: positionAccessorIndex,
|
|
801
|
+
byteOffset: positionBufferView.byteOffset ?? 0,
|
|
802
|
+
byteLength: positionBufferView.byteLength,
|
|
803
|
+
bufferIndex: positionBufferView.buffer,
|
|
804
|
+
}),
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const positionDecoded = decodeAccessor({
|
|
808
|
+
accessorIndex: positionAccessorIndex,
|
|
809
|
+
accessor: positionAccessor,
|
|
810
|
+
bufferView: positionBufferView,
|
|
811
|
+
buffer: positionBuffer,
|
|
812
|
+
role: 'attribute',
|
|
813
|
+
});
|
|
814
|
+
if (!positionDecoded.ok) return err(positionDecoded.error);
|
|
815
|
+
if (positionDecoded.value.kind !== 'f32') {
|
|
816
|
+
return err(
|
|
817
|
+
gltfErr('gltf-accessor-type-mismatch', {
|
|
818
|
+
accessorIndex: positionAccessorIndex,
|
|
819
|
+
reason: 'unknownComponentType',
|
|
820
|
+
}),
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const positionsDecoded = positionDecoded.value.data;
|
|
824
|
+
// Re-allocate over a fresh ArrayBuffer so the GltfMeshIr.positions type
|
|
825
|
+
// (`Float32Array<ArrayBuffer>`) holds without ArrayBufferLike leakage.
|
|
826
|
+
const positions = new Float32Array(positionsDecoded.length);
|
|
827
|
+
positions.set(positionsDecoded);
|
|
828
|
+
|
|
829
|
+
// Decode optional vertex attributes: NORMAL (VEC3), TEXCOORD_0 (VEC2),
|
|
830
|
+
// TANGENT (VEC4). Each uses the existing decodeAttributeAccessor helper.
|
|
831
|
+
// Missing attributes leave the GltfMeshIr field undefined.
|
|
832
|
+
const attrs = prim.attributes ?? {};
|
|
833
|
+
|
|
834
|
+
let colors0: Float32Array | undefined;
|
|
835
|
+
const colorIdx = attrs.COLOR_0;
|
|
836
|
+
if (colorIdx !== undefined) {
|
|
837
|
+
const colorAccessor = accessors[colorIdx];
|
|
838
|
+
const colorBufferView = bufferViews[colorAccessor?.bufferView ?? -1];
|
|
839
|
+
if (colorAccessor === undefined || colorBufferView === undefined) {
|
|
840
|
+
return err(
|
|
841
|
+
gltfErr('gltf-color-accessor-malformed', {
|
|
842
|
+
semantic: 'COLOR_0',
|
|
843
|
+
accessorIndex: colorIdx,
|
|
844
|
+
reason: 'reference',
|
|
845
|
+
}),
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
const colorBuffer = buffers[colorBufferView.buffer];
|
|
849
|
+
if (colorBuffer === undefined) {
|
|
850
|
+
return err(
|
|
851
|
+
gltfErr('gltf-color-accessor-malformed', {
|
|
852
|
+
semantic: 'COLOR_0',
|
|
853
|
+
accessorIndex: colorIdx,
|
|
854
|
+
reason: 'reference',
|
|
855
|
+
}),
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
const decoded = decodeColorAccessor({
|
|
859
|
+
accessorIndex: colorIdx,
|
|
860
|
+
accessor: colorAccessor,
|
|
861
|
+
bufferView: colorBufferView,
|
|
862
|
+
buffer: colorBuffer,
|
|
863
|
+
bufferIndex: colorBufferView.buffer,
|
|
864
|
+
semantic: 'COLOR_0',
|
|
865
|
+
});
|
|
866
|
+
if (!decoded.ok) return err(decoded.error);
|
|
867
|
+
if (decoded.value.length !== (positions.length / 3) * 4) {
|
|
868
|
+
return err(
|
|
869
|
+
gltfErr('gltf-color-accessor-malformed', {
|
|
870
|
+
semantic: 'COLOR_0',
|
|
871
|
+
accessorIndex: colorIdx,
|
|
872
|
+
reason: 'count',
|
|
873
|
+
}),
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
colors0 = decoded.value;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
let normals: Float32Array | undefined;
|
|
880
|
+
const normalIdx = attrs.NORMAL;
|
|
881
|
+
if (normalIdx !== undefined) {
|
|
882
|
+
const acc = accessors[normalIdx];
|
|
883
|
+
if (acc !== undefined) {
|
|
884
|
+
const decoded = decodeAttributeAccessor(normalIdx, acc, bufferViews, buffers);
|
|
885
|
+
if (decoded.ok) {
|
|
886
|
+
const src = decoded.value;
|
|
887
|
+
const owned = new Float32Array(src.length);
|
|
888
|
+
owned.set(src);
|
|
889
|
+
normals = owned;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
let texcoord0: Float32Array | undefined;
|
|
895
|
+
const texIdx = attrs.TEXCOORD_0;
|
|
896
|
+
if (texIdx !== undefined) {
|
|
897
|
+
const acc = accessors[texIdx];
|
|
898
|
+
if (acc !== undefined) {
|
|
899
|
+
const decoded = decodeAttributeAccessor(texIdx, acc, bufferViews, buffers);
|
|
900
|
+
if (decoded.ok) {
|
|
901
|
+
const src = decoded.value;
|
|
902
|
+
const owned = new Float32Array(src.length);
|
|
903
|
+
owned.set(src);
|
|
904
|
+
texcoord0 = owned;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// feat-20260629-multi-uv-set-support m1-w2: decode TEXCOORD_1..7
|
|
910
|
+
// using the same decodeAttributeAccessor→Float32Array pattern.
|
|
911
|
+
// Missing accessor → field stays undefined (no error, mirrors TEXCOORD_0).
|
|
912
|
+
// componentType non-FLOAT → decodeAttributeAccessor returns error already.
|
|
913
|
+
let texcoord1: Float32Array | undefined;
|
|
914
|
+
let texcoord2: Float32Array | undefined;
|
|
915
|
+
let texcoord3: Float32Array | undefined;
|
|
916
|
+
let texcoord4: Float32Array | undefined;
|
|
917
|
+
let texcoord5: Float32Array | undefined;
|
|
918
|
+
let texcoord6: Float32Array | undefined;
|
|
919
|
+
let texcoord7: Float32Array | undefined;
|
|
920
|
+
for (let k = 1; k <= 7; k++) {
|
|
921
|
+
const tcIdx = attrs[`TEXCOORD_${k}`];
|
|
922
|
+
if (tcIdx === undefined) continue;
|
|
923
|
+
const acc = accessors[tcIdx];
|
|
924
|
+
if (acc !== undefined) {
|
|
925
|
+
const decoded = decodeAttributeAccessor(tcIdx, acc, bufferViews, buffers);
|
|
926
|
+
if (decoded.ok) {
|
|
927
|
+
const src = decoded.value;
|
|
928
|
+
const owned = new Float32Array(src.length);
|
|
929
|
+
owned.set(src);
|
|
930
|
+
if (k === 1) texcoord1 = owned;
|
|
931
|
+
else if (k === 2) texcoord2 = owned;
|
|
932
|
+
else if (k === 3) texcoord3 = owned;
|
|
933
|
+
else if (k === 4) texcoord4 = owned;
|
|
934
|
+
else if (k === 5) texcoord5 = owned;
|
|
935
|
+
else if (k === 6) texcoord6 = owned;
|
|
936
|
+
else texcoord7 = owned;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
let tangents: Float32Array | undefined;
|
|
942
|
+
const tanIdx = attrs.TANGENT;
|
|
943
|
+
if (tanIdx !== undefined) {
|
|
944
|
+
const acc = accessors[tanIdx];
|
|
945
|
+
if (acc !== undefined) {
|
|
946
|
+
const decoded = decodeAttributeAccessor(tanIdx, acc, bufferViews, buffers);
|
|
947
|
+
if (decoded.ok) {
|
|
948
|
+
const src = decoded.value;
|
|
949
|
+
const owned = new Float32Array(src.length);
|
|
950
|
+
owned.set(src);
|
|
951
|
+
tangents = owned;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const rawTargets = prim.targets ?? [];
|
|
957
|
+
for (const target of rawTargets) {
|
|
958
|
+
const colorTarget = target.COLOR_0;
|
|
959
|
+
if (colorTarget !== undefined) {
|
|
960
|
+
return err(
|
|
961
|
+
gltfErr('gltf-color-accessor-unsupported', {
|
|
962
|
+
semantic: 'COLOR_0',
|
|
963
|
+
accessorIndex: colorTarget,
|
|
964
|
+
reason: 'morph',
|
|
965
|
+
}),
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const morphAttributeCount = rawTargets.reduce(
|
|
970
|
+
(count, target) =>
|
|
971
|
+
count +
|
|
972
|
+
(target.POSITION === undefined ? 0 : 1) +
|
|
973
|
+
(target.NORMAL === undefined ? 0 : 1) +
|
|
974
|
+
(target.TANGENT === undefined ? 0 : 1),
|
|
975
|
+
0,
|
|
976
|
+
);
|
|
977
|
+
const morphError = (
|
|
978
|
+
reason:
|
|
979
|
+
| 'target-count-exceeded'
|
|
980
|
+
| 'attribute-count-exceeded'
|
|
981
|
+
| 'attribute-length-mismatch'
|
|
982
|
+
| 'weights-length-mismatch'
|
|
983
|
+
| 'sparse-or-unsupported-accessor',
|
|
984
|
+
) =>
|
|
985
|
+
err(
|
|
986
|
+
gltfErr('gltf-morph-invalid', {
|
|
987
|
+
meshIndex,
|
|
988
|
+
primitiveIndex: meshJson.primitives.indexOf(prim),
|
|
989
|
+
reason,
|
|
990
|
+
targetCount: rawTargets.length,
|
|
991
|
+
attributeCount: morphAttributeCount,
|
|
992
|
+
vertexCount: positions.length / 3,
|
|
993
|
+
}),
|
|
994
|
+
);
|
|
995
|
+
if (rawTargets.length > 8) return morphError('target-count-exceeded');
|
|
996
|
+
if (morphAttributeCount > 8) return morphError('attribute-count-exceeded');
|
|
997
|
+
|
|
998
|
+
const morphTargets: {
|
|
999
|
+
readonly position?: Float32Array;
|
|
1000
|
+
readonly normal?: Float32Array;
|
|
1001
|
+
readonly tangent?: Float32Array;
|
|
1002
|
+
}[] = [];
|
|
1003
|
+
for (const target of rawTargets) {
|
|
1004
|
+
const output: {
|
|
1005
|
+
position?: Float32Array;
|
|
1006
|
+
normal?: Float32Array;
|
|
1007
|
+
tangent?: Float32Array;
|
|
1008
|
+
} = {};
|
|
1009
|
+
for (const [key, accessorIndex] of Object.entries(target)) {
|
|
1010
|
+
if (key !== 'POSITION' && key !== 'NORMAL' && key !== 'TANGENT') continue;
|
|
1011
|
+
const targetAccessor = accessors[accessorIndex];
|
|
1012
|
+
if (targetAccessor === undefined) return morphError('attribute-length-mismatch');
|
|
1013
|
+
const decoded = decodeAttributeAccessor(
|
|
1014
|
+
accessorIndex,
|
|
1015
|
+
targetAccessor,
|
|
1016
|
+
bufferViews,
|
|
1017
|
+
buffers,
|
|
1018
|
+
);
|
|
1019
|
+
if (!decoded.ok) {
|
|
1020
|
+
return morphError(
|
|
1021
|
+
decoded.error.code === 'gltf-accessor-type-mismatch'
|
|
1022
|
+
? 'sparse-or-unsupported-accessor'
|
|
1023
|
+
: 'attribute-length-mismatch',
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
const expected = (positions.length / 3) * (key === 'TANGENT' ? 4 : 3);
|
|
1027
|
+
if (decoded.value.length !== expected) return morphError('attribute-length-mismatch');
|
|
1028
|
+
const owned = new Float32Array(decoded.value.length);
|
|
1029
|
+
owned.set(decoded.value);
|
|
1030
|
+
if (key === 'POSITION') output.position = owned;
|
|
1031
|
+
else if (key === 'NORMAL') output.normal = owned;
|
|
1032
|
+
else output.tangent = owned;
|
|
1033
|
+
}
|
|
1034
|
+
morphTargets.push(output);
|
|
1035
|
+
}
|
|
1036
|
+
const meshWeights = meshJson.weights;
|
|
1037
|
+
if (meshWeights !== undefined && meshWeights.length !== rawTargets.length) {
|
|
1038
|
+
return morphError('weights-length-mismatch');
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// JOINTS_0 / WEIGHTS_0 (feat-20260611 M1 w1): paired skinning attributes.
|
|
1042
|
+
// glTF 2.0 spec section 3.7.2.1 requires the pair to appear together for
|
|
1043
|
+
// any skinned primitive; lone presence -> gltf-skin-attr-asymmetric.
|
|
1044
|
+
// JOINTS_0 componentType: UBYTE(5121) widen->U16 via decodeAccessor
|
|
1045
|
+
// role='joints' (D-3 width-convert at parse), or USHORT(5123) standard
|
|
1046
|
+
// U16 path. WEIGHTS_0 componentType: FLOAT(5126) standard F32 path.
|
|
1047
|
+
const jointsIdx = attrs.JOINTS_0;
|
|
1048
|
+
const weightsIdx = attrs.WEIGHTS_0;
|
|
1049
|
+
const hasJoints = jointsIdx !== undefined;
|
|
1050
|
+
const hasWeights = weightsIdx !== undefined;
|
|
1051
|
+
if (hasJoints !== hasWeights) {
|
|
1052
|
+
const primitiveIndex = meshJson.primitives.indexOf(prim);
|
|
1053
|
+
return err(
|
|
1054
|
+
gltfErr('gltf-skin-attr-asymmetric', {
|
|
1055
|
+
meshIndex,
|
|
1056
|
+
primitiveIndex,
|
|
1057
|
+
hasJoints,
|
|
1058
|
+
hasWeights,
|
|
1059
|
+
}),
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
let joints0: Uint16Array | undefined;
|
|
1063
|
+
let weights0: Float32Array | undefined;
|
|
1064
|
+
if (hasJoints && hasWeights) {
|
|
1065
|
+
const jointsAccessor = accessors[jointsIdx as number];
|
|
1066
|
+
const jointsBufferView = bufferViews[jointsAccessor?.bufferView ?? -1];
|
|
1067
|
+
if (jointsAccessor === undefined || jointsBufferView === undefined) {
|
|
1068
|
+
return err(unknownAccessor(jointsIdx as number));
|
|
1069
|
+
}
|
|
1070
|
+
const jointsBuffer = buffers[jointsBufferView.buffer];
|
|
1071
|
+
if (jointsBuffer === undefined) return err(unknownAccessor(jointsIdx as number));
|
|
1072
|
+
const jointsDecoded = decodeAccessor({
|
|
1073
|
+
accessorIndex: jointsIdx as number,
|
|
1074
|
+
accessor: jointsAccessor,
|
|
1075
|
+
bufferView: jointsBufferView,
|
|
1076
|
+
buffer: jointsBuffer,
|
|
1077
|
+
role: 'joints',
|
|
1078
|
+
});
|
|
1079
|
+
if (!jointsDecoded.ok) return err(jointsDecoded.error);
|
|
1080
|
+
if (jointsDecoded.value.kind !== 'u16') {
|
|
1081
|
+
return err(
|
|
1082
|
+
gltfErr('gltf-accessor-type-mismatch', {
|
|
1083
|
+
accessorIndex: jointsIdx as number,
|
|
1084
|
+
reason: 'unknownComponentType',
|
|
1085
|
+
}),
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
const src = jointsDecoded.value.data;
|
|
1089
|
+
const owned = new Uint16Array(src.length);
|
|
1090
|
+
owned.set(src);
|
|
1091
|
+
joints0 = owned;
|
|
1092
|
+
|
|
1093
|
+
const weightsAccessor = accessors[weightsIdx as number];
|
|
1094
|
+
const weightsBufferView = bufferViews[weightsAccessor?.bufferView ?? -1];
|
|
1095
|
+
if (weightsAccessor === undefined || weightsBufferView === undefined) {
|
|
1096
|
+
return err(unknownAccessor(weightsIdx as number));
|
|
1097
|
+
}
|
|
1098
|
+
const weightsBuffer = buffers[weightsBufferView.buffer];
|
|
1099
|
+
if (weightsBuffer === undefined) return err(unknownAccessor(weightsIdx as number));
|
|
1100
|
+
const weightsDecoded = decodeAccessor({
|
|
1101
|
+
accessorIndex: weightsIdx as number,
|
|
1102
|
+
accessor: weightsAccessor,
|
|
1103
|
+
bufferView: weightsBufferView,
|
|
1104
|
+
buffer: weightsBuffer,
|
|
1105
|
+
role: 'attribute',
|
|
1106
|
+
});
|
|
1107
|
+
if (!weightsDecoded.ok) return err(weightsDecoded.error);
|
|
1108
|
+
if (weightsDecoded.value.kind !== 'f32') {
|
|
1109
|
+
return err(
|
|
1110
|
+
gltfErr('gltf-accessor-type-mismatch', {
|
|
1111
|
+
accessorIndex: weightsIdx as number,
|
|
1112
|
+
reason: 'unknownComponentType',
|
|
1113
|
+
}),
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
const wsrc = weightsDecoded.value.data;
|
|
1117
|
+
const wowned = new Float32Array(wsrc.length);
|
|
1118
|
+
wowned.set(wsrc);
|
|
1119
|
+
weights0 = wowned;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// bug-20260612 hello-skin visual layered gate: glTF spec marks
|
|
1123
|
+
// primitive.indices optional; undefined means non-indexed geometry.
|
|
1124
|
+
// Leave indices undefined here; bridge.ts handles the non-indexed
|
|
1125
|
+
// path by routing MeshAsset.indices to undefined.
|
|
1126
|
+
let indices: Uint16Array | Uint32Array | undefined;
|
|
1127
|
+
if (prim.indices !== undefined) {
|
|
1128
|
+
const indexAccessor = accessors[prim.indices];
|
|
1129
|
+
const indexBufferView = bufferViews[indexAccessor?.bufferView ?? -1];
|
|
1130
|
+
if (indexAccessor === undefined || indexBufferView === undefined) {
|
|
1131
|
+
return err(
|
|
1132
|
+
gltfErr('gltf-buffer-out-of-bounds', {
|
|
1133
|
+
accessor: prim.indices,
|
|
1134
|
+
byteOffset: 0,
|
|
1135
|
+
byteLength: 0,
|
|
1136
|
+
bufferIndex: indexBufferView?.buffer ?? 0,
|
|
1137
|
+
}),
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
const indexBuffer = buffers[indexBufferView.buffer];
|
|
1141
|
+
if (indexBuffer === undefined) {
|
|
1142
|
+
return err(
|
|
1143
|
+
gltfErr('gltf-buffer-out-of-bounds', {
|
|
1144
|
+
accessor: prim.indices,
|
|
1145
|
+
byteOffset: indexBufferView.byteOffset ?? 0,
|
|
1146
|
+
byteLength: indexBufferView.byteLength,
|
|
1147
|
+
bufferIndex: indexBufferView.buffer,
|
|
1148
|
+
}),
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
const indexDecoded = decodeAccessor({
|
|
1152
|
+
accessorIndex: prim.indices,
|
|
1153
|
+
accessor: indexAccessor,
|
|
1154
|
+
bufferView: indexBufferView,
|
|
1155
|
+
buffer: indexBuffer,
|
|
1156
|
+
role: 'indices',
|
|
1157
|
+
});
|
|
1158
|
+
if (!indexDecoded.ok) return err(indexDecoded.error);
|
|
1159
|
+
if (indexDecoded.value.kind === 'u16') {
|
|
1160
|
+
// Re-allocate over a fresh ArrayBuffer so the GltfMeshIr.indices type
|
|
1161
|
+
// (`Uint16Array<ArrayBuffer>`) holds without the underlying
|
|
1162
|
+
// SharedArrayBuffer-friendly ArrayBufferLike slot leaking through.
|
|
1163
|
+
const src = indexDecoded.value.data;
|
|
1164
|
+
const owned = new Uint16Array(src.length);
|
|
1165
|
+
owned.set(src);
|
|
1166
|
+
indices = owned;
|
|
1167
|
+
} else if (indexDecoded.value.kind === 'u32') {
|
|
1168
|
+
// U32 indices ride through end-to-end: MeshAsset.indices is
|
|
1169
|
+
// `Uint16Array | Uint32Array`, mesh-bin serializes iwidth=4, and
|
|
1170
|
+
// the GPU runtime auto-selects 'uint32' via `instanceof Uint32Array`
|
|
1171
|
+
// (createRenderer.ts). bridge.ts merges by value, so small meshes
|
|
1172
|
+
// whose maxIndex < 65536 are losslessly narrowed to Uint16 there.
|
|
1173
|
+
// Re-allocate over a fresh ArrayBuffer for the same reason as u16.
|
|
1174
|
+
const src = indexDecoded.value.data;
|
|
1175
|
+
const owned = new Uint32Array(src.length);
|
|
1176
|
+
owned.set(src);
|
|
1177
|
+
indices = owned;
|
|
1178
|
+
} else {
|
|
1179
|
+
return err(
|
|
1180
|
+
gltfErr('gltf-accessor-type-mismatch', {
|
|
1181
|
+
accessorIndex: prim.indices,
|
|
1182
|
+
reason: 'unknownComponentType',
|
|
1183
|
+
}),
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const meshIr: GltfMeshIr = {
|
|
1189
|
+
...(meshJson.name === undefined ? {} : { name: meshJson.name }),
|
|
1190
|
+
positions,
|
|
1191
|
+
...(normals === undefined ? {} : { normals }),
|
|
1192
|
+
...(texcoord0 === undefined ? {} : { texcoord0 }),
|
|
1193
|
+
...(texcoord1 === undefined ? {} : { texcoord1 }),
|
|
1194
|
+
...(texcoord2 === undefined ? {} : { texcoord2 }),
|
|
1195
|
+
...(texcoord3 === undefined ? {} : { texcoord3 }),
|
|
1196
|
+
...(texcoord4 === undefined ? {} : { texcoord4 }),
|
|
1197
|
+
...(texcoord5 === undefined ? {} : { texcoord5 }),
|
|
1198
|
+
...(texcoord6 === undefined ? {} : { texcoord6 }),
|
|
1199
|
+
...(texcoord7 === undefined ? {} : { texcoord7 }),
|
|
1200
|
+
...(tangents === undefined ? {} : { tangents }),
|
|
1201
|
+
...(colors0 === undefined ? {} : { colors0 }),
|
|
1202
|
+
...(joints0 === undefined ? {} : { joints0 }),
|
|
1203
|
+
...(weights0 === undefined ? {} : { weights0 }),
|
|
1204
|
+
...(morphTargets.length === 0 ? {} : { morphTargets }),
|
|
1205
|
+
...(meshWeights === undefined ? {} : { morphWeights: new Float32Array(meshWeights) }),
|
|
1206
|
+
...(indices === undefined ? {} : { indices }),
|
|
1207
|
+
materialIndex: prim.material ?? null,
|
|
1208
|
+
meshIndex,
|
|
1209
|
+
};
|
|
1210
|
+
meshes.push(meshIr);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Textures, images, samplers (Tier-C: parse top-level arrays).
|
|
1215
|
+
const texturesJson = json.textures ?? [];
|
|
1216
|
+
const textures: GltfTextureIr[] = [];
|
|
1217
|
+
for (const texJson of texturesJson) {
|
|
1218
|
+
const texIr: GltfTextureIr = {
|
|
1219
|
+
source: texJson.source ?? -1,
|
|
1220
|
+
...(texJson.sampler === undefined ? {} : { sampler: texJson.sampler }),
|
|
1221
|
+
...(texJson.name === undefined ? {} : { name: texJson.name }),
|
|
1222
|
+
};
|
|
1223
|
+
textures.push(texIr);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const imagesJson = json.images ?? [];
|
|
1227
|
+
const images: GltfImageIr[] = [];
|
|
1228
|
+
for (const imgJson of imagesJson) {
|
|
1229
|
+
const mimeType = imgJson.mimeType;
|
|
1230
|
+
if (mimeType !== undefined && mimeType !== 'image/jpeg' && mimeType !== 'image/png') {
|
|
1231
|
+
return err(gltfErr('gltf-image-mime-unsupported', { mimeType }));
|
|
1232
|
+
}
|
|
1233
|
+
const imgIr: GltfImageIr = {
|
|
1234
|
+
...(imgJson.uri === undefined ? {} : { uri: imgJson.uri }),
|
|
1235
|
+
...(imgJson.mimeType === undefined ? {} : { mimeType: imgJson.mimeType }),
|
|
1236
|
+
...(imgJson.bufferView === undefined ? {} : { bufferView: imgJson.bufferView }),
|
|
1237
|
+
...(imgJson.name === undefined ? {} : { name: imgJson.name }),
|
|
1238
|
+
};
|
|
1239
|
+
images.push(imgIr);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Load external image URIs via externalLoader.
|
|
1243
|
+
for (const img of images) {
|
|
1244
|
+
if (img.uri !== undefined) {
|
|
1245
|
+
if (dataUriBase64Payload(img.uri) !== undefined) continue; // data: URI, skip external load
|
|
1246
|
+
try {
|
|
1247
|
+
await ctx.externalLoader(img.uri);
|
|
1248
|
+
} catch (_e) {
|
|
1249
|
+
return err(gltfErr('gltf-texture-load-failed', { uri: img.uri }));
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const samplersJson = json.samplers ?? [];
|
|
1255
|
+
const samplers: GltfSamplerIr[] = [];
|
|
1256
|
+
for (const sampJson of samplersJson) {
|
|
1257
|
+
samplers.push({
|
|
1258
|
+
...(sampJson.magFilter === undefined ? {} : { magFilter: sampJson.magFilter }),
|
|
1259
|
+
...(sampJson.minFilter === undefined ? {} : { minFilter: sampJson.minFilter }),
|
|
1260
|
+
wrapS: sampJson.wrapS ?? 10497, // REPEAT (glTF default)
|
|
1261
|
+
wrapT: sampJson.wrapT ?? 10497,
|
|
1262
|
+
...(sampJson.name === undefined ? {} : { name: sampJson.name }),
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Materials (Tier-C subset: metallic-roughness PBR + normal/emissive maps).
|
|
1267
|
+
const materialsJson = json.materials ?? [];
|
|
1268
|
+
const materials: GltfMaterialIr[] = [];
|
|
1269
|
+
for (const matJson of materialsJson) {
|
|
1270
|
+
const pbr = matJson.pbrMetallicRoughness;
|
|
1271
|
+
const baseColor = pbr?.baseColorFactor ?? [1, 1, 1, 1];
|
|
1272
|
+
const baseColor4: readonly [number, number, number, number] = [
|
|
1273
|
+
baseColor[0] ?? 1,
|
|
1274
|
+
baseColor[1] ?? 1,
|
|
1275
|
+
baseColor[2] ?? 1,
|
|
1276
|
+
baseColor[3] ?? 1,
|
|
1277
|
+
];
|
|
1278
|
+
|
|
1279
|
+
const alphaMode =
|
|
1280
|
+
matJson.alphaMode === 'MASK' || matJson.alphaMode === 'BLEND' ? matJson.alphaMode : undefined;
|
|
1281
|
+
const alphaCutoff = alphaMode === 'MASK' ? (matJson.alphaCutoff ?? 0.5) : undefined;
|
|
1282
|
+
const emissiveFactor = matJson.emissiveFactor;
|
|
1283
|
+
const emissiveFactor3: readonly [number, number, number] = [
|
|
1284
|
+
emissiveFactor?.[0] ?? 0,
|
|
1285
|
+
emissiveFactor?.[1] ?? 0,
|
|
1286
|
+
emissiveFactor?.[2] ?? 0,
|
|
1287
|
+
];
|
|
1288
|
+
materials.push({
|
|
1289
|
+
...(matJson.name === undefined ? {} : { name: matJson.name }),
|
|
1290
|
+
baseColorFactor: baseColor4,
|
|
1291
|
+
...(emissiveFactor === undefined ? {} : { emissiveFactor: emissiveFactor3 }),
|
|
1292
|
+
metallicFactor: pbr?.metallicFactor ?? 1.0,
|
|
1293
|
+
roughnessFactor: pbr?.roughnessFactor ?? 1.0,
|
|
1294
|
+
...(pbr?.baseColorTexture === undefined
|
|
1295
|
+
? {}
|
|
1296
|
+
: { baseColorTexture: parseTextureInfo(pbr.baseColorTexture, textures) }),
|
|
1297
|
+
...(pbr?.metallicRoughnessTexture === undefined
|
|
1298
|
+
? {}
|
|
1299
|
+
: { metallicRoughnessTexture: parseTextureInfo(pbr.metallicRoughnessTexture, textures) }),
|
|
1300
|
+
...(matJson.normalTexture === undefined
|
|
1301
|
+
? {}
|
|
1302
|
+
: {
|
|
1303
|
+
normalTexture: {
|
|
1304
|
+
...parseTextureInfo(matJson.normalTexture, textures),
|
|
1305
|
+
...(matJson.normalTexture.scale === undefined
|
|
1306
|
+
? {}
|
|
1307
|
+
: { scale: matJson.normalTexture.scale }),
|
|
1308
|
+
},
|
|
1309
|
+
}),
|
|
1310
|
+
...(matJson.occlusionTexture === undefined
|
|
1311
|
+
? {}
|
|
1312
|
+
: {
|
|
1313
|
+
occlusionTexture: {
|
|
1314
|
+
...parseTextureInfo(matJson.occlusionTexture, textures),
|
|
1315
|
+
...(matJson.occlusionTexture.strength === undefined
|
|
1316
|
+
? {}
|
|
1317
|
+
: { strength: matJson.occlusionTexture.strength }),
|
|
1318
|
+
},
|
|
1319
|
+
}),
|
|
1320
|
+
...(matJson.emissiveTexture === undefined
|
|
1321
|
+
? {}
|
|
1322
|
+
: { emissiveTexture: parseTextureInfo(matJson.emissiveTexture, textures) }),
|
|
1323
|
+
...(alphaMode === undefined ? {} : { alphaMode }),
|
|
1324
|
+
...(alphaCutoff === undefined ? {} : { alphaCutoff }),
|
|
1325
|
+
...(matJson.doubleSided === true ? { doubleSided: true } : {}),
|
|
1326
|
+
...(pbr?.baseColorTexture?.texCoord === undefined || pbr.baseColorTexture.texCoord === 0
|
|
1327
|
+
? {}
|
|
1328
|
+
: { baseColorTexCoord: pbr.baseColorTexture.texCoord }),
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Nodes + diagnostics.
|
|
1333
|
+
const diagnostics = {
|
|
1334
|
+
nodeNames: [] as string[],
|
|
1335
|
+
unsupportedExtensions: [...unsupportedExtensions] as string[],
|
|
1336
|
+
matrixTrsCoexistNodes: [] as number[],
|
|
1337
|
+
};
|
|
1338
|
+
const nodes: GltfNodeIr[] = [];
|
|
1339
|
+
const nodesJson = json.nodes ?? [];
|
|
1340
|
+
for (let nodeIndex = 0; nodeIndex < nodesJson.length; nodeIndex++) {
|
|
1341
|
+
const nodeJson = nodesJson[nodeIndex];
|
|
1342
|
+
if (nodeJson === undefined) continue;
|
|
1343
|
+
const transform = decomposeNodeTransform(nodeJson, nodeIndex, diagnostics);
|
|
1344
|
+
if (nodeJson.name !== undefined) diagnostics.nodeNames.push(nodeJson.name);
|
|
1345
|
+
const instancingExt = nodeJson.extensions?.EXT_mesh_gpu_instancing;
|
|
1346
|
+
let instancing: NodeInstancingIr | undefined;
|
|
1347
|
+
if (instancingExt !== undefined) {
|
|
1348
|
+
const instancingResult = decodeNodeInstancing(
|
|
1349
|
+
nodeIndex,
|
|
1350
|
+
instancingExt.attributes ?? {},
|
|
1351
|
+
accessors,
|
|
1352
|
+
bufferViews,
|
|
1353
|
+
buffers,
|
|
1354
|
+
);
|
|
1355
|
+
if (!instancingResult.ok) return err(instancingResult.error);
|
|
1356
|
+
instancing = instancingResult.value;
|
|
1357
|
+
}
|
|
1358
|
+
const nodeMorphWeights =
|
|
1359
|
+
nodeJson.weights === undefined ? undefined : new Float32Array(nodeJson.weights);
|
|
1360
|
+
nodes.push({
|
|
1361
|
+
...(nodeJson.name === undefined ? {} : { name: nodeJson.name }),
|
|
1362
|
+
transform,
|
|
1363
|
+
meshIndex: nodeJson.mesh ?? null,
|
|
1364
|
+
skinIndex: nodeJson.skin ?? null,
|
|
1365
|
+
children: nodeJson.children ?? [],
|
|
1366
|
+
camera: nodeJson.camera ?? null,
|
|
1367
|
+
lightIndex: nodeJson.extensions?.KHR_lights_punctual?.light ?? null,
|
|
1368
|
+
...(instancing === undefined ? {} : { instancing }),
|
|
1369
|
+
...(nodeMorphWeights === undefined ? {} : { morphWeights: nodeMorphWeights }),
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// Scenes.
|
|
1374
|
+
const scenesJson = json.scenes ?? [];
|
|
1375
|
+
const scenes: GltfSceneIr[] = scenesJson.map((s) => ({
|
|
1376
|
+
...(s.name === undefined ? {} : { name: s.name }),
|
|
1377
|
+
nodes: s.nodes ?? [],
|
|
1378
|
+
}));
|
|
1379
|
+
const defaultSceneIndex = json.scene ?? 0;
|
|
1380
|
+
|
|
1381
|
+
// Skins (feat-20260523-skin-skeleton-animation M0).
|
|
1382
|
+
const skinsJson = json.skins;
|
|
1383
|
+
const skinResult = parseSkin(skinsJson, nodesJson, accessors, bufferViews, buffers);
|
|
1384
|
+
if (!skinResult.ok) return err(skinResult.error);
|
|
1385
|
+
const skeletons = skinResult.value;
|
|
1386
|
+
|
|
1387
|
+
// Animations (feat-20260523-skin-skeleton-animation M0).
|
|
1388
|
+
const animationsJson = json.animations;
|
|
1389
|
+
const animResult = parseAnimation(animationsJson, nodesJson, accessors, bufferViews, buffers);
|
|
1390
|
+
if (!animResult.ok) return err(animResult.error);
|
|
1391
|
+
const animationClips = animResult.value;
|
|
1392
|
+
|
|
1393
|
+
return ok({
|
|
1394
|
+
meshes,
|
|
1395
|
+
materials,
|
|
1396
|
+
nodes,
|
|
1397
|
+
scenes,
|
|
1398
|
+
skeletons,
|
|
1399
|
+
animationClips,
|
|
1400
|
+
textures: textures.length > 0 ? textures : undefined,
|
|
1401
|
+
images: images.length > 0 ? images : undefined,
|
|
1402
|
+
samplers: samplers.length > 0 ? samplers : undefined,
|
|
1403
|
+
defaultSceneIndex,
|
|
1404
|
+
diagnostics: {
|
|
1405
|
+
nodeNames: diagnostics.nodeNames,
|
|
1406
|
+
unsupportedExtensions: diagnostics.unsupportedExtensions,
|
|
1407
|
+
matrixTrsCoexistNodes: diagnostics.matrixTrsCoexistNodes,
|
|
1408
|
+
},
|
|
1409
|
+
meshPrimitiveCount,
|
|
1410
|
+
lights: lightsResult.value,
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Parse a glTF 2.0 JSON document. The `externalLoader` resolves
|
|
1416
|
+
* `buffers[].uri` references that are NOT data: URIs (callers in Node
|
|
1417
|
+
* land typically wrap fs.readFile; browser callers wrap fetch).
|
|
1418
|
+
*
|
|
1419
|
+
* Pure function modulo `externalLoader` (caller-provided I/O is the
|
|
1420
|
+
* only side effect). No global state, no fs / network direct call.
|
|
1421
|
+
*/
|
|
1422
|
+
export async function parseGltfForImporter(
|
|
1423
|
+
json: unknown,
|
|
1424
|
+
externalLoader: ExternalLoader,
|
|
1425
|
+
filePath: string,
|
|
1426
|
+
options: GltfParseOptions = {},
|
|
1427
|
+
): Promise<Result<GltfDoc, GltfParseError>> {
|
|
1428
|
+
if (json === null || typeof json !== 'object') {
|
|
1429
|
+
return err(
|
|
1430
|
+
gltfErr('gltf-malformed-header', {
|
|
1431
|
+
filePath,
|
|
1432
|
+
byteOffset: 0,
|
|
1433
|
+
}),
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
return parseGltfWithBin(json as RootGltfJson, {
|
|
1437
|
+
externalLoader,
|
|
1438
|
+
filePath,
|
|
1439
|
+
...(options.meshopt === undefined ? {} : { meshopt: options.meshopt }),
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function legacyParseResult(
|
|
1444
|
+
result: Result<GltfDoc, GltfParseError>,
|
|
1445
|
+
filePath: string,
|
|
1446
|
+
byteOffset: number,
|
|
1447
|
+
): Result<GltfDoc, GltfError> {
|
|
1448
|
+
if (result.ok) return ok(result.value);
|
|
1449
|
+
if (result.error instanceof ImportError) {
|
|
1450
|
+
return err(gltfErr('gltf-malformed-header', { filePath, byteOffset }));
|
|
1451
|
+
}
|
|
1452
|
+
return err(result.error);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
export async function parseGltf(
|
|
1456
|
+
json: unknown,
|
|
1457
|
+
externalLoader: ExternalLoader,
|
|
1458
|
+
filePath: string,
|
|
1459
|
+
options: GltfParseOptions = {},
|
|
1460
|
+
): Promise<Result<GltfDoc, GltfError>> {
|
|
1461
|
+
const result = await parseGltfForImporter(json, externalLoader, filePath, options);
|
|
1462
|
+
return legacyParseResult(result, filePath, 0);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* Parse a GLB 2.0 binary container: split into JSON + BIN chunks via
|
|
1467
|
+
* `parseGlbChunks`, then run the JSON document through `parseGltf` with
|
|
1468
|
+
* the BIN chunk wired in as buffer-0 backing storage.
|
|
1469
|
+
*/
|
|
1470
|
+
export async function parseGlbForImporter(
|
|
1471
|
+
buffer: ArrayBuffer,
|
|
1472
|
+
filePath: string,
|
|
1473
|
+
options: GltfParseOptions = {},
|
|
1474
|
+
): Promise<Result<GltfDoc, GltfParseError>> {
|
|
1475
|
+
const chunksResult = parseGlbChunks(buffer, filePath);
|
|
1476
|
+
if (!chunksResult.ok) return err(chunksResult.error);
|
|
1477
|
+
let json: unknown;
|
|
1478
|
+
try {
|
|
1479
|
+
json = JSON.parse(new TextDecoder().decode(chunksResult.value.jsonChunk));
|
|
1480
|
+
} catch (_e) {
|
|
1481
|
+
return err(
|
|
1482
|
+
gltfErr('gltf-malformed-header', {
|
|
1483
|
+
filePath,
|
|
1484
|
+
byteOffset: 12,
|
|
1485
|
+
}),
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
if (json === null || typeof json !== 'object') {
|
|
1489
|
+
return err(
|
|
1490
|
+
gltfErr('gltf-malformed-header', {
|
|
1491
|
+
filePath,
|
|
1492
|
+
byteOffset: 12,
|
|
1493
|
+
}),
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
const externalLoader: ExternalLoader = async (uri: string) => {
|
|
1497
|
+
throw new Error(`parseGlb: GLB containers must not reference external URIs (got ${uri})`);
|
|
1498
|
+
};
|
|
1499
|
+
return parseGltfWithBin(json as RootGltfJson, {
|
|
1500
|
+
externalLoader,
|
|
1501
|
+
...(chunksResult.value.binChunk === undefined ? {} : { binChunk: chunksResult.value.binChunk }),
|
|
1502
|
+
filePath,
|
|
1503
|
+
...(options.meshopt === undefined ? {} : { meshopt: options.meshopt }),
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
export async function parseGlb(
|
|
1508
|
+
buffer: ArrayBuffer,
|
|
1509
|
+
filePath: string,
|
|
1510
|
+
options: GltfParseOptions = {},
|
|
1511
|
+
): Promise<Result<GltfDoc, GltfError>> {
|
|
1512
|
+
const result = await parseGlbForImporter(buffer, filePath, options);
|
|
1513
|
+
return legacyParseResult(result, filePath, 12);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
export interface GltfAssetPack {
|
|
1517
|
+
readonly meta: GltfMetaJson;
|
|
1518
|
+
readonly subAssets: readonly GltfSubAssetEntry[];
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
export type GltfAssetPackResult = Result<GltfAssetPack, GltfSourceKeyError>;
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Project a parsed `GltfDoc` into the disk-shape `<source>.meta.json` plus
|
|
1525
|
+
* the freshly minted `subAssets[]` list (UUIDv7 + reimport-reuse-meta
|
|
1526
|
+
* two-stage match per plan-strategy section 2.4). Source-key conflicts are
|
|
1527
|
+
* returned before GUID minting or serialization.
|
|
1528
|
+
*
|
|
1529
|
+
* `existingMeta` is the previously-written `<source>.meta.json` parsed back
|
|
1530
|
+
* into memory (callers typically read + JSON.parse it before invoking).
|
|
1531
|
+
* `undefined` triggers first-pass full-fresh GUID minting.
|
|
1532
|
+
*
|
|
1533
|
+
* Sub-asset ordering: meshes first, then materials, then scenes - the
|
|
1534
|
+
* order is stable across reimports so AC-13 byte-identical holds when
|
|
1535
|
+
* source content does not change. Stable semantic sourceKey values are
|
|
1536
|
+
* assigned by the producer reuse boundary; sourceIndex remains a locator.
|
|
1537
|
+
*/
|
|
1538
|
+
export function toAssetPack(
|
|
1539
|
+
doc: GltfDoc,
|
|
1540
|
+
existingMeta: GltfMetaJson | undefined,
|
|
1541
|
+
source: string,
|
|
1542
|
+
): GltfAssetPackResult {
|
|
1543
|
+
const items: GltfDocItem[] = [];
|
|
1544
|
+
// Mesh sub-assets are keyed on the *original glTF mesh-index*, not on the
|
|
1545
|
+
// flat GltfMeshIr index. parseGltf flattens N glTF meshes with M_i primitives
|
|
1546
|
+
// into sum(M_i) GltfMeshIr rows (each carries its owning meshIndex). The
|
|
1547
|
+
// gltfImporter merges all primitives sharing a meshIndex into one MeshAsset
|
|
1548
|
+
// with M_i Submesh entries (one per primitive), so the meta sidecar must
|
|
1549
|
+
// emit exactly one `kind: 'mesh'` row per unique meshIndex — not one per
|
|
1550
|
+
// flat GltfMeshIr row, which would over-emit M_i sub-assets per glTF mesh and
|
|
1551
|
+
// desynchronise from runtime MeshRenderer.materials[].length.
|
|
1552
|
+
const seenMeshIndices = new Set<number>();
|
|
1553
|
+
for (const m of doc.meshes) {
|
|
1554
|
+
if (m === undefined) continue;
|
|
1555
|
+
if (seenMeshIndices.has(m.meshIndex)) continue;
|
|
1556
|
+
seenMeshIndices.add(m.meshIndex);
|
|
1557
|
+
items.push({
|
|
1558
|
+
kind: 'mesh',
|
|
1559
|
+
sourceIndex: m.meshIndex,
|
|
1560
|
+
...(m.name === undefined ? {} : { name: m.name }),
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
for (let i = 0; i < doc.materials.length; i++) {
|
|
1564
|
+
const m = doc.materials[i];
|
|
1565
|
+
if (m === undefined) continue;
|
|
1566
|
+
items.push({
|
|
1567
|
+
kind: 'material',
|
|
1568
|
+
sourceIndex: i,
|
|
1569
|
+
...(m.name === undefined ? {} : { name: m.name }),
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
for (let i = 0; i < doc.scenes.length; i++) {
|
|
1573
|
+
const s = doc.scenes[i];
|
|
1574
|
+
if (s === undefined) continue;
|
|
1575
|
+
items.push({
|
|
1576
|
+
kind: 'scene',
|
|
1577
|
+
sourceIndex: i,
|
|
1578
|
+
...(s.name === undefined ? {} : { name: s.name }),
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
// Texture sub-assets — one entry per `images[]` row (feat-20260608 M3 D-3,
|
|
1582
|
+
// requirements G-2 / AC-13). Orphan images (declared but unreferenced by
|
|
1583
|
+
// any `textures[]` row) still produce a sub-asset; the importer assigns
|
|
1584
|
+
// them colorSpace 'linear' (no colour-encoded purpose inferable). Without
|
|
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
|
+
const images = doc.images ?? [];
|
|
1588
|
+
for (let i = 0; i < images.length; i++) {
|
|
1589
|
+
const img = images[i];
|
|
1590
|
+
if (img === undefined) continue;
|
|
1591
|
+
items.push({
|
|
1592
|
+
kind: 'texture',
|
|
1593
|
+
sourceIndex: i,
|
|
1594
|
+
...(img.name === undefined ? {} : { name: img.name }),
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
const samplers = doc.samplers ?? [];
|
|
1598
|
+
for (let i = 0; i < samplers.length; i++) {
|
|
1599
|
+
const sampler = samplers[i];
|
|
1600
|
+
if (sampler === undefined) continue;
|
|
1601
|
+
items.push({
|
|
1602
|
+
kind: 'sampler',
|
|
1603
|
+
sourceIndex: i,
|
|
1604
|
+
...(sampler.name === undefined ? {} : { name: sampler.name }),
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
// Skeletons (feat-20260523-skin-skeleton-animation M0).
|
|
1608
|
+
for (let i = 0; i < doc.skeletons.length; i++) {
|
|
1609
|
+
items.push({ kind: 'skeleton', sourceIndex: i });
|
|
1610
|
+
}
|
|
1611
|
+
// Skin bindings (feat-20260523-skin-skeleton-animation M0).
|
|
1612
|
+
// Emit one skin sub-asset per GltfSkeletonRecord (1:1 mapping).
|
|
1613
|
+
for (let i = 0; i < doc.skeletons.length; i++) {
|
|
1614
|
+
items.push({ kind: 'skin', sourceIndex: i });
|
|
1615
|
+
}
|
|
1616
|
+
// Animation clips (feat-20260523-skin-skeleton-animation M0).
|
|
1617
|
+
for (let i = 0; i < doc.animationClips.length; i++) {
|
|
1618
|
+
const clip = doc.animationClips[i];
|
|
1619
|
+
if (clip === undefined) continue;
|
|
1620
|
+
items.push({
|
|
1621
|
+
kind: 'animation-clip',
|
|
1622
|
+
sourceIndex: i,
|
|
1623
|
+
...(clip.name === undefined ? {} : { name: clip.name }),
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
const reuse = reimportReuseMeta(items, existingMeta);
|
|
1628
|
+
if (!reuse.ok) return reuse;
|
|
1629
|
+
const subAssetByKindIndex = new Map(
|
|
1630
|
+
reuse.value.subAssets.map((entry) => [`${entry.kind}:${entry.sourceIndex}`, entry]),
|
|
1631
|
+
);
|
|
1632
|
+
const sourceOverrides: Record<string, Readonly<Record<string, unknown>>> = {
|
|
1633
|
+
...(existingMeta?.sourceOverrides ?? {}),
|
|
1634
|
+
};
|
|
1635
|
+
for (const meshIndex of seenMeshIndices) {
|
|
1636
|
+
const meshOutput = subAssetByKindIndex.get(`mesh:${meshIndex}`);
|
|
1637
|
+
if (meshOutput?.sourceKey === undefined) continue;
|
|
1638
|
+
const materialSlots: MeshMaterialSlotTopologyEntry[] = [];
|
|
1639
|
+
const slotByMaterial = new Map<number | null, number>();
|
|
1640
|
+
const usedNames = new Set<string>();
|
|
1641
|
+
const uniqueName = (raw: string): string => {
|
|
1642
|
+
const base = raw.trim() || 'Material';
|
|
1643
|
+
let candidate = base;
|
|
1644
|
+
let suffix = 2;
|
|
1645
|
+
while (usedNames.has(candidate)) candidate = `${base}_${suffix++}`;
|
|
1646
|
+
usedNames.add(candidate);
|
|
1647
|
+
return candidate;
|
|
1648
|
+
};
|
|
1649
|
+
for (const primitive of doc.meshes.filter((mesh) => mesh.meshIndex === meshIndex)) {
|
|
1650
|
+
const materialIndex = primitive.materialIndex;
|
|
1651
|
+
if (slotByMaterial.has(materialIndex)) continue;
|
|
1652
|
+
const materialOutput =
|
|
1653
|
+
materialIndex === null ? undefined : subAssetByKindIndex.get(`material:${materialIndex}`);
|
|
1654
|
+
slotByMaterial.set(materialIndex, materialSlots.length);
|
|
1655
|
+
materialSlots.push({
|
|
1656
|
+
slotName: uniqueName(
|
|
1657
|
+
materialIndex === null
|
|
1658
|
+
? 'Default'
|
|
1659
|
+
: (doc.materials[materialIndex]?.name ?? `Material_${materialIndex}`),
|
|
1660
|
+
),
|
|
1661
|
+
sourceKey:
|
|
1662
|
+
materialIndex === null
|
|
1663
|
+
? 'gltf:default'
|
|
1664
|
+
: (materialOutput?.sourceKey ?? `gltf:material:${materialIndex}`),
|
|
1665
|
+
...(materialOutput === undefined ? {} : { defaultMaterialGuid: materialOutput.guid }),
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
const previousPayload = existingMeta?.sourceOverrides?.[meshOutput.sourceKey];
|
|
1669
|
+
const previousRaw = previousPayload?.materialSlots;
|
|
1670
|
+
const previous = Array.isArray(previousRaw)
|
|
1671
|
+
? previousRaw.filter(
|
|
1672
|
+
(slot): slot is MeshMaterialSlotTopologyEntry =>
|
|
1673
|
+
slot !== null &&
|
|
1674
|
+
typeof slot === 'object' &&
|
|
1675
|
+
!Array.isArray(slot) &&
|
|
1676
|
+
typeof (slot as { slotName?: unknown }).slotName === 'string',
|
|
1677
|
+
)
|
|
1678
|
+
: [];
|
|
1679
|
+
const reconciled = reconcileMeshMaterialSlotTopology(materialSlots, previous);
|
|
1680
|
+
if (!reconciled.ok) {
|
|
1681
|
+
return err({
|
|
1682
|
+
code: 'mesh-material-slot-topology-change',
|
|
1683
|
+
expected: `unambiguous material slot identity for mesh ${meshOutput.guid}`,
|
|
1684
|
+
hint: reconciled.error.hint,
|
|
1685
|
+
detail: {
|
|
1686
|
+
sourceIndices: reconciled.error.nextIndices,
|
|
1687
|
+
previousIndices: reconciled.error.previousIndices,
|
|
1688
|
+
},
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
sourceOverrides[meshOutput.sourceKey] = {
|
|
1692
|
+
...(previousPayload ?? {}),
|
|
1693
|
+
materialSlots: reconciled.slots,
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
const meta: GltfMetaJson = {
|
|
1697
|
+
schemaVersion: 1,
|
|
1698
|
+
kind: 'external-asset-package',
|
|
1699
|
+
importer: 'gltf',
|
|
1700
|
+
source,
|
|
1701
|
+
subAssets: reuse.value.subAssets,
|
|
1702
|
+
...(Object.keys(sourceOverrides).length === 0 ? {} : { sourceOverrides }),
|
|
1703
|
+
sourceOverrideDescriptors: reuse.value.subAssets
|
|
1704
|
+
.filter((entry) => entry.kind === 'mesh' && entry.sourceKey !== undefined)
|
|
1705
|
+
.map((entry) => ({
|
|
1706
|
+
sourceKey: entry.sourceKey as string,
|
|
1707
|
+
semantic: 'mesh-material-slot-defaults' as const,
|
|
1708
|
+
payloadSchema: MESH_MATERIAL_SLOT_SOURCE_OVERRIDE_PAYLOAD_SCHEMA,
|
|
1709
|
+
})),
|
|
1710
|
+
importSettings: {
|
|
1711
|
+
defaultSceneIndex: doc.defaultSceneIndex,
|
|
1712
|
+
...(typeof existingMeta?.importSettings.standardMaterialGuid === 'string'
|
|
1713
|
+
? { standardMaterialGuid: existingMeta.importSettings.standardMaterialGuid }
|
|
1714
|
+
: {}),
|
|
1715
|
+
...(existingMeta?.importSettings.downscaleMaxDimension !== undefined
|
|
1716
|
+
? { downscaleMaxDimension: existingMeta.importSettings.downscaleMaxDimension }
|
|
1717
|
+
: {}),
|
|
1718
|
+
diagnostics: {
|
|
1719
|
+
nodeNames: doc.diagnostics.nodeNames,
|
|
1720
|
+
unsupportedExtensions: doc.diagnostics.unsupportedExtensions,
|
|
1721
|
+
matrixTrsCoexistNodes: doc.diagnostics.matrixTrsCoexistNodes,
|
|
1722
|
+
},
|
|
1723
|
+
},
|
|
1724
|
+
};
|
|
1725
|
+
return ok({ meta, subAssets: reuse.value.subAssets });
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// Suppress unused-import warning; COMPONENT_TYPE is re-exported so
|
|
1729
|
+
// downstream consumers can grep the constants table from a single
|
|
1730
|
+
// import. (TS strict mode would otherwise drop the binding.)
|
|
1731
|
+
export { COMPONENT_TYPE };
|