@jgengine/assets 0.10.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/CHANGELOG.md +39 -2
- package/README.md +103 -50
- package/dist/aliases.d.ts +5 -0
- package/dist/aliases.js +28 -9
- package/dist/catalogs/build.d.ts +27 -0
- package/dist/catalogs/build.js +28 -2
- package/dist/catalogs/buildSprites.d.ts +10 -0
- package/dist/catalogs/buildSprites.js +15 -0
- package/dist/catalogs/starter.d.ts +5 -0
- package/dist/catalogs/starter.js +10 -1
- package/dist/cli/paths.d.ts +9 -1
- package/dist/cli/paths.js +11 -1
- package/dist/cli/pull.d.ts +12 -0
- package/dist/cli/pull.js +216 -25
- package/dist/collisionMeshAssets.d.ts +2 -0
- package/dist/collisionMeshAssets.js +6 -0
- package/dist/dims.d.ts +7 -0
- package/dist/dims.js +154 -0
- package/dist/download.d.ts +66 -7
- package/dist/download.js +264 -18
- package/dist/find.d.ts +20 -1
- package/dist/find.js +52 -2
- package/dist/generated/index.js +18 -6
- package/dist/generated/kaykit-adventurers.json +706 -0
- package/dist/generated/kaykit-city-builder.json +904 -0
- package/dist/generated/kaykit-dungeon.json +5002 -0
- package/dist/generated/kaykit-furniture.json +1168 -0
- package/dist/generated/kaykit-skeletons.json +410 -0
- package/dist/generated/kaykit-space-base.json +1199 -0
- package/dist/generated/quaternius-medieval-village.json +3698 -0
- package/dist/generated/quaternius-modular-scifi.json +4013 -0
- package/dist/generated/quaternius-stylized-nature.json +1498 -0
- package/dist/generated-sprites/index.d.ts +5 -0
- package/dist/generated-sprites/index.js +4 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +9 -2
- package/dist/indexGen.d.ts +2 -2
- package/dist/indexGen.js +34 -10
- package/dist/manifest.d.ts +16 -1
- package/dist/materials.d.ts +61 -0
- package/dist/materials.js +98 -0
- package/dist/packs/starter.d.ts +46 -0
- package/dist/packs/starter.js +248 -0
- package/dist/provisioning.d.ts +88 -0
- package/dist/provisioning.js +74 -0
- package/dist/registry-catalog.json +35 -0
- package/dist/snippet.d.ts +4 -0
- package/dist/snippet.js +27 -0
- package/dist/sources/ambientcg.d.ts +5 -0
- package/dist/sources/ambientcg.js +64 -0
- package/dist/sources/gameicons.d.ts +10 -0
- package/dist/sources/gameicons.js +21 -0
- package/dist/sources/index.d.ts +9 -2
- package/dist/sources/index.js +11 -3
- package/dist/sources/kaykit.js +55 -6
- package/dist/sources/quaternius.js +60 -9
- package/dist/spriteIndexGen.d.ts +17 -0
- package/dist/spriteIndexGen.js +108 -0
- package/dist/verify.d.ts +1 -0
- package/dist/verify.js +38 -1
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-assets/SKILL.md +45 -0
- package/skills/jgengine-assets/api.md +282 -0
- package/skills/jgengine-assets/capabilities.md +13 -0
- package/dist/generated/kenney-blaster.json +0 -362
- package/dist/generated/kenney-nature.json +0 -3292
- package/dist/generated/kenney-space.json +0 -1532
- package/dist/sources/kenney.d.ts +0 -2
- package/dist/sources/kenney.js +0 -36
- package/llms.txt +0 -1010
package/dist/download.js
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
import { unzipSync } from "fflate";
|
|
2
2
|
import { isScrapeDownload } from "./manifest.js";
|
|
3
|
+
/** Max size of a downloaded (still-compressed) archive, in bytes. Provider zips run tens of MB; this leaves headroom without buffering an unbounded response. */
|
|
4
|
+
export const MAX_ARCHIVE_DOWNLOAD_BYTES = 256 * 1024 * 1024;
|
|
5
|
+
/** Max total uncompressed size this module will inflate out of one archive, in bytes. */
|
|
6
|
+
export const MAX_ARCHIVE_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
|
|
7
|
+
/** Max number of entries this module will extract out of one archive. */
|
|
8
|
+
export const MAX_ARCHIVE_ENTRY_COUNT = 20_000;
|
|
9
|
+
/** Max allowed originalSize/size ratio for a single archive entry — past this it's treated as a zip bomb. */
|
|
10
|
+
export const MAX_ARCHIVE_COMPRESSION_RATIO = 100;
|
|
11
|
+
/** @internal */
|
|
12
|
+
export function boundedExtractFilter(matches) {
|
|
13
|
+
let totalUncompressedBytes = 0;
|
|
14
|
+
let entryCount = 0;
|
|
15
|
+
return (file) => {
|
|
16
|
+
if (!matches(file))
|
|
17
|
+
return false;
|
|
18
|
+
if (file.size > 0 && file.originalSize / file.size > MAX_ARCHIVE_COMPRESSION_RATIO) {
|
|
19
|
+
throw new Error(`archive entry "${file.name}" has a ${(file.originalSize / file.size).toFixed(0)}x compression ratio, exceeds the ${MAX_ARCHIVE_COMPRESSION_RATIO}x zip-bomb guard`);
|
|
20
|
+
}
|
|
21
|
+
totalUncompressedBytes += file.originalSize;
|
|
22
|
+
if (totalUncompressedBytes > MAX_ARCHIVE_UNCOMPRESSED_BYTES) {
|
|
23
|
+
throw new Error(`archive would extract more than the ${MAX_ARCHIVE_UNCOMPRESSED_BYTES} byte uncompressed-size cap`);
|
|
24
|
+
}
|
|
25
|
+
entryCount += 1;
|
|
26
|
+
if (entryCount > MAX_ARCHIVE_ENTRY_COUNT) {
|
|
27
|
+
throw new Error(`archive has more than the ${MAX_ARCHIVE_ENTRY_COUNT} entry cap`);
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
};
|
|
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
|
+
}
|
|
3
63
|
function resolveRelative(path, pageUrl) {
|
|
4
64
|
try {
|
|
5
65
|
return new URL(path, pageUrl).toString();
|
|
@@ -8,6 +68,7 @@ function resolveRelative(path, pageUrl) {
|
|
|
8
68
|
return path;
|
|
9
69
|
}
|
|
10
70
|
}
|
|
71
|
+
/** @internal */
|
|
11
72
|
export function findArchiveUrl(html, pageUrl) {
|
|
12
73
|
const matches = html.match(/[^\s"'()<>]+\.zip/gi);
|
|
13
74
|
if (matches === null)
|
|
@@ -26,6 +87,7 @@ function score(candidate) {
|
|
|
26
87
|
value += 1;
|
|
27
88
|
return value;
|
|
28
89
|
}
|
|
90
|
+
/** @internal */
|
|
29
91
|
export async function resolveArchiveUrl(source, fetchImpl = fetch) {
|
|
30
92
|
const download = source.download;
|
|
31
93
|
if (!isScrapeDownload(download))
|
|
@@ -40,17 +102,30 @@ export async function resolveArchiveUrl(source, fetchImpl = fetch) {
|
|
|
40
102
|
}
|
|
41
103
|
return url;
|
|
42
104
|
}
|
|
105
|
+
/** @internal */
|
|
43
106
|
export async function downloadArchive(url, fetchImpl = fetch) {
|
|
44
107
|
const response = await fetchImpl(url, { redirect: "follow" });
|
|
45
108
|
if (!response.ok)
|
|
46
109
|
throw new Error(`download ${url} -> HTTP ${response.status}`);
|
|
47
|
-
|
|
110
|
+
const declaredLength = response.headers.get("content-length");
|
|
111
|
+
if (declaredLength !== null) {
|
|
112
|
+
const declared = Number(declaredLength);
|
|
113
|
+
if (Number.isFinite(declared) && declared > MAX_ARCHIVE_DOWNLOAD_BYTES) {
|
|
114
|
+
throw new Error(`download ${url} declares ${declared} bytes, exceeds the ${MAX_ARCHIVE_DOWNLOAD_BYTES} byte cap`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
118
|
+
if (bytes.byteLength > MAX_ARCHIVE_DOWNLOAD_BYTES) {
|
|
119
|
+
throw new Error(`download ${url} is ${bytes.byteLength} bytes, exceeds the ${MAX_ARCHIVE_DOWNLOAD_BYTES} byte cap`);
|
|
120
|
+
}
|
|
121
|
+
return bytes;
|
|
48
122
|
}
|
|
49
123
|
/**
|
|
50
124
|
* Layout for the `--mirror` / `JGENGINE_ASSETS_MIRROR` base URL override: the
|
|
51
125
|
* archive for a pack is expected at `<baseUrl>/<provider>/<packId>.zip`, e.g.
|
|
52
126
|
* `https://my-mirror.example.com/kenney/kenney-nature.zip`.
|
|
53
|
-
|
|
127
|
+
* @internal
|
|
128
|
+
*/
|
|
54
129
|
export function mirrorOverrideUrl(baseUrl, source) {
|
|
55
130
|
const base = baseUrl.replace(/\/+$/, "");
|
|
56
131
|
return `${base}/${source.provider}/${source.id}.zip`;
|
|
@@ -66,7 +141,9 @@ export function mirrorOverrideUrl(baseUrl, source) {
|
|
|
66
141
|
* `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`.
|
|
67
142
|
*/
|
|
68
143
|
export const DEFAULT_RELEASE_BASE = "https://github.com/Noisemaker111/jgengine/releases/download/packs";
|
|
69
|
-
/** URL of `source`'s archive on the default GitHub-release mirror.
|
|
144
|
+
/** URL of `source`'s archive on the default GitHub-release mirror.
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
70
147
|
export function defaultReleaseUrl(source) {
|
|
71
148
|
return `${DEFAULT_RELEASE_BASE}/${source.provider}-${source.id}.zip`;
|
|
72
149
|
}
|
|
@@ -89,7 +166,8 @@ async function verifyPinnedSha(source, archive) {
|
|
|
89
166
|
* source supplied the bytes; a mismatch is treated as a failed attempt so the
|
|
90
167
|
* next source in the chain is tried. Throws with every attempted URL and its
|
|
91
168
|
* failure reason when all sources fail.
|
|
92
|
-
|
|
169
|
+
* @internal
|
|
170
|
+
*/
|
|
93
171
|
export async function downloadPackArchive(source, options = {}) {
|
|
94
172
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
95
173
|
const attempted = [];
|
|
@@ -135,10 +213,8 @@ export async function downloadPackArchive(source, options = {}) {
|
|
|
135
213
|
throw new Error(`failed to download ${source.id} from all ${attempted.length} source(s):\n` +
|
|
136
214
|
errors.map((line) => ` - ${line}`).join("\n"));
|
|
137
215
|
}
|
|
138
|
-
|
|
139
|
-
const entries = unzipSync(archive, {
|
|
140
|
-
filter: (file) => /\.glb$/i.test(file.name),
|
|
141
|
-
});
|
|
216
|
+
function dedupeByBasename(archive, pattern) {
|
|
217
|
+
const entries = unzipSync(archive, { filter: boundedExtractFilter((file) => pattern.test(file.name)) });
|
|
142
218
|
const byName = new Map();
|
|
143
219
|
for (const [path, bytes] of Object.entries(entries)) {
|
|
144
220
|
const base = path.split("/").pop();
|
|
@@ -149,22 +225,192 @@ export function extractGlbs(archive) {
|
|
|
149
225
|
}
|
|
150
226
|
return Array.from(byName, ([file, bytes]) => ({ file, bytes })).sort((a, b) => a.file.localeCompare(b.file));
|
|
151
227
|
}
|
|
152
|
-
|
|
153
|
-
|
|
228
|
+
function buildGlb(jsonText, binBytes) {
|
|
229
|
+
const jsonPad = (4 - (jsonText.length % 4)) % 4;
|
|
230
|
+
const jsonChunk = new Uint8Array(jsonText.length + jsonPad);
|
|
231
|
+
new TextEncoder().encodeInto(jsonText, jsonChunk);
|
|
232
|
+
for (let i = jsonText.length; i < jsonChunk.length; i++)
|
|
233
|
+
jsonChunk[i] = 0x20;
|
|
234
|
+
const binPad = binBytes === undefined ? 0 : (4 - (binBytes.byteLength % 4)) % 4;
|
|
235
|
+
const binChunkLen = binBytes === undefined ? 0 : binBytes.byteLength + binPad;
|
|
236
|
+
const total = 12 +
|
|
237
|
+
8 +
|
|
238
|
+
jsonChunk.byteLength +
|
|
239
|
+
(binBytes === undefined ? 0 : 8 + binChunkLen);
|
|
240
|
+
const out = new Uint8Array(total);
|
|
241
|
+
const view = new DataView(out.buffer);
|
|
242
|
+
view.setUint32(0, 0x46546c67, true); // glTF
|
|
243
|
+
view.setUint32(4, 2, true);
|
|
244
|
+
view.setUint32(8, total, true);
|
|
245
|
+
let o = 12;
|
|
246
|
+
view.setUint32(o, jsonChunk.byteLength, true);
|
|
247
|
+
view.setUint32(o + 4, 0x4e4f534a, true); // JSON
|
|
248
|
+
out.set(jsonChunk, o + 8);
|
|
249
|
+
o += 8 + jsonChunk.byteLength;
|
|
250
|
+
if (binBytes !== undefined) {
|
|
251
|
+
view.setUint32(o, binChunkLen, true);
|
|
252
|
+
view.setUint32(o + 4, 0x004e4942, true); // BIN
|
|
253
|
+
out.set(binBytes, o + 8);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
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
|
+
}
|
|
314
|
+
/**
|
|
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).
|
|
342
|
+
* @internal
|
|
343
|
+
*/
|
|
344
|
+
export function extractGlbs(archive) {
|
|
154
345
|
const entries = unzipSync(archive, {
|
|
155
|
-
filter: (file) =>
|
|
346
|
+
filter: boundedExtractFilter((file) => /\.(glb|gltf|bin)$/i.test(file.name) || PACK_IMAGE_EXT.test(file.name)),
|
|
156
347
|
});
|
|
157
|
-
const
|
|
348
|
+
const byDirBase = new Map();
|
|
349
|
+
const imagesByBase = new Map();
|
|
158
350
|
for (const [path, bytes] of Object.entries(entries)) {
|
|
159
|
-
const
|
|
160
|
-
|
|
351
|
+
const parts = path.replace(/\\/g, "/").split("/");
|
|
352
|
+
const file = parts.pop();
|
|
353
|
+
if (file === undefined || file.length === 0)
|
|
354
|
+
continue;
|
|
355
|
+
if (PACK_IMAGE_EXT.test(file)) {
|
|
356
|
+
if (!imagesByBase.has(file))
|
|
357
|
+
imagesByBase.set(file, bytes);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const dir = parts.join("/");
|
|
361
|
+
// KayKit ships some files as `name.gltf.glb` — strip both suffixes for a clean id.
|
|
362
|
+
const base = file.replace(/\.gltf\.glb$/i, "").replace(/\.(glb|gltf|bin)$/i, "");
|
|
363
|
+
if (base.length === 0 || base === file)
|
|
364
|
+
continue;
|
|
365
|
+
const extMatch = file.match(/\.(glb|gltf|bin)$/i);
|
|
366
|
+
if (extMatch === null)
|
|
367
|
+
continue;
|
|
368
|
+
const ext = extMatch[1].toLowerCase();
|
|
369
|
+
const key = `${dir}\0${base}`;
|
|
370
|
+
const slot = byDirBase.get(key) ?? {};
|
|
371
|
+
if (ext === "glb")
|
|
372
|
+
slot.glb = bytes;
|
|
373
|
+
else if (ext === "gltf")
|
|
374
|
+
slot.gltf = bytes;
|
|
375
|
+
else if (ext === "bin")
|
|
376
|
+
slot.bin = bytes;
|
|
377
|
+
byDirBase.set(key, slot);
|
|
378
|
+
}
|
|
379
|
+
const byName = new Map();
|
|
380
|
+
const referenced = new Set();
|
|
381
|
+
for (const [key, slot] of byDirBase) {
|
|
382
|
+
const base = key.split("\0")[1];
|
|
383
|
+
const file = `${base}.glb`;
|
|
384
|
+
if (byName.has(file))
|
|
161
385
|
continue;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
386
|
+
if (slot.glb !== undefined) {
|
|
387
|
+
const normalized = normalizeGlbImageUris(slot.glb);
|
|
388
|
+
byName.set(file, normalized.bytes);
|
|
389
|
+
for (const image of normalized.referenced)
|
|
390
|
+
referenced.add(image);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (slot.gltf !== undefined) {
|
|
394
|
+
const packed = packGltfToGlb(slot.gltf, slot.bin);
|
|
395
|
+
byName.set(file, packed);
|
|
396
|
+
for (const image of externalGlbImages(packed))
|
|
397
|
+
referenced.add(image);
|
|
398
|
+
}
|
|
165
399
|
}
|
|
166
|
-
|
|
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 };
|
|
406
|
+
}
|
|
407
|
+
/** Pulls every SVG/PNG out of a sprite/icon-pack archive, deduped by basename regardless of nesting depth.
|
|
408
|
+
* @internal
|
|
409
|
+
*/
|
|
410
|
+
export function extractSpriteFiles(archive) {
|
|
411
|
+
return dedupeByBasename(archive, /\.(svg|png)$/i);
|
|
167
412
|
}
|
|
413
|
+
/** @internal */
|
|
168
414
|
export async function sha256Hex(bytes) {
|
|
169
415
|
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
170
416
|
new Uint8Array(buffer).set(bytes);
|
package/dist/find.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AssetKind = "model" | "pack" | "component" | "icon";
|
|
1
|
+
export type AssetKind = "model" | "pack" | "material" | "component" | "icon" | "sprite" | "spritePack";
|
|
2
2
|
export type AssetMatch = {
|
|
3
3
|
kind: "model";
|
|
4
4
|
id: string;
|
|
@@ -10,6 +10,11 @@ export type AssetMatch = {
|
|
|
10
10
|
source: string;
|
|
11
11
|
title: string;
|
|
12
12
|
categories: readonly string[];
|
|
13
|
+
} | {
|
|
14
|
+
kind: "material";
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
categories: readonly string[];
|
|
13
18
|
} | {
|
|
14
19
|
kind: "component";
|
|
15
20
|
name: string;
|
|
@@ -18,6 +23,20 @@ export type AssetMatch = {
|
|
|
18
23
|
} | {
|
|
19
24
|
kind: "icon";
|
|
20
25
|
name: string;
|
|
26
|
+
}
|
|
27
|
+
/** One already-indexed file inside a pulled sprite/icon pack, e.g. `gameicons-icons/sword`. */
|
|
28
|
+
| {
|
|
29
|
+
kind: "sprite";
|
|
30
|
+
id: string;
|
|
31
|
+
source: string;
|
|
32
|
+
via: "index";
|
|
33
|
+
}
|
|
34
|
+
/** A whole sprite/icon pack, not yet resolved to individual files — same role as `pack` for models. */
|
|
35
|
+
| {
|
|
36
|
+
kind: "spritePack";
|
|
37
|
+
source: string;
|
|
38
|
+
title: string;
|
|
39
|
+
categories: readonly string[];
|
|
21
40
|
};
|
|
22
41
|
export interface FindOptions {
|
|
23
42
|
/** Restrict results to one kind. */
|
package/dist/find.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { aliases } from "./aliases.js";
|
|
2
2
|
import { generatedIndex } from "./generated/index.js";
|
|
3
|
+
import { generatedSpriteIndex } from "./generated-sprites/index.js";
|
|
3
4
|
import { keyFromFile } from "./indexGen.js";
|
|
5
|
+
import { materialAliases } from "./materials.js";
|
|
4
6
|
import { registryCatalog } from "./registry.js";
|
|
5
7
|
import { singles } from "./singles.js";
|
|
6
|
-
import {
|
|
8
|
+
import { materialSources, modelSources, spriteSources } from "./sources/index.js";
|
|
9
|
+
import { keyFromSpriteFile } from "./spriteIndexGen.js";
|
|
7
10
|
function normalize(value) {
|
|
8
11
|
return value.toLowerCase().replace(/[\s_/-]+/g, " ").trim();
|
|
9
12
|
}
|
|
@@ -92,7 +95,7 @@ export function rankAssets(query, options = {}) {
|
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
if (options.kind === undefined || options.kind === "pack") {
|
|
95
|
-
for (const source of
|
|
98
|
+
for (const source of modelSources) {
|
|
96
99
|
push(best(q, source.id, source.title, source.provider, ...source.categories), {
|
|
97
100
|
kind: "pack",
|
|
98
101
|
source: source.id,
|
|
@@ -101,6 +104,27 @@ export function rankAssets(query, options = {}) {
|
|
|
101
104
|
});
|
|
102
105
|
}
|
|
103
106
|
}
|
|
107
|
+
if (options.kind === undefined || options.kind === "material") {
|
|
108
|
+
for (const source of materialSources) {
|
|
109
|
+
push(best(q, source.id, source.title, ...source.categories), {
|
|
110
|
+
kind: "material",
|
|
111
|
+
id: source.id,
|
|
112
|
+
title: source.title,
|
|
113
|
+
categories: source.categories,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
for (const alias of materialAliases) {
|
|
117
|
+
const target = materialSources.find((source) => source.id === alias.target);
|
|
118
|
+
if (target === undefined)
|
|
119
|
+
continue;
|
|
120
|
+
push(best(q, alias.key, alias.target), {
|
|
121
|
+
kind: "material",
|
|
122
|
+
id: target.id,
|
|
123
|
+
title: target.title,
|
|
124
|
+
categories: target.categories,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
104
128
|
if (options.kind === undefined || options.kind === "component") {
|
|
105
129
|
for (const component of registryCatalog.components) {
|
|
106
130
|
push(best(q, component.name, component.title, component.description), {
|
|
@@ -116,6 +140,26 @@ export function rankAssets(query, options = {}) {
|
|
|
116
140
|
push(best(q, icon), { kind: "icon", name: icon });
|
|
117
141
|
}
|
|
118
142
|
}
|
|
143
|
+
if (options.kind === undefined || options.kind === "sprite") {
|
|
144
|
+
for (const entry of generatedSpriteIndex) {
|
|
145
|
+
push(best(q, entry.id, keyFromSpriteFile(entry.file), ...entry.categories), {
|
|
146
|
+
kind: "sprite",
|
|
147
|
+
id: entry.id,
|
|
148
|
+
source: entry.source,
|
|
149
|
+
via: "index",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (options.kind === undefined || options.kind === "spritePack") {
|
|
154
|
+
for (const source of spriteSources) {
|
|
155
|
+
push(best(q, source.id, source.title, source.provider, ...source.categories), {
|
|
156
|
+
kind: "spritePack",
|
|
157
|
+
source: source.id,
|
|
158
|
+
title: source.title,
|
|
159
|
+
categories: source.categories,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
119
163
|
ranked.sort((a, b) => b.score - a.score || matchKey(a.match).localeCompare(matchKey(b.match)));
|
|
120
164
|
return dedupe(ranked).slice(0, options.limit ?? 12);
|
|
121
165
|
}
|
|
@@ -125,6 +169,12 @@ function matchKey(match) {
|
|
|
125
169
|
return `model:${match.id}`;
|
|
126
170
|
case "pack":
|
|
127
171
|
return `pack:${match.source}`;
|
|
172
|
+
case "material":
|
|
173
|
+
return `material:${match.id}`;
|
|
174
|
+
case "sprite":
|
|
175
|
+
return `sprite:${match.id}`;
|
|
176
|
+
case "spritePack":
|
|
177
|
+
return `spritePack:${match.source}`;
|
|
128
178
|
default:
|
|
129
179
|
return `${match.kind}:${match.name}`;
|
|
130
180
|
}
|
package/dist/generated/index.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import s_kaykit_adventurers from "./kaykit-adventurers.json" with { type: "json" };
|
|
2
|
+
import s_kaykit_city_builder from "./kaykit-city-builder.json" with { type: "json" };
|
|
3
|
+
import s_kaykit_dungeon from "./kaykit-dungeon.json" with { type: "json" };
|
|
4
|
+
import s_kaykit_furniture from "./kaykit-furniture.json" with { type: "json" };
|
|
5
|
+
import s_kaykit_skeletons from "./kaykit-skeletons.json" with { type: "json" };
|
|
6
|
+
import s_kaykit_space_base from "./kaykit-space-base.json" with { type: "json" };
|
|
7
|
+
import s_quaternius_medieval_village from "./quaternius-medieval-village.json" with { type: "json" };
|
|
8
|
+
import s_quaternius_modular_scifi from "./quaternius-modular-scifi.json" with { type: "json" };
|
|
9
|
+
import s_quaternius_stylized_nature from "./quaternius-stylized-nature.json" with { type: "json" };
|
|
4
10
|
export const generatedBySource = {
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
11
|
+
"kaykit-adventurers": s_kaykit_adventurers,
|
|
12
|
+
"kaykit-city-builder": s_kaykit_city_builder,
|
|
13
|
+
"kaykit-dungeon": s_kaykit_dungeon,
|
|
14
|
+
"kaykit-furniture": s_kaykit_furniture,
|
|
15
|
+
"kaykit-skeletons": s_kaykit_skeletons,
|
|
16
|
+
"kaykit-space-base": s_kaykit_space_base,
|
|
17
|
+
"quaternius-medieval-village": s_quaternius_medieval_village,
|
|
18
|
+
"quaternius-modular-scifi": s_quaternius_modular_scifi,
|
|
19
|
+
"quaternius-stylized-nature": s_quaternius_stylized_nature,
|
|
8
20
|
};
|
|
9
21
|
export const generatedIndex = Object.values(generatedBySource).flat();
|