@threepipe/plugin-blend-importer 0.0.9 → 0.3.0

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 (47) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +25 -1
  3. package/dist/BlendLoadPlugin.d.ts +63 -3
  4. package/dist/BlendLoadPlugin.d.ts.map +1 -1
  5. package/dist/decompress.d.ts +10 -0
  6. package/dist/decompress.d.ts.map +1 -0
  7. package/dist/dependencies.txt +31 -1
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1351 -89
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1349 -87
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/loader/camera.d.ts +1 -1
  15. package/dist/loader/ctx.d.ts +9 -1
  16. package/dist/loader/ctx.d.ts.map +1 -1
  17. package/dist/loader/geometry.d.ts +2 -2
  18. package/dist/loader/geometry.d.ts.map +1 -1
  19. package/dist/loader/index.d.ts +1 -1
  20. package/dist/loader/light.d.ts +1 -1
  21. package/dist/loader/material.d.ts +4 -1
  22. package/dist/loader/material.d.ts.map +1 -1
  23. package/dist/loader/mesh.d.ts +1 -1
  24. package/dist/loader/mesh.d.ts.map +1 -1
  25. package/docs/assets/highlight.css +49 -0
  26. package/docs/assets/icons.js +1 -1
  27. package/docs/assets/icons.svg +1 -1
  28. package/docs/assets/main.js +2 -2
  29. package/docs/assets/navigation.js +1 -1
  30. package/docs/assets/search.js +1 -1
  31. package/docs/assets/style.css +3 -3
  32. package/docs/classes/BlendLoadPlugin.html +6 -3
  33. package/docs/index.html +1 -1
  34. package/docs/interfaces/BlendFile.html +18 -0
  35. package/docs/interfaces/BlendLoadOptions.html +12 -0
  36. package/package.json +4 -3
  37. package/src/BlendLoadPlugin.ts +204 -13
  38. package/src/decompress.ts +59 -0
  39. package/src/index.ts +1 -0
  40. package/src/js-blend/main.js +4 -1
  41. package/src/js-blend/parser/parser.js +192 -52
  42. package/src/loader/ctx.ts +9 -0
  43. package/src/loader/geometry.ts +270 -13
  44. package/src/loader/material.ts +324 -8
  45. package/src/loader/mesh.ts +34 -8
  46. package/tsconfig.json +42 -0
  47. package/docs/.nojekyll +0 -1
@@ -64,6 +64,9 @@ function worker_code () {
64
64
  working_blend_file = null;
65
65
  finished_objects = [];
66
66
  current_SDNA_template = null;
67
+ // ERROR is module-global; without this reset a failed parse (e.g. an unsupported
68
+ // big-endian file) would be reported for the next, successful parse.
69
+ ERROR = null;
67
70
 
68
71
 
69
72
  // set data
@@ -122,6 +125,16 @@ function worker_code () {
122
125
  return pointerLow;
123
126
  }
124
127
  },
128
+
129
+ // Alignment-safe TypedArray read over the file buffer. Consumers (e.g. loader/geometry.ts)
130
+ // that reach into `__blender_file__.AB` at a datablock's `__data_address__` MUST use this
131
+ // instead of `new Ctor(AB, byteOffset, n)` — Blender packs blocks with no padding, so on
132
+ // v1 (17-byte header) and some v0 files the payload lands at an offset that isn't a multiple
133
+ // of the element size, where a direct view throws RangeError. All supported files are
134
+ // little-endian (big-endian is rejected at header parse), so a raw byte copy is correct.
135
+ readTypedArray: function (Ctor, byteOffset, length) {
136
+ return alignedTypedArray(Ctor, this.AB, byteOffset, length);
137
+ },
125
138
  };
126
139
 
127
140
  self.onmessage = parseFile;
@@ -132,11 +145,25 @@ function worker_code () {
132
145
  This allows the underlying binary data to be changed.
133
146
  */
134
147
 
148
+ // Construct a TypedArray view over the file buffer, copying into a fresh aligned buffer when
149
+ // the byte offset isn't a multiple of the element size. Blender packs blocks with no padding,
150
+ // so on v1 (17-byte header) and some v0 files the payload lands at unaligned offsets where a
151
+ // direct `new Float32Array(buf, oddOffset, n)` would throw. All supported files are
152
+ // little-endian (big-endian is rejected at header parse), so a raw byte copy is correct.
153
+ function alignedTypedArray (Ctor, buffer, byteOffset, length) {
154
+ if ((byteOffset % Ctor.BYTES_PER_ELEMENT) === 0) {
155
+ return new Ctor(buffer, byteOffset, length);
156
+ }
157
+ const out = new Ctor(length);
158
+ new Uint8Array(out.buffer).set(new Uint8Array(buffer, byteOffset, length * Ctor.BYTES_PER_ELEMENT));
159
+ return out;
160
+ }
161
+
135
162
  function float64Prop (offset, Blender_Array_Length, length) {
136
163
  return {
137
164
  get: function () {
138
165
  return (Blender_Array_Length > 1) ?
139
- new Float64Array(this.__blender_file__.AB, this.__data_address__ + offset, length) :
166
+ alignedTypedArray(Float64Array, this.__blender_file__.AB, this.__data_address__ + offset, length) :
140
167
  this.__blender_file__.dv.getFloat64(this.__data_address__ + offset, this.__blender_file__.template.endianess);
141
168
  },
142
169
  set: function (float) {
@@ -153,7 +180,7 @@ function worker_code () {
153
180
  return {
154
181
  get: function () {
155
182
  return (Blender_Array_Length > 1) ?
156
- new Float32Array(this.__blender_file__.AB, this.__data_address__ + offset, length) :
183
+ alignedTypedArray(Float32Array, this.__blender_file__.AB, this.__data_address__ + offset, length) :
157
184
  this.__blender_file__.dv.getFloat32(this.__data_address__ + offset, this.__blender_file__.template.endianess);
158
185
  },
159
186
  set: function (float) {
@@ -170,7 +197,7 @@ function worker_code () {
170
197
  return {
171
198
  get: function () {
172
199
  return (Blender_Array_Length > 1) ?
173
- new Int32Array(this.__blender_file__.AB, this.__data_address__ + offset, length) :
200
+ alignedTypedArray(Int32Array, this.__blender_file__.AB, this.__data_address__ + offset, length) :
174
201
  this.__blender_file__.dv.getInt32(this.__data_address__ + offset, this.__blender_file__.template.endianess);
175
202
  },
176
203
  set: function (float) {
@@ -187,7 +214,7 @@ function worker_code () {
187
214
  return {
188
215
  get: function () {
189
216
  return (Blender_Array_Length > 1) ?
190
- new Uint32Array(this.__blender_file__.AB, this.__data_address__ + offset, length) :
217
+ alignedTypedArray(Uint32Array, this.__blender_file__.AB, this.__data_address__ + offset, length) :
191
218
  this.__blender_file__.dv.getUint32(this.__data_address__ + offset, this.__blender_file__.template.endianess);
192
219
  },
193
220
  set: function (float) {
@@ -204,7 +231,7 @@ function worker_code () {
204
231
  return {
205
232
  get: function () {
206
233
  return (Blender_Array_Length > 1) ?
207
- new Int16Array(this.__blender_file__.AB, this.__data_address__ + offset, length) :
234
+ alignedTypedArray(Int16Array, this.__blender_file__.AB, this.__data_address__ + offset, length) :
208
235
  this.__blender_file__.dv.getInt16(this.__data_address__ + offset, this.__blender_file__.template.endianess);
209
236
  },
210
237
  set: function (float) {
@@ -221,7 +248,7 @@ function worker_code () {
221
248
  return {
222
249
  get: function () {
223
250
  return (Blender_Array_Length > 1) ?
224
- new Uint16Array(this.__blender_file__.AB, this.__data_address__ + offset, length) :
251
+ alignedTypedArray(Uint16Array, this.__blender_file__.AB, this.__data_address__ + offset, length) :
225
252
  this.__blender_file__.dv.getUint16(this.__data_address__ + offset, this.__blender_file__.template.endianess);
226
253
  },
227
254
  set: function (float) {
@@ -274,22 +301,38 @@ function worker_code () {
274
301
  function pointerProp2 (offset) {
275
302
  return {
276
303
  get: function () {
277
- let pointer = this.__blender_file__.getPointer(this.__data_address__ + offset, this.__blender_file__);
278
- const link = this.__blender_file__.memory_lookup[pointer];
304
+ const bf = this.__blender_file__;
305
+ let pointer = bf.getPointer(this.__data_address__ + offset, bf);
306
+ const link = bf.memory_lookup[pointer];
279
307
 
280
308
  const results = [];
281
309
 
282
310
  if (link) {
283
311
  const address = link.__data_address__;
284
- let j = 0;
285
- while (true) {
286
- pointer = this.__blender_file__.getPointer(address + j * 8, this.__blender_file__);
287
- let obj = this.__blender_file__.memory_lookup[pointer];
288
- if (!obj) break;
289
- results.push(obj);
290
- j++;
312
+ const ps = (bf.template && bf.template.pointer_size) || 8;
313
+ // The pointed-to block is a malloc'd array of `block_length / pointer_size` pointers
314
+ // (e.g. `Material **mat` is `totcol` pointers). Read the WHOLE array and keep null
315
+ // (unresolved) entries as null — Blender leaves empty/object-linked material slots as
316
+ // null pointers in the middle of the array, so breaking at the first null silently
317
+ // drops every slot after it (lost materials, invisible faces).
318
+ const count = (typeof link.__byte_length__ === 'number' && link.__byte_length__ > 0)
319
+ ? Math.floor(link.__byte_length__ / ps) : 0;
320
+ if (count > 0) {
321
+ for (let j = 0; j < count; j++) {
322
+ pointer = bf.getPointer(address + j * ps, bf);
323
+ results.push(bf.memory_lookup[pointer] || null);
324
+ }
325
+ } else {
326
+ // Fallback (no known block length): read until the first null, as before.
327
+ let j = 0;
328
+ while (true) {
329
+ pointer = bf.getPointer(address + j * ps, bf);
330
+ const obj = bf.memory_lookup[pointer];
331
+ if (!obj) break;
332
+ results.push(obj);
333
+ j++;
334
+ }
291
335
  }
292
-
293
336
  }
294
337
 
295
338
  return results;
@@ -327,6 +370,35 @@ function worker_code () {
327
370
  };
328
371
  }
329
372
 
373
+ // Signed 1-byte field (int8_t). Used widely in Blender 5.0 DNA (enum domains, flags, etc).
374
+ function int8Prop (offset, Blender_Array_Length, length) {
375
+ return {
376
+ get: function () {
377
+ return (Blender_Array_Length > 1) ?
378
+ alignedTypedArray(Int8Array, this.__blender_file__.AB, this.__data_address__ + offset, length) :
379
+ this.__blender_file__.dv.getInt8(this.__data_address__ + offset);
380
+ },
381
+ set: function (v) { if (Blender_Array_Length <= 1) this.__blender_file__.dv.setInt8(this.__data_address__ + offset, v); },
382
+ enumerable: true, configurable: true,
383
+ };
384
+ }
385
+
386
+ // 8-byte integer field (int64_t / uint64_t). Returned as a JS Number — .blend counts/sizes are
387
+ // well under 2^53. Used by Blender 5.0 DNA (AttributeArray.size, LargeBHead lengths, etc).
388
+ function int64Prop (offset, Blender_Array_Length, length, unsigned) {
389
+ return {
390
+ get: function () {
391
+ if (Blender_Array_Length > 1) {
392
+ return alignedTypedArray(unsigned ? BigUint64Array : BigInt64Array, this.__blender_file__.AB, this.__data_address__ + offset, length);
393
+ }
394
+ const dv = this.__blender_file__.dv, le = this.__blender_file__.template.endianess;
395
+ return Number(unsigned ? dv.getBigUint64(this.__data_address__ + offset, le) : dv.getBigInt64(this.__data_address__ + offset, le));
396
+ },
397
+ set: function () {},
398
+ enumerable: true, configurable: true,
399
+ };
400
+ }
401
+
330
402
  function compileProp (obj, name, type, offset, array_size, IS_POINTER, pointer_size, length) {
331
403
 
332
404
  if (!IS_POINTER) {
@@ -338,18 +410,34 @@ function worker_code () {
338
410
  Object.defineProperty(obj, name, floatProp(offset, array_size, length >> 2));
339
411
  break;
340
412
  case 'int':
413
+ case 'int32_t':
341
414
  Object.defineProperty(obj, name, intProp(offset, array_size, length >> 2));
342
415
  break;
416
+ case 'uint32_t':
417
+ Object.defineProperty(obj, name, uIntProp(offset, array_size, length >> 2));
418
+ break;
343
419
  case 'short':
420
+ case 'int16_t':
344
421
  Object.defineProperty(obj, name, shortProp(offset, array_size, length >> 1));
345
422
  break;
346
423
  case 'ushort':
424
+ case 'uint16_t':
347
425
  Object.defineProperty(obj, name, uShortProp(offset, array_size, length >> 1));
348
426
  break;
427
+ case 'int8_t':
428
+ Object.defineProperty(obj, name, int8Prop(offset, array_size, length));
429
+ break;
349
430
  case 'char':
350
431
  case 'uchar':
432
+ case 'uint8_t':
351
433
  Object.defineProperty(obj, name, charProp(offset, array_size, length));
352
434
  break;
435
+ case 'int64_t':
436
+ Object.defineProperty(obj, name, int64Prop(offset, array_size, length >> 3, false));
437
+ break;
438
+ case 'uint64_t':
439
+ Object.defineProperty(obj, name, int64Prop(offset, array_size, length >> 3, true));
440
+ break;
353
441
  default:
354
442
  // compile list to
355
443
  obj[name] = {};
@@ -530,6 +618,11 @@ function worker_code () {
530
618
  BLENDER_FILE.addObject(this);
531
619
 
532
620
  this.__blender_file__ = BLENDER_FILE;
621
+ // Byte length of this block's payload (data_block_length is the end offset). Lets
622
+ // consumers derive element counts for raw DATA blocks (e.g. poly_offset_indices) whose
623
+ // logical length isn't available elsewhere — needed for Blender 5.0 where Mesh.tot* are
624
+ // runtime (post-geometry-nodes) counts, not the stored array sizes.
625
+ this.__byte_length__ = data_block_length - _data_offset;
533
626
 
534
627
  const struct = this.__list__;
535
628
  let j = 0,
@@ -611,31 +704,76 @@ function worker_code () {
611
704
  struct_names = [];
612
705
  offset = 0;
613
706
 
614
- // todo gzip, zstd(28 b5 2f fd, https://projects.blender.org/archive/blender-file/issues/93858)
707
+ // Compressed (gzip/zstd) blend files are decompressed upstream in BlendLoadPlugin.loadAsync.
615
708
  // Make sure we have a .blend __blender_file__. All blend files have the first 12bytes
616
709
  // set with BLENDER-v### in Utf-8
617
710
  const magic = toString(_data, offset, 7)
618
711
  if (magic !== 'BLENDER') return ERROR = 'File supplied is not a .blend compatible Blender file.';
619
712
 
620
- // otherwise get templete from save version.
713
+ // Decode the file header. Two formats exist
714
+ // (.repos/blender/source/blender/blenloader_core/intern/blo_core_blend_header.cc):
715
+ // v0 (12 bytes): 'BLENDER' + ('_'|'-') + ('v'|'V') + 3-digit version
716
+ // '_' = 4-byte pointers (BHead4), '-' = 8-byte pointers (SmallBHead8)
717
+ // v1 (17 bytes, Blender 5.0+): 'BLENDER' + '17' + '-' + '01' + 'v' + 4-digit version
718
+ // always 8-byte pointers, little-endian, LargeBHead8 blocks.
719
+ // BIG_ENDIAN is misleadingly named — it is `true` for little-endian files (the value is
720
+ // passed straight to DataView getters as the `littleEndian` arg). Keep that convention.
721
+ let version, large_bhead = false
722
+ const fmtByte = toString(_data, 7, 8)
723
+ if (fmtByte === '_' || fmtByte === '-') {
724
+ pointer_size = fmtByte === '_' ? 4 : 8
725
+ BIG_ENDIAN = toString(_data, 8, 9) !== 'V'
726
+ version = toString(_data, 9, 12)
727
+ offset = 12
728
+ } else {
729
+ // New-style 17-byte header. Bytes 7-8 = header size ('17'), 9 = '-',
730
+ // 10-11 = file-format version ('01'), 12 = 'v', 13-16 = blender version.
731
+ const headerSize = parseInt(toString(_data, 7, 9), 10)
732
+ if (headerSize !== 17) return ERROR = 'Unsupported new-style blend header size: ' + headerSize;
733
+ pointer_size = 8
734
+ BIG_ENDIAN = true // v1 is always little-endian
735
+ large_bhead = true
736
+ version = toString(_data, 13, 17)
737
+ offset = headerSize
738
+ }
621
739
 
622
- offset += 7;
623
- pointer_size = ((toString(_data, offset++, offset)) == '_') ? 4 : 8;
624
- BIG_ENDIAN = toString(_data, offset++, offset) !== 'V';
625
- const version = toString(_data, offset, offset + 3);
740
+ if (!BIG_ENDIAN) {
741
+ // Big-endian .blend files (ancient PowerPC/SGI era, e.g. Blender < 2.0) need every field
742
+ // byte-swapped, which we don't do. Bail clearly instead of producing garbage.
743
+ return ERROR = 'Big-endian .blend files are not supported';
744
+ }
626
745
 
746
+ // Per-format block-header layout. BHead variants
747
+ // (.repos/blender/source/blender/blenloader_core/BLO_core_bhead.hh):
748
+ // BHead4 (ptr=4): code@0 len@4(i32) old@8(u32) SDNAnr@12 nr@16 — 20 bytes
749
+ // SmallBHead8 (v0): code@0 len@4(i32) old@8(u64) SDNAnr@16 nr@20 — 24 bytes
750
+ // LargeBHead8 (v1): code@0 SDNAnr@4 old@8(u64) len@16(i64) nr@24(i64) — 32 bytes
751
+ // For v0 these reduce to the original parametrised offsets, so v0 behaviour is unchanged.
752
+ const BLOCK_HDR = large_bhead ? 32 : (16 + pointer_size)
753
+ const SDNA_OFF = large_bhead ? 4 : (8 + pointer_size)
754
+ const LEN_OFF = large_bhead ? 16 : 4
755
+ const NR_OFF = large_bhead ? 24 : (12 + pointer_size)
756
+ const LEN_I64 = large_bhead
757
+ const NR_I64 = large_bhead
758
+ // Blender packs blocks with no inter-block padding (verified empirically + writefile.cc),
759
+ // so block offsets advance by exactly BLOCK_HDR + len. The v1 17-byte header (and some v0
760
+ // files) leave block payloads at offsets that aren't multiples of 4/8 — those are handled
761
+ // at the TypedArray getter level (alignedTypedArray) rather than by realigning the buffer.
762
+ const readLen = (o) => LEN_I64 ? Number(data.getBigInt64(o + LEN_OFF, true)) : data.getInt32(o + LEN_OFF, true)
763
+ const readSdna = (o) => data.getInt32(o + SDNA_OFF, BIG_ENDIAN)
764
+ const readNr = (o) => NR_I64 ? Number(data.getBigInt64(o + NR_OFF, true)) : data.getInt32(o + NR_OFF, BIG_ENDIAN)
627
765
 
628
766
  // create new master template if none exist for current blender version;
629
- if (!templates[version]) {
630
- templates[version] = new MASTER_SDNA_SCHEMA(version);
767
+ // Key on format + version so a v0 "050" and a v1 "0500" can never share a template.
768
+ const templateKey = (large_bhead ? 'v1:' : 'v0:') + version
769
+ if (!templates[templateKey]) {
770
+ templates[templateKey] = new MASTER_SDNA_SCHEMA(version);
631
771
  }
632
772
 
633
- current_SDNA_template = templates[version];
773
+ current_SDNA_template = templates[templateKey];
634
774
 
635
775
  FILE.template = current_SDNA_template;
636
776
 
637
- offset += 3;
638
-
639
777
  // Set SDNA structs if template hasn't been set.
640
778
  // Todo: Move the following block into the MASTER_SDNA_SCHEMA object.
641
779
  //* Like so:*/ current_SDNA_template.set(AB);
@@ -647,11 +785,20 @@ function worker_code () {
647
785
  offset2 = offset;
648
786
 
649
787
  while (true) {
650
- sdna_index = data.getInt32(offset2 + pointer_size + 8, BIG_ENDIAN);
788
+ // Bounds guard: the SDNA-search loop walks block headers looking for DNA1.
789
+ // If a previous block's block_length is wrong (happens on some Blender 4.x
790
+ // geometry-nodes files), offset2 lands past EOF and the getInt32 below throws
791
+ // RangeError synchronously, killing the whole load. Match the main block
792
+ // loop's guard pattern (line ~810).
793
+ if (offset2 + BLOCK_HDR > data.byteLength) {
794
+ ERROR = 'Unexpected end of file while searching for DNA1';
795
+ break
796
+ }
797
+ sdna_index = readSdna(offset2);
651
798
  // eslint-disable-next-line no-control-regex
652
799
  code = toString(_data, offset2, offset2 + 4).replace(/\u0000/g, '');
653
- block_length = data.getInt32(offset2 + 4, true);
654
- offset2 += 16 + (pointer_size);
800
+ block_length = readLen(offset2);
801
+ offset2 += BLOCK_HDR;
655
802
  if (code === 'DNA1') {
656
803
  // DNA found; This is the core of the __blender_file__ and contains all the structure for the various data types used in Blender.
657
804
  count = 0;
@@ -794,6 +941,9 @@ function worker_code () {
794
941
  // debugger
795
942
  // offset = (4 - (offset % 4)) + offset;
796
943
  // }
944
+ // Defensive: skip any stray NUL padding before a block header. Blender writes none
945
+ // between blocks (verified), so for correctly-decoded files this is a no-op; it only
946
+ // matters as a safety net against a mis-decoded length. Block codes are always A-Z.
797
947
  for (let j = 0; j < 8; j++) {
798
948
  if(data.getInt8(offset) === 0) {
799
949
  offset++
@@ -801,41 +951,31 @@ function worker_code () {
801
951
  }
802
952
  break
803
953
  }
804
- if(data.getInt8(offset) === 0) {
805
- debugger
806
- }
807
954
 
808
955
  data_offset = offset;
809
956
 
810
- if (offset + pointer_size + 12 >= data.byteLength) {
957
+ if (offset + BLOCK_HDR > data.byteLength) {
811
958
  ERROR = 'Unexpected end of file while parsing';
812
959
  break
813
960
  }
814
961
 
815
- sdna_index = data.getInt32(offset + pointer_size + 8, BIG_ENDIAN);
816
- // let code_uint = data.getUint32(offset, BIG_ENDIAN);
817
- const code_str = toString(_data, offset, offset + 4) // data.getUint32(offset, BIG_ENDIAN);
962
+ sdna_index = readSdna(offset);
963
+ const code_str = toString(_data, offset, offset + 4)
818
964
 
819
- offset2 = offset + 16 + (pointer_size);
820
- // console.log(code_str, code_str.length, data.getInt8(offset))
965
+ offset2 = offset + BLOCK_HDR;
821
966
 
822
- const blockLength = data.getInt32(offset + 4, true);
823
- if (blockLength < 0 || offset + blockLength + 16 + pointer_size > data.byteLength) {
967
+ const blockLength = readLen(offset);
968
+ if (blockLength < 0 || offset + blockLength + BLOCK_HDR > data.byteLength) {
824
969
  ERROR = 'Invalid block length detected';
825
970
  break
826
971
  }
827
- // if(blockLength === 1) {
828
- // debugger
829
- // }
830
-
831
- // last_offset = offset
832
972
 
833
- offset += blockLength + 16 + (pointer_size);
973
+ offset += blockLength + BLOCK_HDR;
834
974
 
835
975
  if (code_str === 'DNA1') {} // skip - already processed at this point
836
976
  else if (code_str === 'ENDB') break; // end of __blender_file__ found
837
977
  else if (code_str === 'TEST') { // snapshot
838
- const data_start = data_offset + pointer_size + 16;
978
+ const data_start = data_offset + BLOCK_HDR;
839
979
  const width = data.getInt32(data_start, BIG_ENDIAN);
840
980
  const height = data.getInt32(data_start + 4, BIG_ENDIAN);
841
981
  if ((width * height > 0)) {
@@ -844,7 +984,7 @@ function worker_code () {
844
984
  ERROR = 'Invalid TEST block length detected';
845
985
  break;
846
986
  }
847
- const image_data = new Uint32Array(_data, data_start + 8, data_len >> 2);
987
+ const image_data = alignedTypedArray(Uint32Array, _data, data_start + 8, data_len >> 2);
848
988
  const image = {
849
989
  width: width,
850
990
  height: height,
@@ -855,14 +995,14 @@ function worker_code () {
855
995
  }
856
996
  else {
857
997
  // Create a Blender object using a constructor template from current_SDNA_template
858
- const data_start = data_offset + pointer_size + 16;
998
+ const data_start = data_offset + BLOCK_HDR;
859
999
 
860
1000
  // Get a SDNA constructor by name;
861
1001
  const constructor = current_SDNA_template.getSDNAStructureConstructor(current_SDNA_template.SDNA_NAMES[sdna_index]);
862
1002
 
863
- const size = data.getInt32(data_offset + 4, BIG_ENDIAN);
1003
+ const size = readLen(data_offset);
864
1004
 
865
- count = data.getInt32(data_offset + 12 + pointer_size, BIG_ENDIAN);
1005
+ count = readNr(data_offset);
866
1006
 
867
1007
  if (count > 0 && constructor) {
868
1008
  let obj = new constructor();
package/src/loader/ctx.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  PerspectiveCamera,
12
12
  PointLight,
13
13
  SpotLight,
14
+ Texture,
14
15
  } from 'threepipe'
15
16
 
16
17
  export interface Ctx {
@@ -26,4 +27,12 @@ export interface Ctx {
26
27
  BufferAttribute: typeof BufferAttribute
27
28
  MeshPhysicalMaterial: typeof MeshPhysicalMaterial
28
29
  MeshBasicMaterial: typeof MeshBasicMaterial
30
+ /**
31
+ * Load an external (file-path) image referenced by a Blender Image datablock, resolving it
32
+ * relative to the `.blend`'s directory through threepipe's asset pipeline (cache + LoadingManager).
33
+ * Returns a Texture whose image fills in asynchronously (the load is awaited by the loader before
34
+ * the scene is returned). Returns `null` when unavailable (e.g. Node without a DOM). Provided by
35
+ * `BlendLoadPlugin`; absent when the loader is driven outside that plugin.
36
+ */
37
+ loadExternalTexture?: (blenderPath: string, srgb: boolean) => Texture | null
29
38
  }