@jgengine/assets 0.11.0 → 0.13.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/CHANGELOG.md +52 -0
- package/README.md +2 -2
- package/dist/catalogs/build.d.ts +27 -0
- package/dist/catalogs/build.js +21 -2
- package/dist/cli/pull.js +7 -11
- package/dist/collisionMeshAssets.d.ts +2 -0
- package/dist/collisionMeshAssets.js +6 -0
- package/dist/dims.d.ts +14 -0
- package/dist/dims.js +175 -0
- package/dist/download.d.ts +26 -10
- package/dist/download.js +140 -43
- package/dist/generated/kaykit-adventurers.json +459 -37
- package/dist/generated/kaykit-city-builder.json +82 -41
- package/dist/generated/kaykit-dungeon.json +940 -203
- package/dist/generated/kaykit-furniture.json +106 -53
- package/dist/generated/kaykit-skeletons.json +426 -21
- package/dist/generated/kaykit-space-base.json +114 -57
- package/dist/generated/quaternius-medieval-village.json +352 -176
- package/dist/generated/quaternius-modular-scifi.json +405 -196
- package/dist/generated/quaternius-stylized-nature.json +136 -68
- package/dist/indexGen.d.ts +2 -2
- package/dist/indexGen.js +36 -5
- package/dist/manifest.d.ts +6 -0
- package/dist/registry-catalog.json +0 -5
- package/package.json +2 -2
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-assets/SKILL.md +8 -0
- package/skills/jgengine-assets/api.md +14 -4
- package/skills/jgengine-assets/capabilities.md +4 -0
- package/skills/jgengine/reference-cartridge.md +0 -72
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
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
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
|
-
|
|
283
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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);
|