pixelkiln 0.27.0 → 0.29.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/cli.js +328 -109
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +323 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -6
- package/dist/index.d.ts +39 -6
- package/dist/index.js +320 -100
- package/dist/index.js.map +1 -1
- package/docs/ARTIFACTS.md +19 -13
- package/docs/CLI.md +17 -7
- package/docs/MANIFEST.md +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -872,12 +872,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
872
872
|
}
|
|
873
873
|
validate(spec, styleImages) {
|
|
874
874
|
const options = resolvedOptions(spec);
|
|
875
|
-
if (spec.source && !spec.revision)
|
|
876
|
-
if (spec.generator === "frames") {
|
|
877
|
-
throw new Error("ComfyUI frame sets require generated provider outputs, not asset.source");
|
|
878
|
-
}
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
875
|
+
if (spec.source && !spec.revision && spec.generator !== "frames") return;
|
|
881
876
|
if (spec.width < 16 || spec.height < 16 || spec.width > 4096 || spec.height > 4096) {
|
|
882
877
|
throw new Error("ComfyUI output dimensions must be between 16 and 4096 pixels");
|
|
883
878
|
}
|
|
@@ -3673,13 +3668,25 @@ function resolveOutputPath(recordedPath, manifestDir) {
|
|
|
3673
3668
|
return path5.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path5.sep));
|
|
3674
3669
|
}
|
|
3675
3670
|
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
3676
|
-
|
|
3671
|
+
return memberPath(spec.outFile, role, index, total, mediaType);
|
|
3672
|
+
}
|
|
3673
|
+
function memberPath(file, role, index, total, mediaType) {
|
|
3674
|
+
const originalExt = path5.extname(file);
|
|
3677
3675
|
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
3678
|
-
const stem = originalExt ?
|
|
3676
|
+
const stem = originalExt ? file.slice(0, -originalExt.length) : file;
|
|
3679
3677
|
if (total === 1) return `${stem}${ext}`;
|
|
3680
3678
|
const safeRole = (role ?? fallbackOutputRole(index)).replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
3681
3679
|
return `${stem}-${safeRole}${ext}`;
|
|
3682
3680
|
}
|
|
3681
|
+
function isFrameSetEntry(entry) {
|
|
3682
|
+
return entry.generator === "frames" && entry.outputs.length > 1;
|
|
3683
|
+
}
|
|
3684
|
+
function sourceOutputPath(source, entry, index, manifestDir) {
|
|
3685
|
+
const absolute = path5.resolve(manifestDir, source);
|
|
3686
|
+
if (!isFrameSetEntry(entry)) return absolute;
|
|
3687
|
+
const output = entry.outputs[index];
|
|
3688
|
+
return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
|
|
3689
|
+
}
|
|
3683
3690
|
function currentOutputPath(output, spec, index, total) {
|
|
3684
3691
|
return expectedOutputPath(spec, output.role, index, total, output.mediaType);
|
|
3685
3692
|
}
|
|
@@ -4336,12 +4343,15 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
4336
4343
|
for (const [key, entry] of entries) {
|
|
4337
4344
|
const id = key.slice(prefix.length);
|
|
4338
4345
|
locked.add(id);
|
|
4339
|
-
|
|
4346
|
+
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
4340
4347
|
const source = sources[id];
|
|
4341
4348
|
if (source && outputs.length <= 1) {
|
|
4342
4349
|
inputs.push({ id, path: path7.resolve(manifestDir, source) });
|
|
4343
4350
|
continue;
|
|
4344
4351
|
}
|
|
4352
|
+
if (source && isFrameSetEntry(entry)) {
|
|
4353
|
+
outputs = outputs.map((output) => ({ ...output, absolutePath: sourceOutputPath(source, entry, output.index, manifestDir) }));
|
|
4354
|
+
}
|
|
4345
4355
|
let selected = outputs;
|
|
4346
4356
|
if (options.outputRoles?.length) {
|
|
4347
4357
|
selected = outputs.filter(
|
|
@@ -4463,11 +4473,21 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
4463
4473
|
for (const id of ids) {
|
|
4464
4474
|
const cell = cells[id];
|
|
4465
4475
|
const source = sources[id];
|
|
4466
|
-
|
|
4476
|
+
const entry = lock.entries[`${styleId}/${id}`];
|
|
4477
|
+
if (source && !(entry && isFrameSetEntry(entry))) {
|
|
4467
4478
|
placements.push({ id, path: path7.resolve(manifestDir, source), cell });
|
|
4468
4479
|
continue;
|
|
4469
4480
|
}
|
|
4470
|
-
|
|
4481
|
+
if (source && entry) {
|
|
4482
|
+
const selection2 = selectEntryOutput(entry, outputRoles[id]);
|
|
4483
|
+
if (!selection2.ok) {
|
|
4484
|
+
skipped.push({ id, reason: selection2.reason });
|
|
4485
|
+
continue;
|
|
4486
|
+
}
|
|
4487
|
+
const index = entry.outputs.findIndex((output) => output.path === selection2.output.path);
|
|
4488
|
+
placements.push({ id, path: sourceOutputPath(source, entry, Math.max(index, 0), manifestDir), cell });
|
|
4489
|
+
continue;
|
|
4490
|
+
}
|
|
4471
4491
|
if (!entry) {
|
|
4472
4492
|
skipped.push({
|
|
4473
4493
|
id,
|
|
@@ -6036,7 +6056,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6036
6056
|
} else if (opts.force) {
|
|
6037
6057
|
state = "missing";
|
|
6038
6058
|
reason = "--force";
|
|
6039
|
-
} else if (!entry && spec.source) {
|
|
6059
|
+
} else if (!entry && spec.source && spec.generator !== "frames") {
|
|
6040
6060
|
if (existsSync8(path13.resolve(spec.root, spec.source))) {
|
|
6041
6061
|
state = "ok";
|
|
6042
6062
|
reason = `placed from ${spec.source}; not generated`;
|
|
@@ -8246,36 +8266,54 @@ function handEditPath(loaded, spec) {
|
|
|
8246
8266
|
const file = inside ? relative : path20.basename(spec.outFile);
|
|
8247
8267
|
return path20.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
8248
8268
|
}
|
|
8249
|
-
function
|
|
8269
|
+
function handEditBases(spec, lock) {
|
|
8250
8270
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
8251
8271
|
if (entry) {
|
|
8252
|
-
if (entry.outputs.length !== 1) return null;
|
|
8253
|
-
const
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8272
|
+
if (entry.outputs.length !== 1 && !isFrameSetEntry(entry)) return null;
|
|
8273
|
+
const members = entry.outputs.map((output, index) => ({
|
|
8274
|
+
role: entry.outputs.length === 1 ? null : output.role ?? null,
|
|
8275
|
+
path: currentEntryOutputPath(entry, spec, index),
|
|
8276
|
+
mediaType: output.mediaType
|
|
8277
|
+
}));
|
|
8278
|
+
if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
|
|
8279
|
+
if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
|
|
8280
|
+
if (!members.every((member) => existsSync16(member.path))) return null;
|
|
8281
|
+
return members.map(({ role, path: file }) => ({ role, path: file }));
|
|
8282
|
+
}
|
|
8283
|
+
return existsSync16(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
|
|
8284
|
+
}
|
|
8285
|
+
function handEditMembers(loaded, spec, lock, editPath) {
|
|
8286
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
8287
|
+
const bases = handEditBases(spec, lock);
|
|
8288
|
+
if (entry && isFrameSetEntry(entry)) {
|
|
8289
|
+
return entry.outputs.map((output, index) => ({
|
|
8290
|
+
role: output.role ?? null,
|
|
8291
|
+
path: memberPath(editPath, output.role, index, entry.outputs.length, output.mediaType),
|
|
8292
|
+
base: bases?.[index]?.path ?? null
|
|
8293
|
+
}));
|
|
8257
8294
|
}
|
|
8258
|
-
return
|
|
8295
|
+
return [{ role: null, path: editPath, base: bases?.[0]?.path ?? null }];
|
|
8259
8296
|
}
|
|
8260
8297
|
async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
8261
8298
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
8262
|
-
if (entry && entry.outputs.length > 1) {
|
|
8299
|
+
if (entry && entry.outputs.length > 1 && !isFrameSetEntry(entry)) {
|
|
8263
8300
|
throw new ManifestEditError(
|
|
8264
|
-
`${spec.styleId}/${spec.assetId}
|
|
8301
|
+
`${spec.styleId}/${spec.assetId} is a structural set of ${entry.outputs.length} outputs; hand edits cover single images and frame sets`
|
|
8265
8302
|
);
|
|
8266
8303
|
}
|
|
8267
8304
|
const existing = spec.source ? path20.resolve(loaded.root, spec.source) : null;
|
|
8268
8305
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
8306
|
+
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
8269
8307
|
let created = false;
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
if (!base) {
|
|
8308
|
+
for (const member of members) {
|
|
8309
|
+
if (existsSync16(member.path)) continue;
|
|
8310
|
+
if (!member.base) {
|
|
8273
8311
|
throw new ManifestEditError(
|
|
8274
8312
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
8275
8313
|
);
|
|
8276
8314
|
}
|
|
8277
|
-
await mkdir6(path20.dirname(
|
|
8278
|
-
await copyFile(base,
|
|
8315
|
+
await mkdir6(path20.dirname(member.path), { recursive: true });
|
|
8316
|
+
await copyFile(member.base, member.path);
|
|
8279
8317
|
created = true;
|
|
8280
8318
|
}
|
|
8281
8319
|
const source = portableOutputPath(editPath, loaded.root);
|
|
@@ -8292,7 +8330,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
8292
8330
|
declared = result.changed;
|
|
8293
8331
|
manifestSha256 = result.sha256;
|
|
8294
8332
|
}
|
|
8295
|
-
return { editPath, source, created, declared, manifestSha256 };
|
|
8333
|
+
return { editPath, members, source, created, declared, manifestSha256 };
|
|
8296
8334
|
}
|
|
8297
8335
|
async function detachHandEdit(loaded, spec, opts = {}) {
|
|
8298
8336
|
if (!spec.source) {
|
|
@@ -8306,19 +8344,44 @@ async function detachHandEdit(loaded, spec, opts = {}) {
|
|
|
8306
8344
|
});
|
|
8307
8345
|
return { manifestSha256: result.sha256, changed: result.changed };
|
|
8308
8346
|
}
|
|
8309
|
-
var
|
|
8310
|
-
|
|
8311
|
-
|
|
8347
|
+
var CompanionOutputSchema = z6.object({
|
|
8348
|
+
/** Output role for a set member; null for a single image. */
|
|
8349
|
+
role: z6.string().nullable(),
|
|
8350
|
+
/** sha256 of the generated file this member started from; null when it started from untracked art. */
|
|
8312
8351
|
basedOn: z6.string().regex(/^[0-9a-f]{64}$/).nullable(),
|
|
8313
|
-
/** sha256 of the
|
|
8314
|
-
sha256: z6.string().regex(/^[0-9a-f]{64}$/)
|
|
8352
|
+
/** sha256 of the member as saved, so a later change by another tool is visible. */
|
|
8353
|
+
sha256: z6.string().regex(/^[0-9a-f]{64}$/)
|
|
8354
|
+
}).strict();
|
|
8355
|
+
var CompanionV2Schema = z6.object({
|
|
8356
|
+
version: z6.literal(2),
|
|
8315
8357
|
/** Editor identity the bridge reported, e.g. `pixelorama@v1.2.2-stable`. */
|
|
8316
8358
|
editor: z6.string().min(1),
|
|
8317
8359
|
protocol: z6.number().int().positive(),
|
|
8318
8360
|
savedAt: z6.string().datetime(),
|
|
8319
8361
|
/** Basename of the layered project file beside the edit, when one was kept. */
|
|
8362
|
+
project: z6.string().min(1).nullable(),
|
|
8363
|
+
/** One entry per file, in output order. */
|
|
8364
|
+
outputs: z6.array(CompanionOutputSchema).min(1)
|
|
8365
|
+
}).strict();
|
|
8366
|
+
var CompanionV1Schema = z6.object({
|
|
8367
|
+
version: z6.literal(1),
|
|
8368
|
+
basedOn: z6.string().regex(/^[0-9a-f]{64}$/).nullable(),
|
|
8369
|
+
sha256: z6.string().regex(/^[0-9a-f]{64}$/),
|
|
8370
|
+
editor: z6.string().min(1),
|
|
8371
|
+
protocol: z6.number().int().positive(),
|
|
8372
|
+
savedAt: z6.string().datetime(),
|
|
8320
8373
|
project: z6.string().min(1).nullable()
|
|
8321
8374
|
}).strict();
|
|
8375
|
+
var HandEditCompanionSchema = z6.union([CompanionV2Schema, CompanionV1Schema]).transform(
|
|
8376
|
+
(value) => value.version === 2 ? value : {
|
|
8377
|
+
version: 2,
|
|
8378
|
+
editor: value.editor,
|
|
8379
|
+
protocol: value.protocol,
|
|
8380
|
+
savedAt: value.savedAt,
|
|
8381
|
+
project: value.project,
|
|
8382
|
+
outputs: [{ role: null, basedOn: value.basedOn, sha256: value.sha256 }]
|
|
8383
|
+
}
|
|
8384
|
+
);
|
|
8322
8385
|
function handEditCompanionPath(editPath) {
|
|
8323
8386
|
return editPath.replace(/\.png$/i, "") + ".edit.json";
|
|
8324
8387
|
}
|
|
@@ -8340,45 +8403,79 @@ async function readHandEditCompanion(editPath) {
|
|
|
8340
8403
|
}
|
|
8341
8404
|
var MAX_HAND_EDIT_BYTES = 16 * 1024 * 1024;
|
|
8342
8405
|
async function saveHandEdit(loaded, lock, spec, input) {
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
}
|
|
8406
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
8407
|
+
const frameSet = entry ? isFrameSetEntry(entry) : false;
|
|
8408
|
+
const incoming = input.frames ?? (input.png ? [{ role: null, png: input.png }] : []);
|
|
8409
|
+
if (!incoming.length) throw new ManifestEditError("the save carried no image");
|
|
8346
8410
|
if (input.project && input.project.length > MAX_HAND_EDIT_BYTES) {
|
|
8347
8411
|
throw new ManifestEditError(`the project file is ${input.project.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
|
|
8348
8412
|
}
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8413
|
+
if (frameSet) {
|
|
8414
|
+
const expected = entry.outputs.map((output) => output.role ?? null);
|
|
8415
|
+
const got = incoming.map((frame) => frame.role);
|
|
8416
|
+
if (got.length !== expected.length || expected.some((role, index) => got[index] !== role)) {
|
|
8417
|
+
throw new ManifestEditError(
|
|
8418
|
+
`${spec.styleId}/${spec.assetId} is a set of ${expected.length} frames (${expected.join(", ")}); the editor returned ${got.length} (${got.map((role) => role ?? "new").join(", ")}) \u2014 keep the frames it opened with`
|
|
8419
|
+
);
|
|
8420
|
+
}
|
|
8421
|
+
} else if (incoming.length !== 1) {
|
|
8422
|
+
throw new ManifestEditError(`${spec.styleId}/${spec.assetId} is one image; the editor returned ${incoming.length} frames \u2014 keep a single frame`);
|
|
8355
8423
|
}
|
|
8356
|
-
const
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8424
|
+
const decoded = incoming.map((frame, index) => {
|
|
8425
|
+
if (frame.png.length > MAX_HAND_EDIT_BYTES) {
|
|
8426
|
+
throw new ManifestEditError(`the edit is ${frame.png.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
|
|
8427
|
+
}
|
|
8428
|
+
try {
|
|
8429
|
+
validateMedia(frame.png, MediaType.PNG);
|
|
8430
|
+
return decodePng(frame.png);
|
|
8431
|
+
} catch (error) {
|
|
8432
|
+
const which = frameSet ? `frame ${frame.role ?? index}` : "the edit";
|
|
8433
|
+
throw new ManifestEditError(`${which} is not a valid PNG: ${error instanceof Error ? error.message : String(error)}`);
|
|
8434
|
+
}
|
|
8435
|
+
});
|
|
8436
|
+
const bases = handEditBases(spec, lock);
|
|
8437
|
+
const expectedSize = bases?.[0] ? decodePng(await readFile15(bases[0].path)) : { width: spec.width, height: spec.height };
|
|
8438
|
+
for (const [index, image] of decoded.entries()) {
|
|
8439
|
+
if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
|
|
8440
|
+
const which = frameSet ? `frame ${incoming[index].role ?? index}` : "the edit";
|
|
8441
|
+
throw new ManifestEditError(
|
|
8442
|
+
`${which} is ${image.width}\xD7${image.height}; ${spec.styleId}/${spec.assetId} is ${expectedSize.width}\xD7${expectedSize.height}`
|
|
8443
|
+
);
|
|
8444
|
+
}
|
|
8362
8445
|
}
|
|
8363
8446
|
const started = await startHandEdit(loaded, lock, spec, { expectedSha256: input.expectedSha256 });
|
|
8364
|
-
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
8365
|
-
const basedOn = entry ? primaryOutput(entry)?.sha256 ?? null : null;
|
|
8366
|
-
const digest2 = sha256(input.png);
|
|
8367
8447
|
const projectPath = input.project ? handEditProjectPath(started.editPath) : null;
|
|
8368
8448
|
const companion = {
|
|
8369
|
-
version:
|
|
8370
|
-
basedOn,
|
|
8371
|
-
sha256: digest2,
|
|
8449
|
+
version: 2,
|
|
8372
8450
|
editor: input.editor,
|
|
8373
8451
|
protocol: input.protocol,
|
|
8374
8452
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
8375
|
-
project: projectPath ? path20.basename(projectPath) : null
|
|
8453
|
+
project: projectPath ? path20.basename(projectPath) : null,
|
|
8454
|
+
outputs: incoming.map((frame, index) => ({
|
|
8455
|
+
role: frame.role,
|
|
8456
|
+
basedOn: basedOnHash(entry, index),
|
|
8457
|
+
sha256: sha256(frame.png)
|
|
8458
|
+
}))
|
|
8376
8459
|
};
|
|
8377
|
-
|
|
8460
|
+
for (const [index, member] of started.members.entries()) {
|
|
8461
|
+
await replaceFile(member.path, incoming[index].png);
|
|
8462
|
+
}
|
|
8378
8463
|
if (projectPath && input.project) await replaceFile(projectPath, input.project);
|
|
8379
8464
|
const companionPath = handEditCompanionPath(started.editPath);
|
|
8380
8465
|
await replaceFile(companionPath, JSON.stringify(companion, null, 2) + "\n");
|
|
8381
|
-
return {
|
|
8466
|
+
return {
|
|
8467
|
+
...started,
|
|
8468
|
+
sha256: companion.outputs[0].sha256,
|
|
8469
|
+
basedOn: companion.outputs[0].basedOn,
|
|
8470
|
+
companion,
|
|
8471
|
+
companionPath,
|
|
8472
|
+
projectPath
|
|
8473
|
+
};
|
|
8474
|
+
}
|
|
8475
|
+
function basedOnHash(entry, index) {
|
|
8476
|
+
if (!entry) return null;
|
|
8477
|
+
if (entry.outputs.length === 1) return primaryOutput(entry)?.sha256 ?? null;
|
|
8478
|
+
return entry.outputs[index]?.sha256 ?? null;
|
|
8382
8479
|
}
|
|
8383
8480
|
async function replaceFile(file, bytes) {
|
|
8384
8481
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
@@ -8425,7 +8522,10 @@ var HandEditRequestSchema = z7.discriminatedUnion("action", [
|
|
|
8425
8522
|
assetId: z7.string().min(1),
|
|
8426
8523
|
styleId: z7.string().min(1),
|
|
8427
8524
|
expectedSha256: z7.string().regex(/^[0-9a-f]{64}$/),
|
|
8428
|
-
|
|
8525
|
+
/** The flattened image; for a frame set, `frames` instead. */
|
|
8526
|
+
png: Base64.min(1).optional(),
|
|
8527
|
+
/** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
|
|
8528
|
+
frames: z7.array(z7.object({ role: z7.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
|
|
8429
8529
|
/** The editor's layered project file, kept beside the edit. */
|
|
8430
8530
|
pxo: Base64.optional(),
|
|
8431
8531
|
/** Editor identity from the bridge's ready message. */
|
|
@@ -8448,18 +8548,23 @@ function createGalleryEditHandler(opts) {
|
|
|
8448
8548
|
const started = await startHandEdit(ctx.loaded, ctx.lock, spec, { expectedSha256: request.expectedSha256 });
|
|
8449
8549
|
log2(` hand edit ${started.created ? "created" : "found"}: ${started.source}${started.declared ? " (declared in the manifest)" : ""}`);
|
|
8450
8550
|
if (request.open) {
|
|
8451
|
-
const command = (opts.openEditor ?? openInEditor)(started.
|
|
8551
|
+
const command = (opts.openEditor ?? openInEditor)(started.members[0].path);
|
|
8452
8552
|
log2(` opened with: ${command}`);
|
|
8453
8553
|
}
|
|
8454
8554
|
} else if (request.action === "save-edit") {
|
|
8555
|
+
if (request.png !== void 0 === (request.frames !== void 0)) {
|
|
8556
|
+
throw new ManifestEditError("invalid edit: send png for one image or frames for a set, not both");
|
|
8557
|
+
}
|
|
8455
8558
|
const saved = await saveHandEdit(ctx.loaded, ctx.lock, spec, {
|
|
8456
|
-
png: Buffer.from(request.png, "base64"),
|
|
8559
|
+
...request.png !== void 0 ? { png: Buffer.from(request.png, "base64") } : {},
|
|
8560
|
+
...request.frames ? { frames: request.frames.map((frame) => ({ role: frame.role, png: Buffer.from(frame.png, "base64") })) } : {},
|
|
8457
8561
|
...request.pxo ? { project: Buffer.from(request.pxo, "base64") } : {},
|
|
8458
8562
|
editor: request.editor,
|
|
8459
8563
|
protocol: request.protocol,
|
|
8460
8564
|
expectedSha256: request.expectedSha256
|
|
8461
8565
|
});
|
|
8462
|
-
|
|
8566
|
+
const what = saved.members.length > 1 ? `${saved.source} (${saved.members.length} frames)` : saved.source;
|
|
8567
|
+
log2(` hand edit saved from ${request.editor}: ${what}${saved.declared ? " (declared in the manifest)" : ""}${saved.projectPath ? " + project file" : ""}`);
|
|
8463
8568
|
} else {
|
|
8464
8569
|
const result2 = await detachHandEdit(ctx.loaded, spec, { expectedSha256: request.expectedSha256 });
|
|
8465
8570
|
if (result2.changed) log2(` hand edit detached: ${request.styleId}/${request.assetId} (file kept)`);
|
|
@@ -8490,6 +8595,7 @@ function createGalleryEditHandler(opts) {
|
|
|
8490
8595
|
}
|
|
8491
8596
|
|
|
8492
8597
|
// src/gallery/snapshot.ts
|
|
8598
|
+
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
8493
8599
|
function galleryMediaId(absolutePath) {
|
|
8494
8600
|
return createHash3("sha256").update(path22.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
8495
8601
|
}
|
|
@@ -8506,6 +8612,18 @@ function matchesFilter(filter, styleId, assetId) {
|
|
|
8506
8612
|
if (filter?.assets?.length && !filter.assets.includes(assetId)) return false;
|
|
8507
8613
|
return true;
|
|
8508
8614
|
}
|
|
8615
|
+
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
8616
|
+
async function samePixels(a, b) {
|
|
8617
|
+
try {
|
|
8618
|
+
const [left, right] = await Promise.all([readFile16(a), readFile16(b)]);
|
|
8619
|
+
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
8620
|
+
const x = decodePng(left);
|
|
8621
|
+
const y = decodePng(right);
|
|
8622
|
+
return x.width === y.width && x.height === y.height && x.pixels.equals(y.pixels);
|
|
8623
|
+
} catch {
|
|
8624
|
+
return false;
|
|
8625
|
+
}
|
|
8626
|
+
}
|
|
8509
8627
|
function metadataFps(entry) {
|
|
8510
8628
|
if (!entry) return null;
|
|
8511
8629
|
const namespace = entry.providerMetadata?.[entry.provider];
|
|
@@ -8623,6 +8741,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
8623
8741
|
const asset = loaded.manifest.assets[spec.assetId] ?? null;
|
|
8624
8742
|
let outputs;
|
|
8625
8743
|
let edit = null;
|
|
8744
|
+
let edits = [];
|
|
8745
|
+
let editChanged = [];
|
|
8626
8746
|
let editStatus = null;
|
|
8627
8747
|
let editMeta = null;
|
|
8628
8748
|
if (entry && entry.outputs.length) {
|
|
@@ -8633,22 +8753,52 @@ async function buildGallerySnapshot(opts) {
|
|
|
8633
8753
|
);
|
|
8634
8754
|
if (spec.source) {
|
|
8635
8755
|
const editPath = path22.resolve(root, spec.source);
|
|
8636
|
-
const
|
|
8637
|
-
|
|
8638
|
-
|
|
8756
|
+
const memberCount = isFrameSetEntry(entry) ? entry.outputs.length : 1;
|
|
8757
|
+
const editShas = await Promise.all(
|
|
8758
|
+
Array.from({ length: memberCount }, async (_, index) => {
|
|
8759
|
+
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
8760
|
+
return { file, sha256: existsSync17(file) ? await sha256File(file) : null };
|
|
8761
|
+
})
|
|
8762
|
+
);
|
|
8763
|
+
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
8764
|
+
edit = edits[0] ?? null;
|
|
8639
8765
|
const companion = await readHandEditCompanion(editPath);
|
|
8640
8766
|
if (companion) {
|
|
8641
8767
|
const projectPath = handEditProjectPath(editPath);
|
|
8768
|
+
const project = companion.project && existsSync17(projectPath) ? await fileInfo(projectPath) : null;
|
|
8769
|
+
let projectUrl = null;
|
|
8770
|
+
if (project) {
|
|
8771
|
+
const id = galleryMediaId(projectPath);
|
|
8772
|
+
media.set(id, { path: projectPath, contentType: PROJECT_FILE_TYPE });
|
|
8773
|
+
projectUrl = `${galleryMediaRoute(id)}?v=${project.bytes}-${Date.parse(project.modifiedAt).toString(36)}`;
|
|
8774
|
+
}
|
|
8642
8775
|
editMeta = {
|
|
8643
8776
|
editor: companion.editor,
|
|
8644
8777
|
savedAt: companion.savedAt,
|
|
8645
|
-
basedOn: companion.basedOn,
|
|
8646
|
-
changedSince:
|
|
8647
|
-
|
|
8778
|
+
basedOn: companion.outputs[0]?.basedOn ?? null,
|
|
8779
|
+
changedSince: editShas.some(({ sha256: editSha }, index) => {
|
|
8780
|
+
const recorded = companion.outputs[index]?.sha256;
|
|
8781
|
+
return editSha !== null && recorded !== void 0 && editSha !== recorded;
|
|
8782
|
+
}),
|
|
8783
|
+
project: project ? portableOutputPath(projectPath, root) : null,
|
|
8784
|
+
projectUrl
|
|
8648
8785
|
};
|
|
8649
8786
|
}
|
|
8650
|
-
const regenerated =
|
|
8651
|
-
|
|
8787
|
+
const regenerated = edits.some((member, index) => {
|
|
8788
|
+
const generated = outputs[index];
|
|
8789
|
+
if (companion) {
|
|
8790
|
+
const recorded = companion.outputs[index];
|
|
8791
|
+
return recorded !== void 0 && generated.sha256 !== null && recorded.basedOn !== generated.sha256;
|
|
8792
|
+
}
|
|
8793
|
+
return Boolean(generated.modifiedAt && member.modifiedAt && generated.modifiedAt > member.modifiedAt && generated.exists);
|
|
8794
|
+
});
|
|
8795
|
+
editChanged = await Promise.all(edits.map(async (member, index) => {
|
|
8796
|
+
const generated = outputs[index];
|
|
8797
|
+
if (!member.exists || !generated.exists) return true;
|
|
8798
|
+
if (member.sha256 === generated.sha256) return false;
|
|
8799
|
+
return !await samePixels(member.absolutePath, generated.absolutePath);
|
|
8800
|
+
}));
|
|
8801
|
+
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
8652
8802
|
}
|
|
8653
8803
|
} else if (spec.source) {
|
|
8654
8804
|
outputs = [await describeOutput(media, root, path22.resolve(root, spec.source))];
|
|
@@ -8697,6 +8847,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
8697
8847
|
asset,
|
|
8698
8848
|
source: spec.source ?? null,
|
|
8699
8849
|
edit,
|
|
8850
|
+
edits,
|
|
8851
|
+
editChanged,
|
|
8700
8852
|
editStatus,
|
|
8701
8853
|
editMeta,
|
|
8702
8854
|
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
@@ -8754,6 +8906,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
8754
8906
|
asset: null,
|
|
8755
8907
|
source: null,
|
|
8756
8908
|
edit: null,
|
|
8909
|
+
edits: [],
|
|
8910
|
+
editChanged: [],
|
|
8757
8911
|
editStatus: null,
|
|
8758
8912
|
editMeta: null,
|
|
8759
8913
|
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
@@ -9688,10 +9842,15 @@ window.addEventListener('message', (e) => {
|
|
|
9688
9842
|
// declares the file the same way pixelkiln edit does.
|
|
9689
9843
|
|
|
9690
9844
|
let SHEET = null;
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
const
|
|
9694
|
-
|
|
9845
|
+
// What the editor gets: the edit files when they are all there, else the
|
|
9846
|
+
// generated outputs (one PNG, or every frame of a set).
|
|
9847
|
+
const editorSources = (item) => {
|
|
9848
|
+
if (item.edits.length && item.edits.every((e) => e.exists)) return { files: item.edits, fromEdit: true };
|
|
9849
|
+
if (editableOutputs(item)) return { files: item.outputs, fromEdit: false };
|
|
9850
|
+
return null;
|
|
9851
|
+
};
|
|
9852
|
+
const editorSource = (item) => { const s = editorSources(item); return s ? s.files[0] : null; };
|
|
9853
|
+
const canEditInBrowser = (item) => EDITOR && EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editorSources(item);
|
|
9695
9854
|
const toBase64 = (buf) => new Promise((resolve, reject) => {
|
|
9696
9855
|
const r = new FileReader();
|
|
9697
9856
|
r.onload = () => resolve(String(r.result).slice(String(r.result).indexOf(',') + 1));
|
|
@@ -9720,7 +9879,7 @@ function openEditorSheet(item) {
|
|
|
9720
9879
|
const bar = el('div', 'rbar');
|
|
9721
9880
|
const close = el('button', null, 'Close'); close.type = 'button';
|
|
9722
9881
|
close.onclick = () => closeEditorSheet();
|
|
9723
|
-
const title = el('b', null, (item.project ? item.project + ':' : '') + item.styleId + '/' + item.assetId + ' \xB7 ' + item.width + '\xD7' + item.height);
|
|
9882
|
+
const title = el('b', null, (item.project ? item.project + ':' : '') + item.styleId + '/' + item.assetId + ' \xB7 ' + item.width + '\xD7' + item.height + (item.outputs.length > 1 ? ' \xB7 ' + item.outputs.length + ' frames' : ''));
|
|
9724
9883
|
const status = el('span', 'st');
|
|
9725
9884
|
const acts = el('div', 'acts');
|
|
9726
9885
|
const msg = el('span', 'msg');
|
|
@@ -9741,30 +9900,62 @@ function openEditorSheet(item) {
|
|
|
9741
9900
|
stage.append(frame, loading);
|
|
9742
9901
|
panel.append(bar, stage);
|
|
9743
9902
|
host.append(scrim, panel);
|
|
9744
|
-
SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src };
|
|
9903
|
+
SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src, sentProject: false, roles: null };
|
|
9745
9904
|
sheetStatus('cool', 'loading');
|
|
9746
9905
|
sheetButtons();
|
|
9747
9906
|
close.focus({ preventScroll: true });
|
|
9748
9907
|
}
|
|
9908
|
+
// The edit files are what the editor gets; when a browser save kept the
|
|
9909
|
+
// layered project beside them, that goes along too and the editor restores
|
|
9910
|
+
// the layers, falling back to the flattened PNGs if the file cannot be read.
|
|
9911
|
+
// A frame set is sent as frames, one per member, and comes back the same way.
|
|
9749
9912
|
async function sendOpen() {
|
|
9750
|
-
const { item
|
|
9913
|
+
const { item } = SHEET;
|
|
9914
|
+
const sources = editorSources(item);
|
|
9751
9915
|
const style = snap.styles.find((s) => s.id === item.styleId && s.project === item.project);
|
|
9752
|
-
|
|
9916
|
+
const projectUrl = sources.fromEdit && item.editMeta && item.editMeta.projectUrl && SHEET.ready.version >= 2 ? item.editMeta.projectUrl : null;
|
|
9917
|
+
const set = sources.files.length > 1;
|
|
9918
|
+
if (set && SHEET.ready.version < 3) {
|
|
9919
|
+
SHEET.msg.textContent = 'This editor build cannot open frame sets; reinstall it with pixelkiln tools install editor.';
|
|
9920
|
+
sheetStatus('bad', 'unsupported');
|
|
9921
|
+
return;
|
|
9922
|
+
}
|
|
9923
|
+
const pngs = [];
|
|
9924
|
+
let pxo = null;
|
|
9753
9925
|
try {
|
|
9754
|
-
const
|
|
9755
|
-
|
|
9756
|
-
|
|
9926
|
+
for (const file of sources.files) {
|
|
9927
|
+
const res = await fetch(file.url, { cache: 'no-store' });
|
|
9928
|
+
if (!res.ok) throw new Error(await res.text());
|
|
9929
|
+
pngs.push(await res.arrayBuffer());
|
|
9930
|
+
}
|
|
9931
|
+
if (projectUrl) {
|
|
9932
|
+
const p = await fetch(projectUrl, { cache: 'no-store' });
|
|
9933
|
+
if (p.ok) pxo = await p.arrayBuffer();
|
|
9934
|
+
}
|
|
9757
9935
|
} catch (err) {
|
|
9758
9936
|
if (SHEET) { SHEET.msg.textContent = 'Could not load the image: ' + err.message; sheetStatus('bad', 'failed'); }
|
|
9759
9937
|
return;
|
|
9760
9938
|
}
|
|
9761
9939
|
if (!SHEET) return;
|
|
9762
9940
|
const request = 'open-' + (++SHEET.requests);
|
|
9763
|
-
SHEET.
|
|
9941
|
+
SHEET.sentProject = !!pxo;
|
|
9942
|
+
SHEET.roles = set ? item.outputs.map((o) => o.role || null) : null;
|
|
9943
|
+
const message = {
|
|
9764
9944
|
type: 'pixelkiln:open', request,
|
|
9765
9945
|
asset: { key: item.styleId + '/' + item.assetId, id: item.id, name: item.assetId, width: item.width, height: item.height },
|
|
9766
|
-
png, palette: style ? style.palette : [],
|
|
9767
|
-
}
|
|
9946
|
+
png: pngs[0], palette: style ? style.palette : [],
|
|
9947
|
+
};
|
|
9948
|
+
const transfer = [pngs[0]];
|
|
9949
|
+
if (set) {
|
|
9950
|
+
message.frames = pngs.map((png, i) => ({ role: SHEET.roles[i], png }));
|
|
9951
|
+
message.fps = item.fps || 12;
|
|
9952
|
+
for (const png of pngs.slice(1)) transfer.push(png);
|
|
9953
|
+
// The first frame is both png (older editors) and frames[0]; copy it so both transfer.
|
|
9954
|
+
message.png = pngs[0].slice(0);
|
|
9955
|
+
transfer.push(message.png);
|
|
9956
|
+
}
|
|
9957
|
+
if (pxo) { message.pxo = pxo; transfer.push(pxo); }
|
|
9958
|
+
SHEET.frame.contentWindow.postMessage(message, location.origin, transfer);
|
|
9768
9959
|
sheetStatus('cool', 'opening');
|
|
9769
9960
|
}
|
|
9770
9961
|
function requestEditorSave(close) {
|
|
@@ -9790,7 +9981,10 @@ async function onEditorMessage(m) {
|
|
|
9790
9981
|
case 'pixelkiln:opened':
|
|
9791
9982
|
SHEET.opened = true;
|
|
9792
9983
|
SHEET.loading.remove();
|
|
9793
|
-
sheetStatus('ok',
|
|
9984
|
+
if (m.source === 'pxo') sheetStatus('ok', 'editing the edit file' + (m.frames > 1 ? 's' : '') + ' with ' + (m.layers === 1 ? 'the layer' : m.layers + ' layers') + (m.frames > 1 ? ' and ' + m.frames + ' frames' : '') + ' restored');
|
|
9985
|
+
else if (SHEET.sentProject) sheetStatus('warn', 'editing the flattened edit file' + (m.frames > 1 ? 's' : '') + '; the layer file could not be opened');
|
|
9986
|
+
else if (m.source === 'frames') sheetStatus('ok', (SHEET.item.edits.length ? 'editing the edit files' : 'editing a copy of the generated frames') + ' as ' + m.frames + ' frames');
|
|
9987
|
+
else sheetStatus('ok', SHEET.item.edit && SHEET.item.edit.exists ? 'editing the edit file' : 'editing a copy of the generated art');
|
|
9794
9988
|
sheetButtons();
|
|
9795
9989
|
break;
|
|
9796
9990
|
case 'pixelkiln:dirty':
|
|
@@ -9802,14 +9996,21 @@ async function onEditorMessage(m) {
|
|
|
9802
9996
|
const pending = SHEET.pending;
|
|
9803
9997
|
if (!pending || pending.request !== m.request) return;
|
|
9804
9998
|
try {
|
|
9805
|
-
const body = {
|
|
9999
|
+
const body = { editor: SHEET.ready.editor, protocol: SHEET.ready.version };
|
|
10000
|
+
if (SHEET.roles) {
|
|
10001
|
+
if (!Array.isArray(m.frames)) throw new Error('the editor returned no frames');
|
|
10002
|
+
body.frames = [];
|
|
10003
|
+
for (const f of m.frames) body.frames.push({ role: f.role === undefined ? null : f.role, png: await toBase64(f.png) });
|
|
10004
|
+
} else {
|
|
10005
|
+
body.png = await toBase64(m.png);
|
|
10006
|
+
}
|
|
9806
10007
|
if (m.pxo && m.pxo.byteLength) body.pxo = await toBase64(m.pxo);
|
|
9807
10008
|
await postHandEdit(SHEET.item, 'save-edit', body);
|
|
9808
10009
|
const saved = snap.items.find((i) => i.id === SHEET.item.id);
|
|
9809
10010
|
if (saved) SHEET.item = saved;
|
|
9810
10011
|
SHEET.dirty = false;
|
|
9811
10012
|
SHEET.pending = null;
|
|
9812
|
-
ui.notice = { id: SHEET.item.id, text: 'Saved ' + (saved && saved.edit ? saved.edit.path : 'the edit') + '. mount and pack place it in place of the generated art.' };
|
|
10013
|
+
ui.notice = { id: SHEET.item.id, text: 'Saved ' + (saved && saved.edits.length > 1 ? saved.edits.length + ' frames to ' + saved.source : saved && saved.edit ? saved.edit.path : 'the edit') + '. mount and pack place ' + (saved && saved.edits.length > 1 ? 'them' : 'it') + ' in place of the generated art.' };
|
|
9813
10014
|
render();
|
|
9814
10015
|
if (pending.close) { closeEditorSheet(true); return; }
|
|
9815
10016
|
sheetStatus('ok', 'saved');
|
|
@@ -9957,7 +10158,7 @@ function clearFilters() {
|
|
|
9957
10158
|
function thumb(item) {
|
|
9958
10159
|
const cell = el('div', 'cell');
|
|
9959
10160
|
// A hand edit is what ships, so it is what the card shows.
|
|
9960
|
-
const shown = item.edit && item.
|
|
10161
|
+
const shown = item.edit && item.editStatus === 'edited' && item.edits.every((e) => e.url) ? item.edits : item.outputs.filter((o) => o.url);
|
|
9961
10162
|
if (!shown.length) {
|
|
9962
10163
|
cell.classList.add('none');
|
|
9963
10164
|
cell.append(el('span', null, item.state === 'missing' ? 'not generated yet'
|
|
@@ -10371,7 +10572,7 @@ function openCompare() {
|
|
|
10371
10572
|
// ---- hand edits -------------------------------------------------------------
|
|
10372
10573
|
|
|
10373
10574
|
const EDIT_STATUS_TEXT = {
|
|
10374
|
-
same: '
|
|
10575
|
+
same: 'The generated art\u2019s pixels, not changed yet. Open it in your editor and save.',
|
|
10375
10576
|
edited: 'Differs from the generated art. mount and pack place this file; the generated file stays as the record.',
|
|
10376
10577
|
'regenerated-since': 'The generated art changed after this edit was saved \u2014 the edit is based on an older generation.',
|
|
10377
10578
|
missing: 'The declared file is not on disk.',
|
|
@@ -10382,12 +10583,16 @@ async function postHandEdit(item, action, extra) {
|
|
|
10382
10583
|
if (item.project) body.project = item.project;
|
|
10383
10584
|
snap = await postEdit(body);
|
|
10384
10585
|
}
|
|
10586
|
+
// A single PNG, or an ordered frame set: both are hand-editable, one file per
|
|
10587
|
+
// member. Structural sets (tiles, characters) are not.
|
|
10588
|
+
const editableOutputs = (item) => item.outputs.length && (item.outputs.length === 1 || isFrameSet(item)) &&
|
|
10589
|
+
item.outputs.every((o) => o.exists && o.mediaType === 'image/png');
|
|
10385
10590
|
function handEditSection(item) {
|
|
10386
|
-
const canStart = EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 &&
|
|
10387
|
-
item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png';
|
|
10591
|
+
const canStart = EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editableOutputs(item);
|
|
10388
10592
|
if (!item.edit && !canStart) return null;
|
|
10593
|
+
const set = item.edits.length > 1;
|
|
10389
10594
|
const s = el('section', 'meta');
|
|
10390
|
-
s.append(el('h3', null, 'Hand edit'));
|
|
10595
|
+
s.append(el('h3', null, set ? 'Hand edit (' + item.edits.length + ' frames)' : 'Hand edit'));
|
|
10391
10596
|
if (item.edit) {
|
|
10392
10597
|
const pair = el('div', 'pair');
|
|
10393
10598
|
const shared = displayScale(item.width, item.height, 200, 160);
|
|
@@ -10401,24 +10606,36 @@ function handEditSection(item) {
|
|
|
10401
10606
|
f.append(el('figcaption', null, label));
|
|
10402
10607
|
return f;
|
|
10403
10608
|
};
|
|
10404
|
-
|
|
10609
|
+
// For a set, the member the preview is on; the strip there steps through them.
|
|
10610
|
+
const at = set ? Math.min(ui.member, item.edits.length - 1) : 0;
|
|
10611
|
+
const which = set ? ' \xB7 ' + (item.outputs[at].role || 'frame ' + (at + 1)) : '';
|
|
10612
|
+
pair.append(fig(item.outputs[at], 'generated' + which), fig(item.edits[at], 'edit' + which));
|
|
10405
10613
|
s.append(pair);
|
|
10406
10614
|
const dl = el('dl');
|
|
10407
10615
|
const tone = item.editStatus === 'edited' ? 'ok' : item.editStatus === 'same' ? 'dim' : 'warn';
|
|
10408
10616
|
const st = el('span', 'state-' + tone); st.append(el('i', 'dot ' + tone), document.createTextNode(' ' + item.editStatus));
|
|
10409
10617
|
row(dl, 'status', st);
|
|
10410
10618
|
row(dl, 'why', EDIT_STATUS_TEXT[item.editStatus] || '');
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10619
|
+
if (set) {
|
|
10620
|
+
row(dl, 'files', item.source + ' \u2014 one per frame: ' + item.edits.map((e) => e.path.split('/').pop()).join(', '), { mono: true, copy: item.edits.map((e) => e.absolutePath).join('\\n') });
|
|
10621
|
+
const differing = item.editChanged.filter(Boolean).length;
|
|
10622
|
+
row(dl, 'changed', differing + ' of ' + item.edits.length + ' frames differ from the generated art' +
|
|
10623
|
+
(differing ? ': ' + item.edits.filter((e, i) => item.editChanged[i]).map((e) => item.outputs[item.edits.indexOf(e)].role || e.path.split('/').pop()).join(', ') : ''));
|
|
10624
|
+
} else {
|
|
10625
|
+
row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
|
|
10626
|
+
if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
|
|
10627
|
+
}
|
|
10628
|
+
row(dl, 'saved', fmtWhen(item.edits.reduce((latest, e) => e.modifiedAt && (!latest || e.modifiedAt > latest) ? e.modifiedAt : latest, null)));
|
|
10414
10629
|
if (item.editMeta) {
|
|
10415
|
-
row(dl, 'editor', item.editMeta.editor + ', ' + fmtWhen(item.editMeta.savedAt) + (item.editMeta.changedSince ? ' \u2014
|
|
10416
|
-
if (item.editMeta.basedOn) row(dl, 'based on', item.editMeta.basedOn.slice(0, 16) + '\u2026', { mono: true, copy: item.editMeta.basedOn });
|
|
10630
|
+
row(dl, 'editor', item.editMeta.editor + ', ' + fmtWhen(item.editMeta.savedAt) + (item.editMeta.changedSince ? ' \u2014 a file changed since' : ''));
|
|
10631
|
+
if (item.editMeta.basedOn && !set) row(dl, 'based on', item.editMeta.basedOn.slice(0, 16) + '\u2026', { mono: true, copy: item.editMeta.basedOn });
|
|
10417
10632
|
if (item.editMeta.project) row(dl, 'layers', item.editMeta.project, { mono: true });
|
|
10418
10633
|
}
|
|
10419
10634
|
s.append(dl);
|
|
10420
10635
|
} else {
|
|
10421
|
-
s.append(el('div', 'state-dim',
|
|
10636
|
+
s.append(el('div', 'state-dim', isFrameSet(item)
|
|
10637
|
+
? 'Touch the frames up in your own editor or in the browser, where they open as one animation. The generated files stay untouched; the edit is one sibling file per frame that the manifest points at, and mount and pack place those instead.'
|
|
10638
|
+
: 'Touch it up in your own editor. The generated file stays untouched; the edit is a sibling file the manifest points at, and mount and pack place that instead.'));
|
|
10422
10639
|
}
|
|
10423
10640
|
if (EDITABLE && item.declared && projectOf(item)?.manifestSha256) {
|
|
10424
10641
|
const acts = el('div', 'hand-actions');
|
|
@@ -10446,10 +10663,10 @@ function handEditSection(item) {
|
|
|
10446
10663
|
}
|
|
10447
10664
|
if (item.edit) {
|
|
10448
10665
|
go('Open in desktop editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
10449
|
-
go('Detach edit', 'detach-edit', {}, 'Detached. The file is still at ' + item.
|
|
10666
|
+
go('Detach edit', 'detach-edit', {}, 'Detached. The file' + (set ? 's are' : ' is') + ' still at ' + item.source + '; the generated art is placed again.');
|
|
10450
10667
|
} else if (canStart) {
|
|
10451
|
-
go('Edit by hand', 'start-edit', { open: true }, 'Created the edit file and opened it in your editor. Save there, then Refresh to see it here.');
|
|
10452
|
-
go('Create edit file only', 'start-edit', { open: false }, 'Created the edit file and declared it in the manifest.');
|
|
10668
|
+
go('Edit by hand', 'start-edit', { open: true }, 'Created the edit file' + (isFrameSet(item) ? 's and opened the first frame' : ' and opened it') + ' in your editor. Save there, then Refresh to see it here.');
|
|
10669
|
+
go('Create edit file' + (isFrameSet(item) ? 's' : '') + ' only', 'start-edit', { open: false }, 'Created the edit file' + (isFrameSet(item) ? 's' : '') + ' and declared ' + (isFrameSet(item) ? 'them' : 'it') + ' in the manifest.');
|
|
10453
10670
|
}
|
|
10454
10671
|
acts.append(msg);
|
|
10455
10672
|
s.append(acts);
|
|
@@ -11762,8 +11979,8 @@ var pin_default = {
|
|
|
11762
11979
|
pixelorama: "v1.2.2",
|
|
11763
11980
|
godot: "4.7.2",
|
|
11764
11981
|
extensionsApi: 9,
|
|
11765
|
-
protocol:
|
|
11766
|
-
release: "editor-pixelorama-v1.2.2-pk.
|
|
11982
|
+
protocol: 3,
|
|
11983
|
+
release: "editor-pixelorama-v1.2.2-pk.14",
|
|
11767
11984
|
files: {
|
|
11768
11985
|
"index.apple-touch-icon.png": {
|
|
11769
11986
|
sha256: "ee5434873e0fed91e9a888c3ea5a8b79beac25999308be7a6eb41bf4ce640d27",
|
|
@@ -11778,7 +11995,7 @@ var pin_default = {
|
|
|
11778
11995
|
bytes: 7298
|
|
11779
11996
|
},
|
|
11780
11997
|
"index.html": {
|
|
11781
|
-
sha256: "
|
|
11998
|
+
sha256: "2974146c89f4df9f796e840947c334aaf3e4fa3dd18bcd7e08db2eabfd0f086e",
|
|
11782
11999
|
bytes: 5442
|
|
11783
12000
|
},
|
|
11784
12001
|
"index.icon.png": {
|
|
@@ -11790,8 +12007,8 @@ var pin_default = {
|
|
|
11790
12007
|
bytes: 279815
|
|
11791
12008
|
},
|
|
11792
12009
|
"index.pck": {
|
|
11793
|
-
sha256: "
|
|
11794
|
-
bytes:
|
|
12010
|
+
sha256: "736bd383c82fdadf31cc894caf08490b99c5df572aa20db683972c35c7a0465b",
|
|
12011
|
+
bytes: 6429628
|
|
11795
12012
|
},
|
|
11796
12013
|
"index.png": {
|
|
11797
12014
|
sha256: "6e3ca3eb4a99e4c07bdd89ebc873b440ec0ffd66ce1955160da9e7ed80d538d7",
|
|
@@ -15098,12 +15315,14 @@ async function main() {
|
|
|
15098
15315
|
return;
|
|
15099
15316
|
}
|
|
15100
15317
|
const started = await startHandEdit(loaded, lock, spec);
|
|
15101
|
-
|
|
15318
|
+
const set = started.members.length > 1;
|
|
15319
|
+
log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} frames)` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
|
|
15320
|
+
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path30.relative(process.cwd(), member.path)}`);
|
|
15102
15321
|
if (args.noOpen) {
|
|
15103
|
-
log(` edit it with your own tool, then run pixelkiln plan; mount and pack place it in place of the generated art`);
|
|
15322
|
+
log(` edit ${set ? "them" : "it"} with your own tool, then run pixelkiln plan; mount and pack place ${set ? "them" : "it"} in place of the generated art`);
|
|
15104
15323
|
} else {
|
|
15105
|
-
const command = openInEditor(started.
|
|
15106
|
-
log(` opened with: ${command}${process.env.PIXELKILN_EDITOR ? "" : " (set PIXELKILN_EDITOR to choose the program)"}`);
|
|
15324
|
+
const command = openInEditor(started.members[0].path);
|
|
15325
|
+
log(` opened ${set ? "the first frame " : ""}with: ${command}${process.env.PIXELKILN_EDITOR ? "" : " (set PIXELKILN_EDITOR to choose the program)"}`);
|
|
15107
15326
|
}
|
|
15108
15327
|
return;
|
|
15109
15328
|
}
|