pixelkiln 0.28.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 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
- const originalExt = path5.extname(spec.outFile);
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 ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
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
- const outputs = resolveEntryOutputs(entry, id, manifestDir);
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
- if (source) {
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
- const entry = lock.entries[`${styleId}/${id}`];
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 handEditBase(spec, lock) {
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 primary = primaryOutput(entry);
8254
- if (!primary || primary.mediaType && primary.mediaType !== "image/png") return null;
8255
- const file = currentEntryOutputPath(entry, spec, 0);
8256
- return existsSync16(file) ? file : null;
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 existsSync16(spec.outFile) ? spec.outFile : null;
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} has ${entry.outputs.length} outputs; hand edits currently cover single-image assets`
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
- if (!existsSync16(editPath)) {
8271
- const base = handEditBase(spec, lock);
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(editPath), { recursive: true });
8278
- await copyFile(base, editPath);
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 HandEditCompanionSchema = z6.object({
8310
- version: z6.literal(1),
8311
- /** sha256 of the generated art the edit started from; null when it started from untracked art. */
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 edit PNG as saved, so a later change by another tool is visible. */
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
- if (input.png.length > MAX_HAND_EDIT_BYTES) {
8344
- throw new ManifestEditError(`the edit is ${input.png.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
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
- let decoded;
8350
- try {
8351
- validateMedia(input.png, MediaType.PNG);
8352
- decoded = decodePng(input.png);
8353
- } catch (error) {
8354
- throw new ManifestEditError(`the edit is not a valid PNG: ${error instanceof Error ? error.message : String(error)}`);
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 base = handEditBase(spec, lock);
8357
- const expected = base ? decodePng(await readFile15(base)) : { width: spec.width, height: spec.height };
8358
- if (decoded.width !== expected.width || decoded.height !== expected.height) {
8359
- throw new ManifestEditError(
8360
- `the edit is ${decoded.width}\xD7${decoded.height}; ${spec.styleId}/${spec.assetId} is ${expected.width}\xD7${expected.height}`
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: 1,
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
- await replaceFile(started.editPath, input.png);
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 { ...started, sha256: digest2, basedOn, companionPath, projectPath };
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
- png: Base64.min(1),
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.editPath);
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
- log2(` hand edit saved from ${request.editor}: ${saved.source}${saved.declared ? " (declared in the manifest)" : ""}${saved.projectPath ? " + project file" : ""}`);
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)`);
@@ -8507,6 +8612,18 @@ function matchesFilter(filter, styleId, assetId) {
8507
8612
  if (filter?.assets?.length && !filter.assets.includes(assetId)) return false;
8508
8613
  return true;
8509
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
+ }
8510
8627
  function metadataFps(entry) {
8511
8628
  if (!entry) return null;
8512
8629
  const namespace = entry.providerMetadata?.[entry.provider];
@@ -8624,6 +8741,8 @@ async function buildGallerySnapshot(opts) {
8624
8741
  const asset = loaded.manifest.assets[spec.assetId] ?? null;
8625
8742
  let outputs;
8626
8743
  let edit = null;
8744
+ let edits = [];
8745
+ let editChanged = [];
8627
8746
  let editStatus = null;
8628
8747
  let editMeta = null;
8629
8748
  if (entry && entry.outputs.length) {
@@ -8634,9 +8753,15 @@ async function buildGallerySnapshot(opts) {
8634
8753
  );
8635
8754
  if (spec.source) {
8636
8755
  const editPath = path22.resolve(root, spec.source);
8637
- const editSha = existsSync17(editPath) ? await sha256File(editPath) : null;
8638
- edit = await describeOutput(media, root, editPath, { sha256: editSha });
8639
- const generated = outputs[0];
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;
8640
8765
  const companion = await readHandEditCompanion(editPath);
8641
8766
  if (companion) {
8642
8767
  const projectPath = handEditProjectPath(editPath);
@@ -8650,14 +8775,30 @@ async function buildGallerySnapshot(opts) {
8650
8775
  editMeta = {
8651
8776
  editor: companion.editor,
8652
8777
  savedAt: companion.savedAt,
8653
- basedOn: companion.basedOn,
8654
- changedSince: editSha !== null && editSha !== companion.sha256,
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
+ }),
8655
8783
  project: project ? portableOutputPath(projectPath, root) : null,
8656
8784
  projectUrl
8657
8785
  };
8658
8786
  }
8659
- const regenerated = companion ? generated?.sha256 !== void 0 && generated.sha256 !== null && companion.basedOn !== generated.sha256 : Boolean(generated?.modifiedAt && edit.modifiedAt && generated.modifiedAt > edit.modifiedAt && generated.exists);
8660
- editStatus = !edit.exists ? "missing" : generated && editSha === generated.sha256 ? "same" : regenerated ? "regenerated-since" : "edited";
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";
8661
8802
  }
8662
8803
  } else if (spec.source) {
8663
8804
  outputs = [await describeOutput(media, root, path22.resolve(root, spec.source))];
@@ -8706,6 +8847,8 @@ async function buildGallerySnapshot(opts) {
8706
8847
  asset,
8707
8848
  source: spec.source ?? null,
8708
8849
  edit,
8850
+ edits,
8851
+ editChanged,
8709
8852
  editStatus,
8710
8853
  editMeta,
8711
8854
  upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
@@ -8763,6 +8906,8 @@ async function buildGallerySnapshot(opts) {
8763
8906
  asset: null,
8764
8907
  source: null,
8765
8908
  edit: null,
8909
+ edits: [],
8910
+ editChanged: [],
8766
8911
  editStatus: null,
8767
8912
  editMeta: null,
8768
8913
  upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
@@ -9697,10 +9842,15 @@ window.addEventListener('message', (e) => {
9697
9842
  // declares the file the same way pixelkiln edit does.
9698
9843
 
9699
9844
  let SHEET = null;
9700
- const editorSource = (item) => item.edit && item.edit.exists ? item.edit
9701
- : item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png' ? item.outputs[0] : null;
9702
- const canEditInBrowser = (item) => EDITOR && EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editorSource(item) &&
9703
- !(item.outputs.length > 1);
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);
9704
9854
  const toBase64 = (buf) => new Promise((resolve, reject) => {
9705
9855
  const r = new FileReader();
9706
9856
  r.onload = () => resolve(String(r.result).slice(String(r.result).indexOf(',') + 1));
@@ -9729,7 +9879,7 @@ function openEditorSheet(item) {
9729
9879
  const bar = el('div', 'rbar');
9730
9880
  const close = el('button', null, 'Close'); close.type = 'button';
9731
9881
  close.onclick = () => closeEditorSheet();
9732
- 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' : ''));
9733
9883
  const status = el('span', 'st');
9734
9884
  const acts = el('div', 'acts');
9735
9885
  const msg = el('span', 'msg');
@@ -9750,23 +9900,34 @@ function openEditorSheet(item) {
9750
9900
  stage.append(frame, loading);
9751
9901
  panel.append(bar, stage);
9752
9902
  host.append(scrim, panel);
9753
- SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src, sentProject: false };
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 };
9754
9904
  sheetStatus('cool', 'loading');
9755
9905
  sheetButtons();
9756
9906
  close.focus({ preventScroll: true });
9757
9907
  }
9758
- // The edit file is what the editor gets; when a browser save kept the layered
9759
- // project beside it, that goes along too and the editor restores the layers,
9760
- // falling back to the flattened PNG if the file cannot be read.
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.
9761
9912
  async function sendOpen() {
9762
- const { item, source } = SHEET;
9913
+ const { item } = SHEET;
9914
+ const sources = editorSources(item);
9763
9915
  const style = snap.styles.find((s) => s.id === item.styleId && s.project === item.project);
9764
- const projectUrl = source === item.edit && item.editMeta && item.editMeta.projectUrl && SHEET.ready.version >= 2 ? item.editMeta.projectUrl : null;
9765
- let png, pxo = null;
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;
9766
9925
  try {
9767
- const res = await fetch(source.url, { cache: 'no-store' });
9768
- if (!res.ok) throw new Error(await res.text());
9769
- png = await res.arrayBuffer();
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
+ }
9770
9931
  if (projectUrl) {
9771
9932
  const p = await fetch(projectUrl, { cache: 'no-store' });
9772
9933
  if (p.ok) pxo = await p.arrayBuffer();
@@ -9778,12 +9939,21 @@ async function sendOpen() {
9778
9939
  if (!SHEET) return;
9779
9940
  const request = 'open-' + (++SHEET.requests);
9780
9941
  SHEET.sentProject = !!pxo;
9942
+ SHEET.roles = set ? item.outputs.map((o) => o.role || null) : null;
9781
9943
  const message = {
9782
9944
  type: 'pixelkiln:open', request,
9783
9945
  asset: { key: item.styleId + '/' + item.assetId, id: item.id, name: item.assetId, width: item.width, height: item.height },
9784
- png, palette: style ? style.palette : [],
9946
+ png: pngs[0], palette: style ? style.palette : [],
9785
9947
  };
9786
- const transfer = [png];
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
+ }
9787
9957
  if (pxo) { message.pxo = pxo; transfer.push(pxo); }
9788
9958
  SHEET.frame.contentWindow.postMessage(message, location.origin, transfer);
9789
9959
  sheetStatus('cool', 'opening');
@@ -9811,8 +9981,9 @@ async function onEditorMessage(m) {
9811
9981
  case 'pixelkiln:opened':
9812
9982
  SHEET.opened = true;
9813
9983
  SHEET.loading.remove();
9814
- if (m.source === 'pxo') sheetStatus('ok', 'editing the edit file with its ' + (m.layers === 1 ? 'layer' : m.layers + ' layers') + ' restored');
9815
- else if (SHEET.sentProject) sheetStatus('warn', 'editing the flattened edit file; its layer file could not be opened');
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');
9816
9987
  else sheetStatus('ok', SHEET.item.edit && SHEET.item.edit.exists ? 'editing the edit file' : 'editing a copy of the generated art');
9817
9988
  sheetButtons();
9818
9989
  break;
@@ -9825,14 +9996,21 @@ async function onEditorMessage(m) {
9825
9996
  const pending = SHEET.pending;
9826
9997
  if (!pending || pending.request !== m.request) return;
9827
9998
  try {
9828
- const body = { png: await toBase64(m.png), editor: SHEET.ready.editor, protocol: SHEET.ready.version };
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
+ }
9829
10007
  if (m.pxo && m.pxo.byteLength) body.pxo = await toBase64(m.pxo);
9830
10008
  await postHandEdit(SHEET.item, 'save-edit', body);
9831
10009
  const saved = snap.items.find((i) => i.id === SHEET.item.id);
9832
10010
  if (saved) SHEET.item = saved;
9833
10011
  SHEET.dirty = false;
9834
10012
  SHEET.pending = null;
9835
- 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.' };
9836
10014
  render();
9837
10015
  if (pending.close) { closeEditorSheet(true); return; }
9838
10016
  sheetStatus('ok', 'saved');
@@ -9980,7 +10158,7 @@ function clearFilters() {
9980
10158
  function thumb(item) {
9981
10159
  const cell = el('div', 'cell');
9982
10160
  // A hand edit is what ships, so it is what the card shows.
9983
- const shown = item.edit && item.edit.url && item.editStatus === 'edited' ? [item.edit] : item.outputs.filter((o) => o.url);
10161
+ const shown = item.edit && item.editStatus === 'edited' && item.edits.every((e) => e.url) ? item.edits : item.outputs.filter((o) => o.url);
9984
10162
  if (!shown.length) {
9985
10163
  cell.classList.add('none');
9986
10164
  cell.append(el('span', null, item.state === 'missing' ? 'not generated yet'
@@ -10394,7 +10572,7 @@ function openCompare() {
10394
10572
  // ---- hand edits -------------------------------------------------------------
10395
10573
 
10396
10574
  const EDIT_STATUS_TEXT = {
10397
- same: 'A copy of the generated art, not changed yet. Open it in your editor and save.',
10575
+ same: 'The generated art\u2019s pixels, not changed yet. Open it in your editor and save.',
10398
10576
  edited: 'Differs from the generated art. mount and pack place this file; the generated file stays as the record.',
10399
10577
  'regenerated-since': 'The generated art changed after this edit was saved \u2014 the edit is based on an older generation.',
10400
10578
  missing: 'The declared file is not on disk.',
@@ -10405,12 +10583,16 @@ async function postHandEdit(item, action, extra) {
10405
10583
  if (item.project) body.project = item.project;
10406
10584
  snap = await postEdit(body);
10407
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');
10408
10590
  function handEditSection(item) {
10409
- const canStart = EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 &&
10410
- 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);
10411
10592
  if (!item.edit && !canStart) return null;
10593
+ const set = item.edits.length > 1;
10412
10594
  const s = el('section', 'meta');
10413
- s.append(el('h3', null, 'Hand edit'));
10595
+ s.append(el('h3', null, set ? 'Hand edit (' + item.edits.length + ' frames)' : 'Hand edit'));
10414
10596
  if (item.edit) {
10415
10597
  const pair = el('div', 'pair');
10416
10598
  const shared = displayScale(item.width, item.height, 200, 160);
@@ -10424,24 +10606,36 @@ function handEditSection(item) {
10424
10606
  f.append(el('figcaption', null, label));
10425
10607
  return f;
10426
10608
  };
10427
- pair.append(fig(item.outputs[0], 'generated'), fig(item.edit, 'edit'));
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));
10428
10613
  s.append(pair);
10429
10614
  const dl = el('dl');
10430
10615
  const tone = item.editStatus === 'edited' ? 'ok' : item.editStatus === 'same' ? 'dim' : 'warn';
10431
10616
  const st = el('span', 'state-' + tone); st.append(el('i', 'dot ' + tone), document.createTextNode(' ' + item.editStatus));
10432
10617
  row(dl, 'status', st);
10433
10618
  row(dl, 'why', EDIT_STATUS_TEXT[item.editStatus] || '');
10434
- row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
10435
- if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
10436
- row(dl, 'saved', fmtWhen(item.edit.modifiedAt));
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)));
10437
10629
  if (item.editMeta) {
10438
- row(dl, 'editor', item.editMeta.editor + ', ' + fmtWhen(item.editMeta.savedAt) + (item.editMeta.changedSince ? ' \u2014 the file changed since' : ''));
10439
- 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 });
10440
10632
  if (item.editMeta.project) row(dl, 'layers', item.editMeta.project, { mono: true });
10441
10633
  }
10442
10634
  s.append(dl);
10443
10635
  } else {
10444
- s.append(el('div', 'state-dim', '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.'));
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.'));
10445
10639
  }
10446
10640
  if (EDITABLE && item.declared && projectOf(item)?.manifestSha256) {
10447
10641
  const acts = el('div', 'hand-actions');
@@ -10469,10 +10663,10 @@ function handEditSection(item) {
10469
10663
  }
10470
10664
  if (item.edit) {
10471
10665
  go('Open in desktop editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
10472
- go('Detach edit', 'detach-edit', {}, 'Detached. The file is still at ' + item.edit.path + '; the generated art is placed again.');
10666
+ go('Detach edit', 'detach-edit', {}, 'Detached. The file' + (set ? 's are' : ' is') + ' still at ' + item.source + '; the generated art is placed again.');
10473
10667
  } else if (canStart) {
10474
- 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.');
10475
- 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.');
10476
10670
  }
10477
10671
  acts.append(msg);
10478
10672
  s.append(acts);
@@ -11785,8 +11979,8 @@ var pin_default = {
11785
11979
  pixelorama: "v1.2.2",
11786
11980
  godot: "4.7.2",
11787
11981
  extensionsApi: 9,
11788
- protocol: 2,
11789
- release: "editor-pixelorama-v1.2.2-pk.10",
11982
+ protocol: 3,
11983
+ release: "editor-pixelorama-v1.2.2-pk.14",
11790
11984
  files: {
11791
11985
  "index.apple-touch-icon.png": {
11792
11986
  sha256: "ee5434873e0fed91e9a888c3ea5a8b79beac25999308be7a6eb41bf4ce640d27",
@@ -11801,7 +11995,7 @@ var pin_default = {
11801
11995
  bytes: 7298
11802
11996
  },
11803
11997
  "index.html": {
11804
- sha256: "ad777659ca39a22872437d44595b749c1637f83994d5f82d50dca861f2186b9c",
11998
+ sha256: "2974146c89f4df9f796e840947c334aaf3e4fa3dd18bcd7e08db2eabfd0f086e",
11805
11999
  bytes: 5442
11806
12000
  },
11807
12001
  "index.icon.png": {
@@ -11813,8 +12007,8 @@ var pin_default = {
11813
12007
  bytes: 279815
11814
12008
  },
11815
12009
  "index.pck": {
11816
- sha256: "546a3f9851df0854c9f6052f29f1e559de79deb53b936d44a72c3475234c26ff",
11817
- bytes: 6427868
12010
+ sha256: "736bd383c82fdadf31cc894caf08490b99c5df572aa20db683972c35c7a0465b",
12011
+ bytes: 6429628
11818
12012
  },
11819
12013
  "index.png": {
11820
12014
  sha256: "6e3ca3eb4a99e4c07bdd89ebc873b440ec0ffd66ce1955160da9e7ed80d538d7",
@@ -15121,12 +15315,14 @@ async function main() {
15121
15315
  return;
15122
15316
  }
15123
15317
  const started = await startHandEdit(loaded, lock, spec);
15124
- log(` ${started.created ? "created" : "found"} ${started.source}${started.declared ? " and declared it as the asset's source" : ""}`);
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)}`);
15125
15321
  if (args.noOpen) {
15126
- 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`);
15127
15323
  } else {
15128
- const command = openInEditor(started.editPath);
15129
- 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)"}`);
15130
15326
  }
15131
15327
  return;
15132
15328
  }