@jgengine/assets 0.11.0 → 0.12.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.
package/dist/download.js CHANGED
@@ -29,6 +29,37 @@ export function boundedExtractFilter(matches) {
29
29
  return true;
30
30
  };
31
31
  }
32
+ const PACK_IMAGE_EXT = /\.(png|jpe?g|webp)$/i;
33
+ /** Flatten a glTF image URI ("../Textures/color%20map.png") to its decoded basename. */
34
+ function imageUriBasename(uri) {
35
+ const decoded = (() => {
36
+ try {
37
+ return decodeURIComponent(uri);
38
+ }
39
+ catch {
40
+ return uri;
41
+ }
42
+ })();
43
+ const clean = decoded.replace(/\\/g, "/");
44
+ return clean.split("/").pop() ?? clean;
45
+ }
46
+ /**
47
+ * Rewrite external image URIs to flat basenames so a model always finds its
48
+ * texture beside itself in the pack dir, no matter how the source archive
49
+ * nested it ("Textures/x.png", "../texture/x.png", "x.png" all become "x.png").
50
+ * Returns the basenames of every external image the model references.
51
+ */
52
+ function normalizeGltfImageUris(json) {
53
+ const referenced = [];
54
+ for (const image of json.images ?? []) {
55
+ if (image.uri === undefined || image.uri.startsWith("data:"))
56
+ continue;
57
+ const base = imageUriBasename(image.uri);
58
+ image.uri = base;
59
+ referenced.push(base);
60
+ }
61
+ return referenced;
62
+ }
32
63
  function resolveRelative(path, pageUrl) {
33
64
  try {
34
65
  return new URL(path, pageUrl).toString();
@@ -194,23 +225,7 @@ function dedupeByBasename(archive, pattern) {
194
225
  }
195
226
  return Array.from(byName, ([file, bytes]) => ({ file, bytes })).sort((a, b) => a.file.localeCompare(b.file));
196
227
  }
197
- /**
198
- * Pack a `.gltf` + optional external `.bin` into a single `.glb` so reindex /
199
- * the shell only ever deal in one-file models. Strips buffer `uri` fields so
200
- * the binary chunk is self-contained. External image URIs stay as-is (loaders
201
- * resolve relative to the catalog URL only for GLB-embedded images).
202
- * @internal
203
- */
204
- export function packGltfToGlb(gltfBytes, binBytes) {
205
- const json = JSON.parse(new TextDecoder().decode(gltfBytes));
206
- if (Array.isArray(json.buffers)) {
207
- for (const buffer of json.buffers) {
208
- delete buffer.uri;
209
- if (binBytes !== undefined)
210
- buffer.byteLength = binBytes.byteLength;
211
- }
212
- }
213
- const jsonText = JSON.stringify(json);
228
+ function buildGlb(jsonText, binBytes) {
214
229
  const jsonPad = (4 - (jsonText.length % 4)) % 4;
215
230
  const jsonChunk = new Uint8Array(jsonText.length + jsonPad);
216
231
  new TextEncoder().encodeInto(jsonText, jsonChunk);
@@ -239,22 +254,109 @@ export function packGltfToGlb(gltfBytes, binBytes) {
239
254
  }
240
255
  return out;
241
256
  }
257
+ /** Parse a `.glb` container into its JSON document and optional BIN chunk. Returns null for non-glb bytes. */
258
+ function parseGlb(bytes) {
259
+ if (bytes.byteLength < 20)
260
+ return null;
261
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
262
+ if (view.getUint32(0, true) !== 0x46546c67)
263
+ return null;
264
+ let offset = 12;
265
+ let json;
266
+ let bin;
267
+ while (offset + 8 <= bytes.byteLength) {
268
+ const length = view.getUint32(offset, true);
269
+ const kind = view.getUint32(offset + 4, true);
270
+ const body = bytes.subarray(offset + 8, offset + 8 + length);
271
+ if (kind === 0x4e4f534a) {
272
+ try {
273
+ json = JSON.parse(new TextDecoder().decode(body));
274
+ }
275
+ catch {
276
+ return null;
277
+ }
278
+ }
279
+ else if (kind === 0x004e4942) {
280
+ bin = body;
281
+ }
282
+ offset += 8 + length;
283
+ }
284
+ if (json === undefined)
285
+ return null;
286
+ return bin === undefined ? { json } : { json, bin };
287
+ }
288
+ /**
289
+ * External image basenames a packed `.glb` still references (its textures must
290
+ * sit beside it in the pack dir). Empty for self-contained models.
291
+ * @internal
292
+ */
293
+ export function externalGlbImages(bytes) {
294
+ const chunks = parseGlb(bytes);
295
+ if (chunks === null)
296
+ return [];
297
+ return (chunks.json.images ?? [])
298
+ .map((image) => image.uri)
299
+ .filter((uri) => uri !== undefined && !uri.startsWith("data:"))
300
+ .map((uri) => imageUriBasename(uri));
301
+ }
302
+ /** Rewrite a native `.glb`'s external image URIs to flat basenames; returns the input untouched when nothing changes. */
303
+ function normalizeGlbImageUris(bytes) {
304
+ const chunks = parseGlb(bytes);
305
+ if (chunks === null)
306
+ return { bytes, referenced: [] };
307
+ const before = JSON.stringify(chunks.json.images ?? []);
308
+ const referenced = normalizeGltfImageUris(chunks.json);
309
+ if (referenced.length === 0 || JSON.stringify(chunks.json.images ?? []) === before) {
310
+ return { bytes, referenced };
311
+ }
312
+ return { bytes: buildGlb(JSON.stringify(chunks.json), chunks.bin), referenced };
313
+ }
242
314
  /**
243
- * Pull every GLB out of an archive. Also converts co-located `.gltf` + `.bin`
244
- * pairs (Quaternius Standard packs on OpenGameArt) into `.glb` so the catalog
245
- * stays one-file-per-model. GLB wins when both formats share a basename.
315
+ * Pack a `.gltf` + optional external `.bin` into a single `.glb` so reindex /
316
+ * the shell only ever deal in one-file models. Strips buffer `uri` fields so
317
+ * the binary chunk is self-contained; external image URIs are flattened to
318
+ * basenames so the pack ships each referenced texture beside the models
319
+ * (loaders resolve the relative URI against the model's own URL).
320
+ * @internal
321
+ */
322
+ export function packGltfToGlb(gltfBytes, binBytes) {
323
+ const json = JSON.parse(new TextDecoder().decode(gltfBytes));
324
+ if (Array.isArray(json.buffers)) {
325
+ for (const buffer of json.buffers) {
326
+ delete buffer.uri;
327
+ if (binBytes !== undefined)
328
+ buffer.byteLength = binBytes.byteLength;
329
+ }
330
+ }
331
+ normalizeGltfImageUris(json);
332
+ return buildGlb(JSON.stringify(json), binBytes);
333
+ }
334
+ /**
335
+ * Pull every GLB out of an archive, plus the textures those models reference.
336
+ * Converts co-located `.gltf` + `.bin` pairs (Quaternius Standard packs on
337
+ * OpenGameArt, KayKit bits packs) into `.glb` so the catalog stays
338
+ * one-file-per-model; GLB wins when both formats share a basename. External
339
+ * image URIs are flattened to basenames and the matching archive images are
340
+ * returned for writing beside the models — a pulled pack renders textured, not
341
+ * as white clay (#1005).
246
342
  * @internal
247
343
  */
248
344
  export function extractGlbs(archive) {
249
345
  const entries = unzipSync(archive, {
250
- filter: boundedExtractFilter((file) => /\.(glb|gltf|bin)$/i.test(file.name)),
346
+ filter: boundedExtractFilter((file) => /\.(glb|gltf|bin)$/i.test(file.name) || PACK_IMAGE_EXT.test(file.name)),
251
347
  });
252
348
  const byDirBase = new Map();
349
+ const imagesByBase = new Map();
253
350
  for (const [path, bytes] of Object.entries(entries)) {
254
351
  const parts = path.replace(/\\/g, "/").split("/");
255
352
  const file = parts.pop();
256
353
  if (file === undefined || file.length === 0)
257
354
  continue;
355
+ if (PACK_IMAGE_EXT.test(file)) {
356
+ if (!imagesByBase.has(file))
357
+ imagesByBase.set(file, bytes);
358
+ continue;
359
+ }
258
360
  const dir = parts.join("/");
259
361
  // KayKit ships some files as `name.gltf.glb` — strip both suffixes for a clean id.
260
362
  const base = file.replace(/\.gltf\.glb$/i, "").replace(/\.(glb|gltf|bin)$/i, "");
@@ -275,20 +377,32 @@ export function extractGlbs(archive) {
275
377
  byDirBase.set(key, slot);
276
378
  }
277
379
  const byName = new Map();
380
+ const referenced = new Set();
278
381
  for (const [key, slot] of byDirBase) {
279
382
  const base = key.split("\0")[1];
280
383
  const file = `${base}.glb`;
384
+ if (byName.has(file))
385
+ continue;
281
386
  if (slot.glb !== undefined) {
282
- if (!byName.has(file))
283
- byName.set(file, slot.glb);
387
+ const normalized = normalizeGlbImageUris(slot.glb);
388
+ byName.set(file, normalized.bytes);
389
+ for (const image of normalized.referenced)
390
+ referenced.add(image);
284
391
  continue;
285
392
  }
286
393
  if (slot.gltf !== undefined) {
287
- if (!byName.has(file))
288
- byName.set(file, packGltfToGlb(slot.gltf, slot.bin));
394
+ const packed = packGltfToGlb(slot.gltf, slot.bin);
395
+ byName.set(file, packed);
396
+ for (const image of externalGlbImages(packed))
397
+ referenced.add(image);
289
398
  }
290
399
  }
291
- return Array.from(byName, ([file, bytes]) => ({ file, bytes })).sort((a, b) => a.file.localeCompare(b.file));
400
+ const models = Array.from(byName, ([file, bytes]) => ({ file, bytes })).sort((a, b) => a.file.localeCompare(b.file));
401
+ const images = Array.from(referenced)
402
+ .filter((file) => imagesByBase.has(file))
403
+ .map((file) => ({ file, bytes: imagesByBase.get(file) }))
404
+ .sort((a, b) => a.file.localeCompare(b.file));
405
+ return { models, images };
292
406
  }
293
407
  /** Pulls every SVG/PNG out of a sprite/icon-pack archive, deduped by basename regardless of nesting depth.
294
408
  * @internal
@@ -296,23 +410,6 @@ export function extractGlbs(archive) {
296
410
  export function extractSpriteFiles(archive) {
297
411
  return dedupeByBasename(archive, /\.(svg|png)$/i);
298
412
  }
299
- const TEXTURE_ENTRY = /(?:^|\/)(Textures\/[^/]+)$/i;
300
- /** @internal */
301
- export function extractTextures(archive) {
302
- const entries = unzipSync(archive, {
303
- filter: boundedExtractFilter((file) => TEXTURE_ENTRY.test(file.name)),
304
- });
305
- const byRel = new Map();
306
- for (const [path, bytes] of Object.entries(entries)) {
307
- const match = path.match(TEXTURE_ENTRY);
308
- if (match === null)
309
- continue;
310
- const rel = match[1];
311
- if (!byRel.has(rel))
312
- byRel.set(rel, bytes);
313
- }
314
- return Array.from(byRel, ([file, bytes]) => ({ file, bytes })).sort((a, b) => a.file.localeCompare(b.file));
315
- }
316
413
  /** @internal */
317
414
  export async function sha256Hex(bytes) {
318
415
  const buffer = new ArrayBuffer(bytes.byteLength);
@@ -17,7 +17,8 @@
17
17
  "x": -0.004841230809688568,
18
18
  "z": 0.010057404637336731
19
19
  },
20
- "minY": -0.32719185948371887
20
+ "minY": -0.32719185948371887,
21
+ "maxY": 0.40435054898262024
21
22
  }
22
23
  },
23
24
  {
@@ -38,7 +39,8 @@
38
39
  "x": 0.0000034403055906295776,
39
40
  "z": 0.016935043036937714
40
41
  },
41
- "minY": -0.38292860984802246
42
+ "minY": -0.38292860984802246,
43
+ "maxY": 0.36561787128448486
42
44
  }
43
45
  },
44
46
  {
@@ -59,7 +61,8 @@
59
61
  "x": -0.23169700056314468,
60
62
  "z": 5.736947059631348e-7
61
63
  },
62
- "minY": -0.27304908633232117
64
+ "minY": -0.27304908633232117,
65
+ "maxY": 0.9713736176490784
63
66
  }
64
67
  },
65
68
  {
@@ -80,7 +83,8 @@
80
83
  "x": 0,
81
84
  "z": 0.0000011622905731201172
82
85
  },
83
- "minY": -0.43298184871673584
86
+ "minY": -0.43298184871673584,
87
+ "maxY": 1.2916465997695923
84
88
  }
85
89
  },
86
90
  {
@@ -101,7 +105,8 @@
101
105
  "x": -0.015458569234047537,
102
106
  "z": 0.3998250197287797
103
107
  },
104
- "minY": -0.000026527046429691836
108
+ "minY": -0.000026527046429691836,
109
+ "maxY": 2.397774257224249
105
110
  }
106
111
  },
107
112
  {
@@ -122,7 +127,8 @@
122
127
  "x": 0.0012482255697250366,
123
128
  "z": 0.33325251936912537
124
129
  },
125
- "minY": -0.17123575508594513
130
+ "minY": -0.17123575508594513,
131
+ "maxY": 0.23463644087314606
126
132
  }
127
133
  },
128
134
  {
@@ -143,7 +149,8 @@
143
149
  "x": 0.00001755356788635254,
144
150
  "z": 0.442745566368103
145
151
  },
146
- "minY": -0.17123575508594513
152
+ "minY": -0.17123575508594513,
153
+ "maxY": 0.30800661444664
147
154
  }
148
155
  },
149
156
  {
@@ -164,7 +171,8 @@
164
171
  "x": 0.009482886642217636,
165
172
  "z": 7.450580596923828e-9
166
173
  },
167
- "minY": -0.22489459812641144
174
+ "minY": -0.22489459812641144,
175
+ "maxY": 0.9809512495994568
168
176
  }
169
177
  },
170
178
  {
@@ -185,7 +193,8 @@
185
193
  "x": 0.04200405905131843,
186
194
  "z": 0.7091565314913972
187
195
  },
188
- "minY": -0.000026527046429691836
196
+ "minY": -0.000026527046429691836,
197
+ "maxY": 2.4665521719403785
189
198
  }
190
199
  },
191
200
  {
@@ -206,7 +215,8 @@
206
215
  "x": 0.08435849880093571,
207
216
  "z": -0.018152036592924414
208
217
  },
209
- "minY": -0.000026527046429691836
218
+ "minY": -0.000026527046429691836,
219
+ "maxY": 3.0028017151609516
210
220
  }
211
221
  },
212
222
  {
@@ -227,7 +237,8 @@
227
237
  "x": 0.06913799792528152,
228
238
  "z": -2.3096799850463867e-7
229
239
  },
230
- "minY": -0.23884634673595428
240
+ "minY": -0.23884634673595428,
241
+ "maxY": 0.2479029893875122
231
242
  }
232
243
  },
233
244
  {
@@ -248,7 +259,8 @@
248
259
  "x": 0.06913799792528152,
249
260
  "z": -2.3096799850463867e-7
250
261
  },
251
- "minY": -0.23884634673595428
262
+ "minY": -0.23884634673595428,
263
+ "maxY": 0.3095194399356842
252
264
  }
253
265
  },
254
266
  {
@@ -269,7 +281,8 @@
269
281
  "x": 0,
270
282
  "z": 0.024237211793661118
271
283
  },
272
- "minY": -0.5085819363594055
284
+ "minY": -0.5085819363594055,
285
+ "maxY": 0.4207805097103119
273
286
  }
274
287
  },
275
288
  {
@@ -290,7 +303,8 @@
290
303
  "x": -0.4691928529064687,
291
304
  "z": 0.20674141575997773
292
305
  },
293
- "minY": -0.000026527046429691836
306
+ "minY": -0.000026527046429691836,
307
+ "maxY": 2.2512404918670654
294
308
  }
295
309
  },
296
310
  {
@@ -311,7 +325,8 @@
311
325
  "x": -0.4691928529064687,
312
326
  "z": 0.24231619467442933
313
327
  },
314
- "minY": -0.000026527046429691836
328
+ "minY": -0.000026527046429691836,
329
+ "maxY": 2.186978340148926
315
330
  }
316
331
  },
317
332
  {
@@ -332,7 +347,8 @@
332
347
  "x": 0.0000029653310775756836,
333
348
  "z": -0.02182820811867714
334
349
  },
335
- "minY": -0.5401484966278076
350
+ "minY": -0.5401484966278076,
351
+ "maxY": 0.4814440906047821
336
352
  }
337
353
  },
338
354
  {
@@ -353,7 +369,8 @@
353
369
  "x": 0.0000029653310775756836,
354
370
  "z": -0.02182820811867714
355
371
  },
356
- "minY": -0.5401484966278076
372
+ "minY": -0.5401484966278076,
373
+ "maxY": 0.4814440906047821
357
374
  }
358
375
  },
359
376
  {
@@ -374,7 +391,8 @@
374
391
  "x": 0.0000028312206268310547,
375
392
  "z": 0.02120475471019745
376
393
  },
377
- "minY": -0.44131818413734436
394
+ "minY": -0.44131818413734436,
395
+ "maxY": 0.44128894805908203
378
396
  }
379
397
  },
380
398
  {
@@ -395,7 +413,8 @@
395
413
  "x": 0.0000028312206268310547,
396
414
  "z": 0.02120475471019745
397
415
  },
398
- "minY": -0.44131818413734436
416
+ "minY": -0.44131818413734436,
417
+ "maxY": 0.44128894805908203
399
418
  }
400
419
  },
401
420
  {
@@ -416,7 +435,8 @@
416
435
  "x": -0.0000028312206268310547,
417
436
  "z": 0.02120475471019745
418
437
  },
419
- "minY": -0.44131818413734436
438
+ "minY": -0.44131818413734436,
439
+ "maxY": 0.44128894805908203
420
440
  }
421
441
  },
422
442
  {
@@ -437,7 +457,8 @@
437
457
  "x": 0.0000028312206268310547,
438
458
  "z": 0.08512479811906815
439
459
  },
440
- "minY": -0.555518388748169
460
+ "minY": -0.555518388748169,
461
+ "maxY": 0.4814440906047821
441
462
  }
442
463
  },
443
464
  {
@@ -458,7 +479,8 @@
458
479
  "x": 0.0000028312206268310547,
459
480
  "z": 0.08512479811906815
460
481
  },
461
- "minY": -0.555518388748169
482
+ "minY": -0.555518388748169,
483
+ "maxY": 0.4814440906047821
462
484
  }
463
485
  },
464
486
  {
@@ -479,7 +501,8 @@
479
501
  "x": -7.450580596923828e-8,
480
502
  "z": 0.005772128701210022
481
503
  },
482
- "minY": -0.5964440107345581
504
+ "minY": -0.5964440107345581,
505
+ "maxY": 0.5964441299438477
483
506
  }
484
507
  },
485
508
  {
@@ -500,7 +523,8 @@
500
523
  "x": -7.450580596923828e-8,
501
524
  "z": 0.005772128701210022
502
525
  },
503
- "minY": -0.5964440107345581
526
+ "minY": -0.5964440107345581,
527
+ "maxY": 0.5964441299438477
504
528
  }
505
529
  },
506
530
  {
@@ -521,7 +545,8 @@
521
545
  "x": 0.0000010728836059570312,
522
546
  "z": -0.0000025480985641479492
523
547
  },
524
- "minY": -0.17134758830070496
548
+ "minY": -0.17134758830070496,
549
+ "maxY": 0.2517341077327728
525
550
  }
526
551
  },
527
552
  {
@@ -542,7 +567,8 @@
542
567
  "x": 0,
543
568
  "z": 0.0021018609404563904
544
569
  },
545
- "minY": -0.28749969601631165
570
+ "minY": -0.28749969601631165,
571
+ "maxY": 0.28750017285346985
546
572
  }
547
573
  },
548
574
  {
@@ -563,7 +589,8 @@
563
589
  "x": 0,
564
590
  "z": 0.023397032171487808
565
591
  },
566
- "minY": -0.2844379246234894
592
+ "minY": -0.2844379246234894,
593
+ "maxY": 0.28441160917282104
567
594
  }
568
595
  },
569
596
  {
@@ -584,7 +611,8 @@
584
611
  "x": 0.042737267911434174,
585
612
  "z": -1.4901161193847656e-8
586
613
  },
587
- "minY": -0.9004455804824829
614
+ "minY": -0.9004455804824829,
615
+ "maxY": 1.2542541027069092
588
616
  }
589
617
  },
590
618
  {
@@ -605,7 +633,8 @@
605
633
  "x": 0.0000036209821701049805,
606
634
  "z": -1.1175870895385742e-8
607
635
  },
608
- "minY": -0.36576569080352783
636
+ "minY": -0.36576569080352783,
637
+ "maxY": 1.4094958305358887
609
638
  }
610
639
  },
611
640
  {
@@ -626,7 +655,8 @@
626
655
  "x": 0.0000035315752029418945,
627
656
  "z": -7.078051567077637e-8
628
657
  },
629
- "minY": -0.4013613760471344
658
+ "minY": -0.4013613760471344,
659
+ "maxY": 1.9644392728805542
630
660
  }
631
661
  },
632
662
  {
@@ -647,7 +677,8 @@
647
677
  "x": 0.0000035315752029418945,
648
678
  "z": -7.078051567077637e-8
649
679
  },
650
- "minY": -0.4013613760471344
680
+ "minY": -0.4013613760471344,
681
+ "maxY": 1.9644392728805542
651
682
  }
652
683
  },
653
684
  {
@@ -668,7 +699,8 @@
668
699
  "x": -0.000029809772968292236,
669
700
  "z": 1.4901161193847656e-8
670
701
  },
671
- "minY": -0.26569339632987976
702
+ "minY": -0.26569339632987976,
703
+ "maxY": 0.7004468441009521
672
704
  }
673
705
  }
674
706
  ]