pixelkiln 0.37.0 → 0.38.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/README.md +1 -1
- package/dist/cli.js +148 -25
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +126 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -1
- package/dist/index.d.ts +72 -1
- package/dist/index.js +121 -11
- package/dist/index.js.map +1 -1
- package/docs/ARTIFACTS.md +18 -2
- package/docs/CLI.md +23 -1
- package/docs/COMFYUI.md +4 -1
- package/docs/LIBRARY.md +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ provenance and no long-lived npm token.
|
|
|
59
59
|
| Shared-account safety | Cross-project claim files or a registered workspace catalog, sibling-style exclusion, reviewed salvage, keep/discard tags, separate confirmed purge. |
|
|
60
60
|
| Quality control | Palette snapping on download, native grid recovery, named approval, regression baselines, and fail-closed packaging. |
|
|
61
61
|
| Sprite packaging | Deterministic RGBA packing, stable-cell mounting, explicit external input lists, structural output roles. |
|
|
62
|
-
| Engine export |
|
|
62
|
+
| Engine export | Aseprite sheet JSON and Godot SpriteFrames from `pack`; Tiled Wang sets and Godot terrain sets from `export`. |
|
|
63
63
|
| Artifact integrity | Portable source/output hashes, canonical fingerprints, manual-edit protection, transactional promotion, crash journal recovery. |
|
|
64
64
|
| Library/extension | Public TypeScript primitives, provider capability interface, and deterministic `FakeProvider`. |
|
|
65
65
|
|
package/dist/cli.js
CHANGED
|
@@ -336,8 +336,9 @@ function parseArgs(argv) {
|
|
|
336
336
|
}
|
|
337
337
|
const manifest = get("--manifest") ?? "pixelkiln.manifest.json";
|
|
338
338
|
const rawFormat = get("--format");
|
|
339
|
-
|
|
340
|
-
|
|
339
|
+
const formats = command === "export" ? ["generic", "tiled", "godot"] : ["generic", "aseprite", "godot"];
|
|
340
|
+
if (rawFormat && !formats.includes(rawFormat)) {
|
|
341
|
+
throw new UsageError(`--format must be ${formats.slice(0, -1).join(", ")}, or ${formats.at(-1)}, got "${rawFormat}"`);
|
|
341
342
|
}
|
|
342
343
|
const numberOption = (flag, opts) => {
|
|
343
344
|
const raw = get(flag);
|
|
@@ -465,7 +466,8 @@ Options
|
|
|
465
466
|
--columns <n> pack/export: sprites or tiles per row (default: near-square)
|
|
466
467
|
--port <n> Local review/gallery server port (default: choose a free port)
|
|
467
468
|
--inputs <path> pack/quality snapshot: JSON input list; needs --out
|
|
468
|
-
--format <format> export: generic (default), tiled, or godot
|
|
469
|
+
--format <format> export: generic (default), tiled, or godot (TileSet)
|
|
470
|
+
pack/mount: generic (default), aseprite, or godot (SpriteFrames)
|
|
469
471
|
--output-role <r> pack: include only this output role (repeatable)
|
|
470
472
|
--primary-only pack: include only unambiguous primary/single outputs
|
|
471
473
|
--max-distance <n> audit: maximum palette distance
|
|
@@ -522,6 +524,7 @@ Examples
|
|
|
522
524
|
pixelkiln adopt --tag
|
|
523
525
|
pixelkiln pack --style heybud-premium
|
|
524
526
|
pixelkiln pack --inputs sprites.json --out dist/sheet # no manifest needed
|
|
527
|
+
pixelkiln pack --style hero --format aseprite # sheet JSON engines load
|
|
525
528
|
pixelkiln mount --style ground
|
|
526
529
|
pixelkiln export --style ground --only terrain --format tiled
|
|
527
530
|
pixelkiln refine --style environment
|
|
@@ -1903,6 +1906,15 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
|
|
|
1903
1906
|
sha256: ""
|
|
1904
1907
|
}];
|
|
1905
1908
|
}
|
|
1909
|
+
function frameSetFps(entry) {
|
|
1910
|
+
const namespace = entry.providerMetadata?.[entry.provider];
|
|
1911
|
+
const frameSet = namespace?.frameSet;
|
|
1912
|
+
if (frameSet && typeof frameSet === "object" && "fps" in frameSet) {
|
|
1913
|
+
const fps = frameSet.fps;
|
|
1914
|
+
if (typeof fps === "number" && Number.isFinite(fps) && fps > 0) return fps;
|
|
1915
|
+
}
|
|
1916
|
+
return null;
|
|
1917
|
+
}
|
|
1906
1918
|
|
|
1907
1919
|
// src/pipeline/quality-profile.ts
|
|
1908
1920
|
import { existsSync as existsSync3 } from "fs";
|
|
@@ -9174,16 +9186,7 @@ async function samePixels(a, b) {
|
|
|
9174
9186
|
return false;
|
|
9175
9187
|
}
|
|
9176
9188
|
}
|
|
9177
|
-
|
|
9178
|
-
if (!entry) return null;
|
|
9179
|
-
const namespace = entry.providerMetadata?.[entry.provider];
|
|
9180
|
-
const frameSet = namespace?.frameSet;
|
|
9181
|
-
if (frameSet && typeof frameSet === "object" && "fps" in frameSet) {
|
|
9182
|
-
const fps = frameSet.fps;
|
|
9183
|
-
if (typeof fps === "number" && Number.isFinite(fps) && fps > 0) return fps;
|
|
9184
|
-
}
|
|
9185
|
-
return null;
|
|
9186
|
-
}
|
|
9189
|
+
var metadataFps = (entry) => entry ? frameSetFps(entry) : null;
|
|
9187
9190
|
async function fileInfo(absolutePath) {
|
|
9188
9191
|
try {
|
|
9189
9192
|
const info = await stat2(absolutePath);
|
|
@@ -12716,6 +12719,7 @@ import { readFile as readFile23 } from "fs/promises";
|
|
|
12716
12719
|
// src/pipeline/pack.ts
|
|
12717
12720
|
import { readFileSync as readFileSync5 } from "fs";
|
|
12718
12721
|
import path35 from "path";
|
|
12722
|
+
var DEFAULT_FRAME_SET_FPS = 12;
|
|
12719
12723
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
12720
12724
|
if (!Array.isArray(raw) || !raw.length) {
|
|
12721
12725
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
@@ -12815,6 +12819,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
12815
12819
|
const inputs = [];
|
|
12816
12820
|
const noOutput = [];
|
|
12817
12821
|
const locked = /* @__PURE__ */ new Set();
|
|
12822
|
+
const sets = [];
|
|
12818
12823
|
for (const [key, entry] of entries) {
|
|
12819
12824
|
const id = key.slice(prefix.length);
|
|
12820
12825
|
locked.add(id);
|
|
@@ -12842,6 +12847,15 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
12842
12847
|
continue;
|
|
12843
12848
|
}
|
|
12844
12849
|
for (const output of selected) inputs.push({ id: output.id, path: output.absolutePath });
|
|
12850
|
+
if (entry.outputs.length > 1) {
|
|
12851
|
+
const fps = entry.generator === "frames" ? frameSetFps(entry) ?? DEFAULT_FRAME_SET_FPS : null;
|
|
12852
|
+
sets.push({
|
|
12853
|
+
id,
|
|
12854
|
+
kind: entry.generator === "frames" ? "frames" : "members",
|
|
12855
|
+
...fps ? { fps } : {},
|
|
12856
|
+
frames: selected.map((output) => output.id)
|
|
12857
|
+
});
|
|
12858
|
+
}
|
|
12845
12859
|
}
|
|
12846
12860
|
for (const [id, source] of Object.entries(sources)) {
|
|
12847
12861
|
if (!locked.has(id)) inputs.push({ id, path: path35.resolve(manifestDir, source) });
|
|
@@ -12850,9 +12864,11 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
12850
12864
|
throw new Error(`No readable sprites for style "${styleId}" \u2014 ${noOutput.length} skipped.`);
|
|
12851
12865
|
}
|
|
12852
12866
|
const packed = packSprites(inputs, options);
|
|
12867
|
+
const packedIds = new Set(packed.atlas.frames.map((frame) => frame.id));
|
|
12868
|
+
const placed = sets.map((set) => ({ ...set, frames: set.frames.filter((frameId) => packedIds.has(frameId)) })).filter((set) => set.frames.length).sort((a, b) => a.id.localeCompare(b.id));
|
|
12853
12869
|
return {
|
|
12854
12870
|
...packed,
|
|
12855
|
-
atlas: { ...packed.atlas, style: styleId },
|
|
12871
|
+
atlas: { ...packed.atlas, style: styleId, ...placed.length ? { sets: placed } : {} },
|
|
12856
12872
|
skipped: [...noOutput, ...packed.skipped]
|
|
12857
12873
|
};
|
|
12858
12874
|
}
|
|
@@ -13261,6 +13277,98 @@ function isRecord2(value) {
|
|
|
13261
13277
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13262
13278
|
}
|
|
13263
13279
|
|
|
13280
|
+
// src/pipeline/sheet-formats.ts
|
|
13281
|
+
var SHEET_FORMATS = ["generic", "aseprite", "godot"];
|
|
13282
|
+
var ASEPRITE_DEFAULT_DURATION_MS = 100;
|
|
13283
|
+
var GODOT_DEFAULT_SPEED = 5;
|
|
13284
|
+
function renderAsepriteSheet(atlas, opts) {
|
|
13285
|
+
const index = new Map(atlas.frames.map((frame, i) => [frame.id, i]));
|
|
13286
|
+
const durations = /* @__PURE__ */ new Map();
|
|
13287
|
+
const frameTags = [];
|
|
13288
|
+
for (const set of atlas.sets ?? []) {
|
|
13289
|
+
const positions = set.frames.map((id) => index.get(id)).filter((i) => i !== void 0);
|
|
13290
|
+
if (!positions.length) continue;
|
|
13291
|
+
if (set.kind === "frames" && set.fps) {
|
|
13292
|
+
for (const id of set.frames) durations.set(id, Math.max(1, Math.round(1e3 / set.fps)));
|
|
13293
|
+
}
|
|
13294
|
+
frameTags.push({ name: set.id, from: Math.min(...positions), to: Math.max(...positions), direction: "forward" });
|
|
13295
|
+
}
|
|
13296
|
+
const frames = {};
|
|
13297
|
+
for (const frame of atlas.frames) {
|
|
13298
|
+
frames[frame.id] = {
|
|
13299
|
+
frame: { x: frame.x, y: frame.y, w: frame.width, h: frame.height },
|
|
13300
|
+
rotated: false,
|
|
13301
|
+
trimmed: false,
|
|
13302
|
+
spriteSourceSize: { x: 0, y: 0, w: frame.width, h: frame.height },
|
|
13303
|
+
sourceSize: { w: frame.width, h: frame.height },
|
|
13304
|
+
duration: durations.get(frame.id) ?? ASEPRITE_DEFAULT_DURATION_MS
|
|
13305
|
+
};
|
|
13306
|
+
}
|
|
13307
|
+
const document = {
|
|
13308
|
+
frames,
|
|
13309
|
+
meta: {
|
|
13310
|
+
app: "https://pixelkiln.griffen.codes",
|
|
13311
|
+
version: opts.version ?? "pixelkiln",
|
|
13312
|
+
image: opts.imageName,
|
|
13313
|
+
format: "RGBA8888",
|
|
13314
|
+
size: { w: atlas.sheet.width, h: atlas.sheet.height },
|
|
13315
|
+
scale: "1",
|
|
13316
|
+
frameTags,
|
|
13317
|
+
layers: [{ name: atlas.style, opacity: 255, blendMode: "normal" }],
|
|
13318
|
+
slices: []
|
|
13319
|
+
}
|
|
13320
|
+
};
|
|
13321
|
+
return JSON.stringify(document, null, 2) + "\n";
|
|
13322
|
+
}
|
|
13323
|
+
function renderGodotSpriteFrames(atlas, opts) {
|
|
13324
|
+
const textureIds = /* @__PURE__ */ new Map();
|
|
13325
|
+
atlas.frames.forEach((frame, i) => textureIds.set(frame.id, `AtlasTexture_${i + 1}`));
|
|
13326
|
+
const inSet = new Set((atlas.sets ?? []).flatMap((set) => set.frames));
|
|
13327
|
+
const lines = [
|
|
13328
|
+
`[gd_resource type="SpriteFrames" load_steps=${atlas.frames.length + 2} format=3]`,
|
|
13329
|
+
"",
|
|
13330
|
+
`[ext_resource type="Texture2D" path=${JSON.stringify(`./${opts.imageName}`)} id="1_texture"]`
|
|
13331
|
+
];
|
|
13332
|
+
for (const frame of atlas.frames) {
|
|
13333
|
+
lines.push(
|
|
13334
|
+
"",
|
|
13335
|
+
`[sub_resource type="AtlasTexture" id="${textureIds.get(frame.id)}"]`,
|
|
13336
|
+
`atlas = ExtResource("1_texture")`,
|
|
13337
|
+
`region = Rect2(${frame.x}, ${frame.y}, ${frame.width}, ${frame.height})`
|
|
13338
|
+
);
|
|
13339
|
+
}
|
|
13340
|
+
const animations = [];
|
|
13341
|
+
const animation = (name, ids, speed, loop) => {
|
|
13342
|
+
const frames = ids.filter((id) => textureIds.has(id)).map((id) => `{
|
|
13343
|
+
"duration": 1.0,
|
|
13344
|
+
"texture": SubResource("${textureIds.get(id)}")
|
|
13345
|
+
}`);
|
|
13346
|
+
if (!frames.length) return;
|
|
13347
|
+
animations.push(
|
|
13348
|
+
`{
|
|
13349
|
+
"frames": [${frames.join(", ")}],
|
|
13350
|
+
"loop": ${loop},
|
|
13351
|
+
"name": &${JSON.stringify(name)},
|
|
13352
|
+
"speed": ${speed.toFixed(1)}
|
|
13353
|
+
}`
|
|
13354
|
+
);
|
|
13355
|
+
};
|
|
13356
|
+
for (const set of atlas.sets ?? []) {
|
|
13357
|
+
if (set.kind === "frames") animation(set.id, set.frames, set.fps ?? GODOT_DEFAULT_SPEED, true);
|
|
13358
|
+
else for (const id of set.frames) animation(id, [id], GODOT_DEFAULT_SPEED, false);
|
|
13359
|
+
}
|
|
13360
|
+
for (const frame of atlas.frames) {
|
|
13361
|
+
if (!inSet.has(frame.id)) animation(frame.id, [frame.id], GODOT_DEFAULT_SPEED, false);
|
|
13362
|
+
}
|
|
13363
|
+
lines.push("", "[resource]", `animations = [${animations.join(", ")}]`, "");
|
|
13364
|
+
return lines.join("\n");
|
|
13365
|
+
}
|
|
13366
|
+
function renderSheetDocument(format, atlas, opts) {
|
|
13367
|
+
if (format === "aseprite") return { extension: ".json", document: renderAsepriteSheet(atlas, opts) };
|
|
13368
|
+
if (format === "godot") return { extension: ".tres", document: renderGodotSpriteFrames(atlas, opts) };
|
|
13369
|
+
return { extension: ".json", document: JSON.stringify(atlas, null, 2) + "\n" };
|
|
13370
|
+
}
|
|
13371
|
+
|
|
13264
13372
|
// src/cli/commands/pack.ts
|
|
13265
13373
|
async function runPack(args) {
|
|
13266
13374
|
if (args.inputs) {
|
|
@@ -13271,20 +13379,22 @@ async function runPack(args) {
|
|
|
13271
13379
|
const raw = JSON.parse(await readFile23(path36.resolve(args.inputs), "utf8"));
|
|
13272
13380
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
13273
13381
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
13274
|
-
const
|
|
13382
|
+
const format = sheetFormat(args);
|
|
13383
|
+
const base = path36.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
|
|
13384
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path36.basename(`${base}.png`) });
|
|
13275
13385
|
const outputs = [
|
|
13276
13386
|
{ path: `${base}.png`, data: png },
|
|
13277
|
-
{ path: `${base}
|
|
13387
|
+
{ path: `${base}${extension}`, data: document }
|
|
13278
13388
|
];
|
|
13279
13389
|
await writeManagedArtifactBundle(`${base}.pixelkiln.json`, outputs, {
|
|
13280
13390
|
kind: "pack",
|
|
13281
13391
|
sources: [await provenanceFile("$inputs", args.inputs), ...sources],
|
|
13282
|
-
options: { columns: args.columns ?? null, order: "id", style: null }
|
|
13392
|
+
options: { columns: args.columns ?? null, format, order: "id", style: null }
|
|
13283
13393
|
}, { force: args.force });
|
|
13284
13394
|
log(
|
|
13285
|
-
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB`
|
|
13395
|
+
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB (${format})`
|
|
13286
13396
|
);
|
|
13287
|
-
log(` ${path36.relative(process.cwd(), base)}.png +
|
|
13397
|
+
log(` ${path36.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
13288
13398
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
13289
13399
|
return;
|
|
13290
13400
|
}
|
|
@@ -13305,10 +13415,12 @@ async function runPack(args) {
|
|
|
13305
13415
|
sources: manifestSources(loaded.manifest, styleId)
|
|
13306
13416
|
});
|
|
13307
13417
|
const style = loaded.manifest.styles[styleId];
|
|
13308
|
-
const base = args.out ? path36.resolve(args.out.replace(/\.png
|
|
13418
|
+
const base = args.out ? path36.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path36.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
|
|
13419
|
+
const format = sheetFormat(args);
|
|
13420
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path36.basename(`${base}.png`) });
|
|
13309
13421
|
const outputs = [
|
|
13310
13422
|
{ path: `${base}.png`, data: png },
|
|
13311
|
-
{ path: `${base}
|
|
13423
|
+
{ path: `${base}${extension}`, data: document }
|
|
13312
13424
|
];
|
|
13313
13425
|
const qualityRecords = qualitySources ? await Promise.all(
|
|
13314
13426
|
packagingSpecs.filter((spec) => spec.quality).map((spec) => provenanceFile(
|
|
@@ -13326,6 +13438,7 @@ async function runPack(args) {
|
|
|
13326
13438
|
],
|
|
13327
13439
|
options: {
|
|
13328
13440
|
columns: args.columns ?? null,
|
|
13441
|
+
format,
|
|
13329
13442
|
order: "id",
|
|
13330
13443
|
outputRoles: [...args.outputRoles].sort(),
|
|
13331
13444
|
primaryOnly: args.primaryOnly,
|
|
@@ -13333,9 +13446,9 @@ async function runPack(args) {
|
|
|
13333
13446
|
}
|
|
13334
13447
|
}, { force: args.force });
|
|
13335
13448
|
log(
|
|
13336
|
-
` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
|
|
13449
|
+
` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)` + (atlas.sets?.length ? `, ${atlas.sets.length} set(s)` : "") + ` (${format})`
|
|
13337
13450
|
);
|
|
13338
|
-
log(` ${path36.relative(process.cwd(), base)}.png +
|
|
13451
|
+
log(` ${path36.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
13339
13452
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
13340
13453
|
}
|
|
13341
13454
|
}
|
|
@@ -13381,11 +13494,13 @@ async function runMount(args) {
|
|
|
13381
13494
|
outputRoles
|
|
13382
13495
|
);
|
|
13383
13496
|
const out = path36.resolve(manifestDir, style.mount.out);
|
|
13384
|
-
const
|
|
13497
|
+
const format = sheetFormat(args);
|
|
13498
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path36.basename(out) });
|
|
13499
|
+
const metadata = out.replace(/\.png$/, "") + extension;
|
|
13385
13500
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
13386
13501
|
const outputs = [
|
|
13387
13502
|
{ path: out, data: png },
|
|
13388
|
-
{ path: metadata, data:
|
|
13503
|
+
{ path: metadata, data: document }
|
|
13389
13504
|
];
|
|
13390
13505
|
const qualityRecords = qualitySources ? await Promise.all(
|
|
13391
13506
|
packagingSpecs.filter((spec) => spec.quality && Object.hasOwn(cells, spec.assetId)).map((spec) => provenanceFile(
|
|
@@ -13407,6 +13522,7 @@ async function runMount(args) {
|
|
|
13407
13522
|
cellHeight: style.mount.cellHeight,
|
|
13408
13523
|
cellWidth: style.mount.cellWidth,
|
|
13409
13524
|
cells: Object.entries(cells).sort(([a], [b]) => a.localeCompare(b)),
|
|
13525
|
+
format,
|
|
13410
13526
|
style: styleId
|
|
13411
13527
|
}
|
|
13412
13528
|
}, { force: args.force });
|
|
@@ -13473,6 +13589,13 @@ async function runExport(args) {
|
|
|
13473
13589
|
);
|
|
13474
13590
|
}
|
|
13475
13591
|
}
|
|
13592
|
+
function sheetFormat(args) {
|
|
13593
|
+
const format = args.format ?? "generic";
|
|
13594
|
+
if (!SHEET_FORMATS.includes(format)) {
|
|
13595
|
+
throw new Error(`--format ${format} is for export; sheets take ${SHEET_FORMATS.join(", ")}`);
|
|
13596
|
+
}
|
|
13597
|
+
return format;
|
|
13598
|
+
}
|
|
13476
13599
|
|
|
13477
13600
|
// src/cli/commands/quality.ts
|
|
13478
13601
|
import path38 from "path";
|