@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.
Files changed (220) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +204 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/animation-target-errors.unit.test.d.ts +2 -0
  5. package/dist/__tests__/animation-target-errors.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/bridge-material-samplers.unit.test.d.ts +2 -0
  7. package/dist/__tests__/bridge-material-samplers.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/bridge-material-values.unit.test.d.ts +2 -0
  9. package/dist/__tests__/bridge-material-values.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/bridge-mixed-color.unit.test.d.ts +2 -0
  11. package/dist/__tests__/bridge-mixed-color.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/bridge-sampler-required.test-d.d.ts +2 -0
  13. package/dist/__tests__/bridge-sampler-required.test-d.d.ts.map +1 -0
  14. package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts +2 -0
  15. package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/bridge-skin-stride.unit.test.d.ts +2 -0
  17. package/dist/__tests__/bridge-skin-stride.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/bridge-test-helpers.d.ts +5 -0
  19. package/dist/__tests__/bridge-test-helpers.d.ts.map +1 -0
  20. package/dist/__tests__/bridge.unit.test.d.ts +2 -0
  21. package/dist/__tests__/bridge.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/cli-gltf.integration.test.d.ts +2 -0
  23. package/dist/__tests__/cli-gltf.integration.test.d.ts.map +1 -0
  24. package/dist/__tests__/color-0-accessor.unit.test.d.ts +2 -0
  25. package/dist/__tests__/color-0-accessor.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/data-uri-surface.unit.test.d.ts +2 -0
  27. package/dist/__tests__/data-uri-surface.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/extension-admission.unit.test.d.ts +2 -0
  29. package/dist/__tests__/extension-admission.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/gltf-error-derived-views.test-d.d.ts +2 -0
  31. package/dist/__tests__/gltf-error-derived-views.test-d.d.ts.map +1 -0
  32. package/dist/__tests__/gltf-error-migration.test.d.ts +2 -0
  33. package/dist/__tests__/gltf-error-migration.test.d.ts.map +1 -0
  34. package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts +2 -0
  35. package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts.map +1 -0
  36. package/dist/__tests__/gltf-importer.test.d.ts +2 -0
  37. package/dist/__tests__/gltf-importer.test.d.ts.map +1 -0
  38. package/dist/__tests__/gltf-local-artifacts.test.d.ts +2 -0
  39. package/dist/__tests__/gltf-local-artifacts.test.d.ts.map +1 -0
  40. package/dist/__tests__/gltf.unit.test.d.ts +2 -0
  41. package/dist/__tests__/gltf.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts +2 -0
  43. package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts.map +1 -0
  44. package/dist/__tests__/material-missing-uv.unit.test.d.ts +2 -0
  45. package/dist/__tests__/material-missing-uv.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/material-pack-refs.integration.test.d.ts +2 -0
  47. package/dist/__tests__/material-pack-refs.integration.test.d.ts.map +1 -0
  48. package/dist/__tests__/material-per-slot-ir.unit.test.d.ts +2 -0
  49. package/dist/__tests__/material-per-slot-ir.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/material-texture-transform.unit.test.d.ts +2 -0
  51. package/dist/__tests__/material-texture-transform.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/material-unused-uv.unit.test.d.ts +2 -0
  53. package/dist/__tests__/material-unused-uv.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/meshopt-decode.unit.test.d.ts +2 -0
  55. package/dist/__tests__/meshopt-decode.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/meshopt-falsifier.unit.test.d.ts +2 -0
  57. package/dist/__tests__/meshopt-falsifier.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts +2 -0
  59. package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts.map +1 -0
  60. package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts +2 -0
  61. package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts.map +1 -0
  62. package/dist/__tests__/morph-import.integration.test.d.ts +2 -0
  63. package/dist/__tests__/morph-import.integration.test.d.ts.map +1 -0
  64. package/dist/__tests__/morph-import.unit.test.d.ts +2 -0
  65. package/dist/__tests__/morph-import.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/parse-decode.unit.test.d.ts +2 -0
  67. package/dist/__tests__/parse-decode.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/parse-gltf-multi-uv.test.d.ts +2 -0
  69. package/dist/__tests__/parse-gltf-multi-uv.test.d.ts.map +1 -0
  70. package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts +2 -0
  71. package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts.map +1 -0
  72. package/dist/__tests__/parse-gltf.unit.test.d.ts +2 -0
  73. package/dist/__tests__/parse-gltf.unit.test.d.ts.map +1 -0
  74. package/dist/__tests__/pick-e2e.integration.test.d.ts +2 -0
  75. package/dist/__tests__/pick-e2e.integration.test.d.ts.map +1 -0
  76. package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
  77. package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
  78. package/dist/__tests__/source-key-diagnostics.integration.test.d.ts +2 -0
  79. package/dist/__tests__/source-key-diagnostics.integration.test.d.ts.map +1 -0
  80. package/dist/__tests__/source-key-producer.integration.test.d.ts +2 -0
  81. package/dist/__tests__/source-key-producer.integration.test.d.ts.map +1 -0
  82. package/dist/__tests__/source-key.unit.test.d.ts +2 -0
  83. package/dist/__tests__/source-key.unit.test.d.ts.map +1 -0
  84. package/dist/accessor/decode-accessor.d.ts +75 -0
  85. package/dist/accessor/decode-accessor.d.ts.map +1 -0
  86. package/dist/accessor/decode-color.d.ts +15 -0
  87. package/dist/accessor/decode-color.d.ts.map +1 -0
  88. package/dist/accessor/index.d.ts +3 -0
  89. package/dist/accessor/index.d.ts.map +1 -0
  90. package/dist/bridge.d.ts +103 -0
  91. package/dist/bridge.d.ts.map +1 -0
  92. package/dist/check-extensions.d.ts +29 -0
  93. package/dist/check-extensions.d.ts.map +1 -0
  94. package/dist/cli-gltf.d.ts +8 -0
  95. package/dist/cli-gltf.d.ts.map +1 -0
  96. package/dist/cli-gltf.mjs +2435 -0
  97. package/dist/cli-gltf.mjs.map +1 -0
  98. package/dist/data-uri.d.ts +6 -0
  99. package/dist/data-uri.d.ts.map +1 -0
  100. package/dist/errors.d.ts +201 -0
  101. package/dist/errors.d.ts.map +1 -0
  102. package/dist/extract-image-bytes.d.ts +42 -0
  103. package/dist/extract-image-bytes.d.ts.map +1 -0
  104. package/dist/gltf-importer.d.ts +21 -0
  105. package/dist/gltf-importer.d.ts.map +1 -0
  106. package/dist/image-color-space.d.ts +35 -0
  107. package/dist/image-color-space.d.ts.map +1 -0
  108. package/dist/importer-entry.d.ts +10 -0
  109. package/dist/importer-entry.d.ts.map +1 -0
  110. package/dist/importer-entry.mjs +3378 -0
  111. package/dist/importer-entry.mjs.map +1 -0
  112. package/dist/index.d.ts +25 -0
  113. package/dist/index.d.ts.map +1 -0
  114. package/dist/index.mjs +3673 -0
  115. package/dist/index.mjs.map +1 -0
  116. package/dist/meshopt-decode.d.ts +39 -0
  117. package/dist/meshopt-decode.d.ts.map +1 -0
  118. package/dist/node-file-entry.d.ts +14 -0
  119. package/dist/node-file-entry.d.ts.map +1 -0
  120. package/dist/node-file-entry.mjs +1946 -0
  121. package/dist/node-file-entry.mjs.map +1 -0
  122. package/dist/node-path.d.ts +14 -0
  123. package/dist/node-path.d.ts.map +1 -0
  124. package/dist/parse-animation.d.ts +70 -0
  125. package/dist/parse-animation.d.ts.map +1 -0
  126. package/dist/parse-glb-chunks.d.ts +25 -0
  127. package/dist/parse-glb-chunks.d.ts.map +1 -0
  128. package/dist/parse-glb.d.ts +3 -0
  129. package/dist/parse-glb.d.ts.map +1 -0
  130. package/dist/parse-gltf-header.d.ts +18 -0
  131. package/dist/parse-gltf-header.d.ts.map +1 -0
  132. package/dist/parse-gltf.d.ts +263 -0
  133. package/dist/parse-gltf.d.ts.map +1 -0
  134. package/dist/parse-skin.d.ts +70 -0
  135. package/dist/parse-skin.d.ts.map +1 -0
  136. package/dist/reimport-reuse-meta.d.ts +48 -0
  137. package/dist/reimport-reuse-meta.d.ts.map +1 -0
  138. package/dist/serialize-meta.d.ts +2 -0
  139. package/dist/serialize-meta.d.ts.map +1 -0
  140. package/dist/source-key.d.ts +44 -0
  141. package/dist/source-key.d.ts.map +1 -0
  142. package/dist/sub-asset-key.d.ts +12 -0
  143. package/dist/sub-asset-key.d.ts.map +1 -0
  144. package/dist/to-asset-pack.d.ts +2 -0
  145. package/dist/to-asset-pack.d.ts.map +1 -0
  146. package/dist/transform.d.ts +37 -0
  147. package/dist/transform.d.ts.map +1 -0
  148. package/package.json +92 -0
  149. package/src/__tests__/animation-target-errors.unit.test.ts +51 -0
  150. package/src/__tests__/bridge-material-samplers.unit.test.ts +67 -0
  151. package/src/__tests__/bridge-material-values.unit.test.ts +106 -0
  152. package/src/__tests__/bridge-mixed-color.unit.test.ts +100 -0
  153. package/src/__tests__/bridge-sampler-required.test-d.ts +59 -0
  154. package/src/__tests__/bridge-skin-shader-route.unit.test.ts +42 -0
  155. package/src/__tests__/bridge-skin-stride.unit.test.ts +115 -0
  156. package/src/__tests__/bridge-test-helpers.ts +9 -0
  157. package/src/__tests__/bridge.unit.test.ts +1227 -0
  158. package/src/__tests__/cli-gltf.integration.test.ts +122 -0
  159. package/src/__tests__/color-0-accessor.unit.test.ts +174 -0
  160. package/src/__tests__/data-uri-surface.unit.test.ts +26 -0
  161. package/src/__tests__/extension-admission.unit.test.ts +19 -0
  162. package/src/__tests__/fixtures/color-0/float-vec3.gltf +13 -0
  163. package/src/__tests__/fixtures/color-0/interleaved-vec3.gltf +6 -0
  164. package/src/__tests__/fixtures/color-0/normalized-ubyte.gltf +6 -0
  165. package/src/__tests__/fixtures/color-0/normalized-ushort.gltf +6 -0
  166. package/src/__tests__/gltf-error-derived-views.test-d.ts +152 -0
  167. package/src/__tests__/gltf-error-migration.test.ts +147 -0
  168. package/src/__tests__/gltf-error-policy-owner.unit.test.ts +185 -0
  169. package/src/__tests__/gltf-importer.test.ts +12 -0
  170. package/src/__tests__/gltf-local-artifacts.test.ts +11 -0
  171. package/src/__tests__/gltf.unit.test.ts +2975 -0
  172. package/src/__tests__/malformed-buffer-data-uri.integration.test.ts +140 -0
  173. package/src/__tests__/material-missing-uv.unit.test.ts +29 -0
  174. package/src/__tests__/material-pack-refs.integration.test.ts +58 -0
  175. package/src/__tests__/material-per-slot-ir.unit.test.ts +90 -0
  176. package/src/__tests__/material-texture-transform.unit.test.ts +105 -0
  177. package/src/__tests__/material-unused-uv.unit.test.ts +44 -0
  178. package/src/__tests__/meshopt-decode.unit.test.ts +87 -0
  179. package/src/__tests__/meshopt-falsifier.unit.test.ts +92 -0
  180. package/src/__tests__/meshopt-mode-filter-owner.test-d.ts +79 -0
  181. package/src/__tests__/meshopt-real-matrix.integration.test.ts +370 -0
  182. package/src/__tests__/morph-import.integration.test.ts +115 -0
  183. package/src/__tests__/morph-import.unit.test.ts +62 -0
  184. package/src/__tests__/parse-decode.unit.test.ts +919 -0
  185. package/src/__tests__/parse-gltf-multi-uv.test.ts +238 -0
  186. package/src/__tests__/parse-gltf-skin-attrs.unit.test.ts +191 -0
  187. package/src/__tests__/parse-gltf.unit.test.ts +133 -0
  188. package/src/__tests__/pick-e2e.integration.test.ts +238 -0
  189. package/src/__tests__/public-surface.unit.test.ts +10 -0
  190. package/src/__tests__/source-key-diagnostics.integration.test.ts +140 -0
  191. package/src/__tests__/source-key-producer.integration.test.ts +171 -0
  192. package/src/__tests__/source-key.unit.test.ts +159 -0
  193. package/src/accessor/decode-accessor.ts +309 -0
  194. package/src/accessor/decode-color.ts +128 -0
  195. package/src/accessor/index.ts +15 -0
  196. package/src/bridge.ts +928 -0
  197. package/src/check-extensions.ts +85 -0
  198. package/src/cli-gltf.ts +339 -0
  199. package/src/data-uri.ts +29 -0
  200. package/src/errors.ts +386 -0
  201. package/src/extract-image-bytes.ts +343 -0
  202. package/src/gltf-importer.ts +976 -0
  203. package/src/image-color-space.ts +104 -0
  204. package/src/importer-entry.ts +24 -0
  205. package/src/index.ts +148 -0
  206. package/src/meshopt-decode.ts +200 -0
  207. package/src/node-file-entry.ts +98 -0
  208. package/src/node-path.ts +41 -0
  209. package/src/parse-animation.ts +286 -0
  210. package/src/parse-glb-chunks.ts +167 -0
  211. package/src/parse-glb.ts +16 -0
  212. package/src/parse-gltf-header.ts +58 -0
  213. package/src/parse-gltf.ts +1731 -0
  214. package/src/parse-skin.ts +238 -0
  215. package/src/reimport-reuse-meta.ts +134 -0
  216. package/src/serialize-meta.ts +24 -0
  217. package/src/source-key.ts +106 -0
  218. package/src/sub-asset-key.ts +61 -0
  219. package/src/to-asset-pack.ts +8 -0
  220. package/src/transform.ts +117 -0
@@ -0,0 +1,343 @@
1
+ // extract-image-bytes.ts - resolve glTF `images[<imageIndex>]` into raw
2
+ // PNG / JPEG bytes (feat-20260608 M3 D-6 / requirements AC-08 + AC-09 + AC-10).
3
+ //
4
+ // The gltfImporter walks `images[]` and funnels three image source paths
5
+ // through a single decode seam:
6
+ // (a) bufferView : slice the named buffer at the bufferView range
7
+ // (b) data: URI : base64-decode the URI payload
8
+ // (c) external URI : ctx.readSibling(uri) reads the file co-located
9
+ // with the .gltf source
10
+ //
11
+ // This module owns the byte-extraction (no decoding); decoding is the job of
12
+ // ctx.decodeImage (D-1 seam: gltfImporter never imports
13
+ // `@forgeax/engine-image` directly). On any failure the caller produces a
14
+ // `gltf-image-extract-failed` GltfError carrying { imageIndex, source,
15
+ // reason }.
16
+ //
17
+ // Why not reuse parse-gltf.ts's buffer resolution? parseGltf.ts encapsulates
18
+ // buffer / bufferView decoding inside `parseGltfWithBin` and never surfaces
19
+ // the byte arrays. Re-running that machinery for image extraction would
20
+ // double the work; instead the gltfImporter parses the doc twice (parseGltf
21
+ // for the IR, this helper for the raw bytes) — both passes are O(file
22
+ // size) and the importer only runs at build time.
23
+
24
+ import type { ImportContext, ImportError as ImportErrorType } from '@forgeax/engine-types';
25
+ import { dataUriBase64Payload, decodeBase64 } from './data-uri.js';
26
+ import { parseGlbChunks } from './parse-glb-chunks.js';
27
+
28
+ /** Source of an image: matches `GltfImageExtractFailedDetail.source`. */
29
+ export type ImageSourceKind = 'bufferView' | 'data-uri' | 'external-uri';
30
+
31
+ /** One extracted image: bytes + the source kind that produced them. */
32
+ export interface ExtractedImage {
33
+ readonly bytes: Uint8Array;
34
+ readonly mimeType: 'image/png' | 'image/jpeg';
35
+ readonly source: ImageSourceKind;
36
+ }
37
+
38
+ /**
39
+ * Failure shape — the importer wraps this in a `gltf-image-extract-failed`
40
+ * GltfError. `source` may be undefined when the image row is malformed
41
+ * (no uri / no bufferView), so the importer can name 'bufferView' (most
42
+ * common) by default.
43
+ */
44
+ export interface ExtractFailure {
45
+ readonly imageIndex: number;
46
+ readonly source: ImageSourceKind;
47
+ readonly reason: string;
48
+ }
49
+
50
+ const DATA_URI_MIME_RE = /^data:([^;,]+)(?:;[^,;]+)*;base64,/;
51
+
52
+ /** Pull the source-read-failed reason out of an ImportError without
53
+ * widening the discriminated union — the caller only ever sees
54
+ * source-read-failed here (readSibling's only failure shape). The
55
+ * ImportError class carries a structurally typed `.detail` field that does
56
+ * not auto-narrow on the `.code` discriminator, so we read `.message`
57
+ * which the class composes from `expected: ...; reason: ...`. */
58
+ function describeImportError(err: ImportErrorType): string {
59
+ return err.message;
60
+ }
61
+
62
+ /** Pick PNG vs JPEG from declared mimeType + magic-byte fallback. */
63
+ function classifyMime(
64
+ declaredMime: string | undefined,
65
+ bytes: Uint8Array,
66
+ ): 'image/png' | 'image/jpeg' | undefined {
67
+ if (declaredMime === 'image/png' || declaredMime === 'image/jpeg') return declaredMime;
68
+ if (
69
+ bytes.length >= 8 &&
70
+ bytes[0] === 0x89 &&
71
+ bytes[1] === 0x50 &&
72
+ bytes[2] === 0x4e &&
73
+ bytes[3] === 0x47
74
+ ) {
75
+ return 'image/png';
76
+ }
77
+ if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
78
+ return 'image/jpeg';
79
+ }
80
+ return undefined;
81
+ }
82
+
83
+ interface BufferViewJsonSlim {
84
+ readonly buffer: number;
85
+ readonly byteOffset?: number;
86
+ readonly byteLength: number;
87
+ }
88
+
89
+ interface ImageJsonSlim {
90
+ readonly uri?: string;
91
+ readonly mimeType?: string;
92
+ readonly bufferView?: number;
93
+ }
94
+
95
+ interface BufferJsonSlim {
96
+ readonly uri?: string;
97
+ readonly byteLength: number;
98
+ }
99
+
100
+ interface RootJsonSlim {
101
+ readonly images?: readonly ImageJsonSlim[];
102
+ readonly bufferViews?: readonly BufferViewJsonSlim[];
103
+ readonly buffers?: readonly BufferJsonSlim[];
104
+ }
105
+
106
+ /**
107
+ * Extract raw image bytes for every `images[]` row in the parsed glTF doc.
108
+ *
109
+ * @param sourceBytes the original .glb / .gltf file bytes (the GLB BIN
110
+ * chunk is reconstructed from these for buffer-0 lookups).
111
+ * @param sourcePath the meta.source path; used only for diagnostics
112
+ * (parseGlbChunks errors carry it through).
113
+ * @param ctx the ImportContext — reused for `readSibling` (external
114
+ * buffer .bin / external image .png/.jpg). data: URIs do not call ctx.
115
+ *
116
+ * Returns a Map<imageIndex, ExtractedImage> with one entry per resolved
117
+ * image. Rows that fail to extract are returned in `failures[]` so the
118
+ * caller can emit one `gltf-image-extract-failed` per failure (single
119
+ * failure does not abort the whole importer — the runner's GUID iron-law
120
+ * check then surfaces missing image GUIDs as a structured outage instead
121
+ * of the silent white-box render).
122
+ */
123
+ export async function extractImageBytes(
124
+ sourceBytes: Uint8Array,
125
+ sourcePath: string,
126
+ ctx: ImportContext,
127
+ ): Promise<{
128
+ readonly extracted: ReadonlyMap<number, ExtractedImage>;
129
+ readonly failures: readonly ExtractFailure[];
130
+ }> {
131
+ const isGlb = sourcePath.toLowerCase().endsWith('.glb');
132
+ let json: RootJsonSlim;
133
+ let glbBin: Uint8Array | undefined;
134
+
135
+ if (isGlb) {
136
+ const ab = sourceBytes.buffer.slice(
137
+ sourceBytes.byteOffset,
138
+ sourceBytes.byteOffset + sourceBytes.byteLength,
139
+ ) as ArrayBuffer;
140
+ const chunks = parseGlbChunks(ab, sourcePath);
141
+ if (!chunks.ok) {
142
+ return {
143
+ extracted: new Map(),
144
+ failures: [
145
+ {
146
+ imageIndex: -1,
147
+ source: 'bufferView',
148
+ reason: `GLB chunk parse failed: ${chunks.error.code}`,
149
+ },
150
+ ],
151
+ };
152
+ }
153
+ try {
154
+ json = JSON.parse(new TextDecoder().decode(chunks.value.jsonChunk)) as RootJsonSlim;
155
+ } catch (e) {
156
+ return {
157
+ extracted: new Map(),
158
+ failures: [
159
+ {
160
+ imageIndex: -1,
161
+ source: 'bufferView',
162
+ reason: `GLB JSON parse failed: ${e instanceof Error ? e.message : String(e)}`,
163
+ },
164
+ ],
165
+ };
166
+ }
167
+ glbBin = chunks.value.binChunk;
168
+ } else {
169
+ try {
170
+ json = JSON.parse(new TextDecoder().decode(sourceBytes)) as RootJsonSlim;
171
+ } catch (e) {
172
+ return {
173
+ extracted: new Map(),
174
+ failures: [
175
+ {
176
+ imageIndex: -1,
177
+ source: 'bufferView',
178
+ reason: `gltf JSON parse failed: ${e instanceof Error ? e.message : String(e)}`,
179
+ },
180
+ ],
181
+ };
182
+ }
183
+ }
184
+
185
+ const images = json.images ?? [];
186
+ const bufferViews = json.bufferViews ?? [];
187
+ const buffersJson = json.buffers ?? [];
188
+ const extracted = new Map<number, ExtractedImage>();
189
+ const failures: ExtractFailure[] = [];
190
+
191
+ // Resolve buffers (lazy: only buffers actually referenced by images get fetched).
192
+ const buffersCache = new Map<number, Uint8Array | { readonly error: ImportErrorType | string }>();
193
+ async function getBuffer(bufferIndex: number): Promise<Uint8Array | { readonly error: string }> {
194
+ const cached = buffersCache.get(bufferIndex);
195
+ if (cached !== undefined) {
196
+ if (cached instanceof Uint8Array) return cached;
197
+ const e = cached.error;
198
+ return { error: typeof e === 'string' ? e : e.message };
199
+ }
200
+ const bufJson = buffersJson[bufferIndex];
201
+ if (bufJson === undefined) {
202
+ const reason = `buffer index ${bufferIndex} out of range (have ${buffersJson.length})`;
203
+ buffersCache.set(bufferIndex, { error: reason });
204
+ return { error: reason };
205
+ }
206
+ if (bufJson.uri === undefined) {
207
+ // GLB BIN chunk slot — buffer-0.
208
+ if (glbBin === undefined) {
209
+ const reason = `buffer ${bufferIndex} has no uri and no GLB BIN chunk available`;
210
+ buffersCache.set(bufferIndex, { error: reason });
211
+ return { error: reason };
212
+ }
213
+ buffersCache.set(bufferIndex, glbBin);
214
+ return glbBin;
215
+ }
216
+ const dataPayload = dataUriBase64Payload(bufJson.uri);
217
+ if (dataPayload !== undefined) {
218
+ try {
219
+ const bytes = decodeBase64(dataPayload);
220
+ buffersCache.set(bufferIndex, bytes);
221
+ return bytes;
222
+ } catch (e) {
223
+ const reason = `buffer ${bufferIndex} data URI base64 decode failed: ${e instanceof Error ? e.message : String(e)}`;
224
+ buffersCache.set(bufferIndex, { error: reason });
225
+ return { error: reason };
226
+ }
227
+ }
228
+ const sib = await ctx.readSibling(bufJson.uri);
229
+ if (!sib.ok) {
230
+ const reason = `external buffer "${bufJson.uri}" read failed: ${describeImportError(sib.error)}`;
231
+ buffersCache.set(bufferIndex, { error: reason });
232
+ return { error: reason };
233
+ }
234
+ buffersCache.set(bufferIndex, sib.value);
235
+ return sib.value;
236
+ }
237
+
238
+ for (let i = 0; i < images.length; i++) {
239
+ const img = images[i];
240
+ if (img === undefined) continue;
241
+
242
+ if (img.bufferView !== undefined) {
243
+ const bv = bufferViews[img.bufferView];
244
+ if (bv === undefined) {
245
+ failures.push({
246
+ imageIndex: i,
247
+ source: 'bufferView',
248
+ reason: `bufferView index ${img.bufferView} out of range`,
249
+ });
250
+ continue;
251
+ }
252
+ const buf = await getBuffer(bv.buffer);
253
+ if (!(buf instanceof Uint8Array)) {
254
+ failures.push({ imageIndex: i, source: 'bufferView', reason: buf.error });
255
+ continue;
256
+ }
257
+ const off = bv.byteOffset ?? 0;
258
+ const len = bv.byteLength;
259
+ if (off + len > buf.byteLength) {
260
+ failures.push({
261
+ imageIndex: i,
262
+ source: 'bufferView',
263
+ reason: `bufferView ${img.bufferView} byte range [${off}..${off + len}) exceeds buffer ${bv.buffer} length ${buf.byteLength}`,
264
+ });
265
+ continue;
266
+ }
267
+ const bytes = buf.subarray(off, off + len);
268
+ const mime = classifyMime(img.mimeType, bytes);
269
+ if (mime === undefined) {
270
+ failures.push({
271
+ imageIndex: i,
272
+ source: 'bufferView',
273
+ reason: `unsupported / unrecognised mime (declared "${img.mimeType ?? '<absent>'}", magic byte sniff failed)`,
274
+ });
275
+ continue;
276
+ }
277
+ extracted.set(i, { bytes: new Uint8Array(bytes), mimeType: mime, source: 'bufferView' });
278
+ continue;
279
+ }
280
+
281
+ if (img.uri !== undefined) {
282
+ const dataPayload = dataUriBase64Payload(img.uri);
283
+ if (dataPayload !== undefined) {
284
+ let bytes: Uint8Array;
285
+ try {
286
+ bytes = decodeBase64(dataPayload);
287
+ } catch (e) {
288
+ failures.push({
289
+ imageIndex: i,
290
+ source: 'data-uri',
291
+ reason: `base64 decode failed: ${e instanceof Error ? e.message : String(e)}`,
292
+ });
293
+ continue;
294
+ }
295
+ const mimeMatch = DATA_URI_MIME_RE.exec(img.uri);
296
+ const declaredMime = mimeMatch?.[1] ?? img.mimeType;
297
+ const mime = classifyMime(declaredMime, bytes);
298
+ if (mime === undefined) {
299
+ failures.push({
300
+ imageIndex: i,
301
+ source: 'data-uri',
302
+ reason: `data: URI mime "${declaredMime ?? '<absent>'}" not png/jpeg`,
303
+ });
304
+ continue;
305
+ }
306
+ extracted.set(i, { bytes, mimeType: mime, source: 'data-uri' });
307
+ continue;
308
+ }
309
+ const sib = await ctx.readSibling(img.uri);
310
+ if (!sib.ok) {
311
+ failures.push({
312
+ imageIndex: i,
313
+ source: 'external-uri',
314
+ reason: `external URI "${img.uri}" read failed: ${describeImportError(sib.error)}`,
315
+ });
316
+ continue;
317
+ }
318
+ const mime = classifyMime(img.mimeType, sib.value);
319
+ if (mime === undefined) {
320
+ failures.push({
321
+ imageIndex: i,
322
+ source: 'external-uri',
323
+ reason: `external URI "${img.uri}" mime not png/jpeg (declared "${img.mimeType ?? '<absent>'}", magic byte sniff failed)`,
324
+ });
325
+ continue;
326
+ }
327
+ extracted.set(i, {
328
+ bytes: new Uint8Array(sib.value),
329
+ mimeType: mime,
330
+ source: 'external-uri',
331
+ });
332
+ continue;
333
+ }
334
+
335
+ failures.push({
336
+ imageIndex: i,
337
+ source: 'bufferView',
338
+ reason: 'image row has neither bufferView nor uri',
339
+ });
340
+ }
341
+
342
+ return { extracted, failures };
343
+ }