@galfus/gltf-loader 0.0.0-stage → 0.31.0-alpha

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/src/parser.ts ADDED
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Low-level binary GLB and JSON glTF deserialization with typed accessor decoding.
3
+ */
4
+
5
+ import {
6
+ type GlTF,
7
+ GLTF_COMPONENT_TYPE,
8
+ type GlTFAccessorType,
9
+ type GlTFComponentType
10
+ } from './types';
11
+
12
+ const GLB_MAGIC = 0x46546c67; // 'glTF'
13
+ const CHUNK_TYPE_JSON = 0x4e4f534a; // 'JSON'
14
+ const CHUNK_TYPE_BIN = 0x004e4942; // 'BIN\0'
15
+
16
+ export interface ParsedGltf {
17
+ gltf: GlTF;
18
+ buffers: Uint8Array[];
19
+ }
20
+
21
+ /**
22
+ * Decodes a base64 data URI into a raw Uint8Array.
23
+ */
24
+ export function decodeDataUri(uri: string): Uint8Array {
25
+ const commaIdx = uri.indexOf(',');
26
+ if (commaIdx === -1) {
27
+ throw new Error('Invalid data URI in glTF buffer');
28
+ }
29
+ const base64Str = uri.slice(commaIdx + 1);
30
+ const binaryStr = atob(base64Str);
31
+ const bytes = new Uint8Array(binaryStr.length);
32
+ for (let i = 0; i < binaryStr.length; i++) {
33
+ bytes[i] = binaryStr.charCodeAt(i);
34
+ }
35
+ return bytes;
36
+ }
37
+
38
+ /**
39
+ * Synchronously parses GLB binary containers or glTF JSON with embedded data URIs.
40
+ *
41
+ * @param input Raw file bytes.
42
+ * @returns Parsed glTF object and array of decoded binary buffers.
43
+ */
44
+ export function parseGltfSync(input: Uint8Array | ArrayBuffer): ParsedGltf {
45
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
46
+ const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
47
+
48
+ // Check for GLB binary container magic header
49
+ if (bytes.byteLength >= 12 && dataView.getUint32(0, true) === GLB_MAGIC) {
50
+ const version = dataView.getUint32(4, true);
51
+ if (version !== 2) {
52
+ throw new Error(`Unsupported GLB container version: ${version} (expected 2)`);
53
+ }
54
+
55
+ const totalLength = dataView.getUint32(8, true);
56
+ if (totalLength > bytes.byteLength) {
57
+ throw new Error('Truncated GLB container');
58
+ }
59
+
60
+ let offset = 12;
61
+ let jsonGltf: GlTF | undefined;
62
+ const buffers: Uint8Array[] = [];
63
+
64
+ while (offset < totalLength) {
65
+ const chunkLength = dataView.getUint32(offset, true);
66
+ const chunkType = dataView.getUint32(offset + 4, true);
67
+ const chunkData = new Uint8Array(bytes.buffer, bytes.byteOffset + offset + 8, chunkLength);
68
+
69
+ if (chunkType === CHUNK_TYPE_JSON) {
70
+ const jsonText = new TextDecoder().decode(chunkData);
71
+ jsonGltf = JSON.parse(jsonText) as GlTF;
72
+ } else if (chunkType === CHUNK_TYPE_BIN) {
73
+ buffers.push(chunkData);
74
+ }
75
+
76
+ offset += 8 + chunkLength;
77
+ }
78
+
79
+ if (!jsonGltf) {
80
+ throw new Error('GLB container is missing the required JSON chunk');
81
+ }
82
+
83
+ return { gltf: jsonGltf, buffers };
84
+ }
85
+
86
+ // Fallback: Plain JSON glTF format
87
+ const jsonText = new TextDecoder().decode(bytes);
88
+ const jsonGltf = JSON.parse(jsonText) as GlTF;
89
+ const buffers: Uint8Array[] = [];
90
+
91
+ if (jsonGltf.buffers) {
92
+ for (const buf of jsonGltf.buffers) {
93
+ if (buf.uri && buf.uri.startsWith('data:')) {
94
+ buffers.push(decodeDataUri(buf.uri));
95
+ } else {
96
+ throw new Error(`Cannot resolve external glTF buffer: ${buf.uri ?? '(missing URI)'}`);
97
+ }
98
+ }
99
+ }
100
+
101
+ return { gltf: jsonGltf, buffers };
102
+ }
103
+
104
+ /**
105
+ * Parses raw input data (Uint8Array or ArrayBuffer) into a GlTF structure and extracted binary buffers.
106
+ *
107
+ * Automatically resolves external buffers if an externalResolver is provided.
108
+ *
109
+ * @param input Raw file bytes.
110
+ * @param externalResolver Optional async callback to resolve external buffer URIs.
111
+ * @returns Parsed glTF object and array of decoded binary buffers.
112
+ */
113
+ export async function parseGltf(
114
+ input: Uint8Array | ArrayBuffer,
115
+ externalResolver?: (uri: string) => Promise<Uint8Array>
116
+ ): Promise<ParsedGltf> {
117
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
118
+ const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
119
+
120
+ if (bytes.byteLength >= 12 && dataView.getUint32(0, true) === GLB_MAGIC) {
121
+ return parseGltfSync(bytes);
122
+ }
123
+
124
+ const jsonText = new TextDecoder().decode(bytes);
125
+ const jsonGltf = JSON.parse(jsonText) as GlTF;
126
+ const buffers: Uint8Array[] = [];
127
+
128
+ if (jsonGltf.buffers) {
129
+ for (const buf of jsonGltf.buffers) {
130
+ if (buf.uri && buf.uri.startsWith('data:')) {
131
+ buffers.push(decodeDataUri(buf.uri));
132
+ } else if (buf.uri && externalResolver) {
133
+ buffers.push(await externalResolver(buf.uri));
134
+ } else {
135
+ throw new Error(`Cannot resolve external glTF buffer: ${buf.uri ?? '(missing URI)'}`);
136
+ }
137
+ }
138
+ }
139
+
140
+ return { gltf: jsonGltf, buffers };
141
+ }
142
+
143
+ /**
144
+ * Returns the number of components for a given glTF accessor type.
145
+ */
146
+ export function getComponentCountForType(type: GlTFAccessorType): number {
147
+ switch (type) {
148
+ case 'SCALAR':
149
+ return 1;
150
+ case 'VEC2':
151
+ return 2;
152
+ case 'VEC3':
153
+ return 3;
154
+ case 'VEC4':
155
+ case 'MAT2':
156
+ return 4;
157
+ case 'MAT3':
158
+ return 9;
159
+ case 'MAT4':
160
+ return 16;
161
+ default:
162
+ return 1;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Returns the byte size of an individual glTF component type.
168
+ */
169
+ export function getComponentByteSize(type: GlTFComponentType): number {
170
+ switch (type) {
171
+ case GLTF_COMPONENT_TYPE.BYTE:
172
+ case GLTF_COMPONENT_TYPE.UNSIGNED_BYTE:
173
+ return 1;
174
+ case GLTF_COMPONENT_TYPE.SHORT:
175
+ case GLTF_COMPONENT_TYPE.UNSIGNED_SHORT:
176
+ return 2;
177
+ case GLTF_COMPONENT_TYPE.UNSIGNED_INT:
178
+ case GLTF_COMPONENT_TYPE.FLOAT:
179
+ return 4;
180
+ default:
181
+ return 4;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Reads and unpacks a glTF accessor into a contiguous Float32Array or Uint32Array.
187
+ *
188
+ * Handles buffer offsets, custom byte strides, and component normalization.
189
+ *
190
+ * @param gltf The root glTF descriptor.
191
+ * @param buffers Array of resolved binary buffers.
192
+ * @param accessorIndex Zero-based accessor index.
193
+ * @returns Contiguous typed array of decoded values.
194
+ */
195
+ export function readAccessor(
196
+ gltf: GlTF,
197
+ buffers: Uint8Array[],
198
+ accessorIndex: number
199
+ ): Float32Array | Uint32Array | Uint16Array {
200
+ const accessor = gltf.accessors?.[accessorIndex];
201
+ if (!accessor) {
202
+ throw new Error(`Accessor index out of bounds: ${accessorIndex}`);
203
+ }
204
+
205
+ if (accessor.sparse) {
206
+ const sparse = accessor.sparse;
207
+ const base = { ...accessor, sparse: undefined };
208
+ const indices = {
209
+ bufferView: sparse.indices.bufferView,
210
+ byteOffset: sparse.indices.byteOffset,
211
+ componentType: sparse.indices.componentType,
212
+ count: sparse.count,
213
+ type: 'SCALAR' as const
214
+ };
215
+ const values = {
216
+ ...base,
217
+ bufferView: sparse.values.bufferView,
218
+ byteOffset: sparse.values.byteOffset,
219
+ count: sparse.count
220
+ };
221
+ const accessors = [...(gltf.accessors ?? []), base, indices, values];
222
+ const doc = { ...gltf, accessors };
223
+ const result = readAccessor(doc, buffers, accessors.length - 3);
224
+ const sparseIndices = readAccessor(doc, buffers, accessors.length - 2);
225
+ const sparseValues = readAccessor(doc, buffers, accessors.length - 1);
226
+ const components = getComponentCountForType(accessor.type);
227
+ for (let i = 0; i < sparse.count; i++) {
228
+ const index = sparseIndices[i]!;
229
+ if (index >= accessor.count) throw new Error('Sparse accessor index out of bounds');
230
+ for (let c = 0; c < components; c++)
231
+ result[index * components + c] = sparseValues[i * components + c]!;
232
+ }
233
+ return result;
234
+ }
235
+
236
+ const numComponents = getComponentCountForType(accessor.type);
237
+ const compSize = getComponentByteSize(accessor.componentType);
238
+ const elementSize = numComponents * compSize;
239
+ const count = accessor.count;
240
+
241
+ if (accessor.bufferView === undefined) {
242
+ // Unallocated/sparse accessor: return zeros
243
+ return new Float32Array(count * numComponents);
244
+ }
245
+
246
+ const bufferView = gltf.bufferViews?.[accessor.bufferView];
247
+ if (!bufferView) {
248
+ throw new Error(`BufferView index out of bounds: ${accessor.bufferView}`);
249
+ }
250
+
251
+ const buffer = buffers[bufferView.buffer];
252
+ if (!buffer) {
253
+ throw new Error(`Buffer index out of bounds: ${bufferView.buffer}`);
254
+ }
255
+
256
+ const baseOffset = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
257
+ const stride = bufferView.byteStride ?? elementSize;
258
+ const dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
259
+
260
+ // Fast path: contiguous floats
261
+ if (
262
+ accessor.componentType === GLTF_COMPONENT_TYPE.FLOAT &&
263
+ stride === elementSize &&
264
+ !accessor.normalized
265
+ ) {
266
+ const totalFloats = count * numComponents;
267
+ const byteStart = buffer.byteOffset + baseOffset;
268
+ return new Float32Array(buffer.buffer.slice(byteStart, byteStart + totalFloats * 4));
269
+ }
270
+
271
+ // Fast path: contiguous unsigned 16-bit integers
272
+ if (
273
+ accessor.componentType === GLTF_COMPONENT_TYPE.UNSIGNED_SHORT &&
274
+ stride === elementSize &&
275
+ !accessor.normalized
276
+ ) {
277
+ const totalShorts = count * numComponents;
278
+ const byteStart = buffer.byteOffset + baseOffset;
279
+ return new Uint16Array(buffer.buffer.slice(byteStart, byteStart + totalShorts * 2));
280
+ }
281
+
282
+ // Fast path: contiguous unsigned 32-bit integers
283
+ if (
284
+ accessor.componentType === GLTF_COMPONENT_TYPE.UNSIGNED_INT &&
285
+ stride === elementSize &&
286
+ !accessor.normalized
287
+ ) {
288
+ const totalInts = count * numComponents;
289
+ const byteStart = buffer.byteOffset + baseOffset;
290
+ return new Uint32Array(buffer.buffer.slice(byteStart, byteStart + totalInts * 4));
291
+ }
292
+
293
+ // Interleaved or custom type decoding
294
+ const outFloats = new Float32Array(count * numComponents);
295
+ let outIdx = 0;
296
+
297
+ for (let i = 0; i < count; i++) {
298
+ const elemOffset = baseOffset + i * stride;
299
+
300
+ for (let c = 0; c < numComponents; c++) {
301
+ const bytePos = elemOffset + c * compSize;
302
+ let val = 0;
303
+
304
+ switch (accessor.componentType) {
305
+ case GLTF_COMPONENT_TYPE.FLOAT:
306
+ val = dataView.getFloat32(bytePos, true);
307
+ break;
308
+ case GLTF_COMPONENT_TYPE.UNSIGNED_INT:
309
+ val = dataView.getUint32(bytePos, true);
310
+ break;
311
+ case GLTF_COMPONENT_TYPE.UNSIGNED_SHORT:
312
+ val = dataView.getUint16(bytePos, true);
313
+ if (accessor.normalized) val /= 65535;
314
+ break;
315
+ case GLTF_COMPONENT_TYPE.SHORT:
316
+ val = dataView.getInt16(bytePos, true);
317
+ if (accessor.normalized) val = Math.max(val / 32767, -1);
318
+ break;
319
+ case GLTF_COMPONENT_TYPE.UNSIGNED_BYTE:
320
+ val = dataView.getUint8(bytePos);
321
+ if (accessor.normalized) val /= 255;
322
+ break;
323
+ case GLTF_COMPONENT_TYPE.BYTE:
324
+ val = dataView.getInt8(bytePos);
325
+ if (accessor.normalized) val = Math.max(val / 127, -1);
326
+ break;
327
+ }
328
+
329
+ outFloats[outIdx++] = val;
330
+ }
331
+ }
332
+
333
+ return outFloats;
334
+ }