pixelkiln 0.52.0 → 0.53.1

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.
@@ -111,6 +111,20 @@ const paletteRuleOf = (item) => {
111
111
  const style = snap.styles.find((s) => s.id === item.styleId && s.project === item.project);
112
112
  return style && style.enforcePalette && style.palette.length >= 2 ? style.palette : null;
113
113
  };
114
+ /**
115
+ * Whether this item has usable pixels to revise: committed `source` art
116
+ * always qualifies, otherwise it needs a current, downloaded generation, or
117
+ * (for a quality-gated style) a current, human-approved refined output. This
118
+ * mirrors docs/REVISIONS.md's dependency gate closely enough to gate the
119
+ * button; the server's own plan is still the final word once the revision
120
+ * asset exists, and reports `blocked` with the real reason if this guess
121
+ * were ever wrong.
122
+ */
123
+ const isRevisable = (item) => {
124
+ if (item.source) return true;
125
+ if (item.quality) return item.quality.review?.status === 'approved' && !!item.quality.check?.current;
126
+ return item.status === 'downloaded' && item.state === 'ok';
127
+ };
114
128
  const activeJobFor = (item) => GEN.jobs.find((job) => ACTIVE_PHASES.has(job.phase) && job.project === item.project && job.keys.includes(item.key)) || null;
115
129
  const JOB_VERB = { generate: 'regenerating', resume: 'resuming', refresh: 'pulling upstream changes for', restore: 'restoring', revert: 'bringing back a previous generation of' };
116
130
  const PHASE_TEXT = { queued: 'queued', submitting: 'submitting to the provider', polling: 'waiting for the provider to finish', fetching: 'downloading the result', review: 'candidates are ready to review' };
@@ -1570,7 +1584,7 @@ function addAssetForm(style) {
1570
1584
  const form = el('form', 'edit');
1571
1585
  form.append(el('h3', null, 'New asset in ' + style.id));
1572
1586
  const id = el('input'); id.type = 'text'; id.placeholder = 'asset-id'; id.required = true; id.autocomplete = 'off';
1573
- id.pattern = '[^/\\]+';
1587
+ id.pattern = '[^\\/\\\\]+';
1574
1588
  const prompt = el('textarea'); prompt.placeholder = 'What to generate. The style adds its prefix and suffix.'; prompt.required = true;
1575
1589
  const width = numberInput(null, 'style default'), height = numberInput(null, 'style default');
1576
1590
  const category = el('input'); category.type = 'text'; category.placeholder = 'optional subfolder';
@@ -1613,6 +1627,54 @@ function addAssetForm(style) {
1613
1627
  return form;
1614
1628
  }
1615
1629
 
1630
+ /**
1631
+ * A new `image-to-image` revision of `item`, restricted to `item`'s own
1632
+ * style (a revision's `from` resolves per style, and this button starts
1633
+ * from one specific generation). `inpaint` needs a mask upload the gallery
1634
+ * does not offer yet, so this form only ever creates `image-to-image`.
1635
+ */
1636
+ function newRevisionForm(item) {
1637
+ const pr = projectOf(item);
1638
+ const form = el('form', 'edit');
1639
+ form.append(el('h3', null, 'New revision of ' + item.assetId));
1640
+ const id = el('input'); id.type = 'text'; id.placeholder = 'asset-id'; id.required = true; id.autocomplete = 'off';
1641
+ id.pattern = '[^\\/\\\\]+';
1642
+ const prompt = el('textarea'); prompt.placeholder = 'Edit instruction, e.g. "add snow on the roof".'; prompt.required = true;
1643
+ const strength = el('input'); strength.type = 'number'; strength.min = '0'; strength.max = '1'; strength.step = '0.05'; strength.placeholder = 'provider default';
1644
+ const row1 = el('div', 'row');
1645
+ row1.append(field('new asset id', id), field('strength', strength, 'PixelLab rejects an explicit strength for image-to-image; leave this blank there.'));
1646
+ form.append(row1, field('edit instruction', prompt, 'The style still adds its prefix and suffix.'));
1647
+ const actions = el('div', 'actions');
1648
+ const save = el('button', 'primary', 'Create revision'); save.type = 'submit';
1649
+ const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { ui.editing = null; renderDrawer(); };
1650
+ const msg = el('span', 'msg');
1651
+ actions.append(save, cancel, msg);
1652
+ form.append(actions);
1653
+ form.onsubmit = async (e) => {
1654
+ e.preventDefault();
1655
+ save.disabled = true; msg.className = 'msg'; msg.textContent = 'saving…';
1656
+ const revision = { mode: 'image-to-image', from: item.assetId };
1657
+ if (strength.value.trim() !== '') revision.strength = Number(strength.value);
1658
+ const asset = { prompt: prompt.value, styles: [item.styleId], revision };
1659
+ try {
1660
+ const body = { action: 'add-asset', assetId: id.value.trim(), expectedSha256: pr.manifestSha256, asset };
1661
+ if (item.project) body.project = item.project;
1662
+ snap = await postEdit(body);
1663
+ const newId = (item.project ? item.project + ':' : '') + item.styleId + '/' + id.value.trim();
1664
+ ui.editing = null;
1665
+ ui.notice = { id: newId, text: 'Added to the manifest. Nothing is generated until you run pixelkiln gen.' };
1666
+ render();
1667
+ if (snap.items.some((i) => i.id === newId)) openItem(newId);
1668
+ } catch (err) {
1669
+ save.disabled = false;
1670
+ msg.className = 'msg bad';
1671
+ msg.textContent = err.message + (err.status === 409 ? ' Press Refresh.' : '');
1672
+ }
1673
+ };
1674
+ setTimeout(() => id.focus(), 0);
1675
+ return form;
1676
+ }
1677
+
1616
1678
  function editForm(item) {
1617
1679
  const pr = projectOf(item);
1618
1680
  const a = item.asset;
@@ -1980,9 +2042,18 @@ function renderDrawer() {
1980
2042
  body.append(s);
1981
2043
  }
1982
2044
  const children = snap.items.filter((i) => i.revisionParentKey === item.key && i.project === item.project);
1983
- if (children.length) {
2045
+ const reviseKey = 'revise:' + item.id;
2046
+ const canRevise = canEdit && isRevisable(item);
2047
+ if (children.length || canRevise) {
1984
2048
  const { s, dl } = section('Revisions from this asset');
1985
2049
  for (const c of children) row(dl, c.revision ? c.revision.mode : 'child', keyLink(item, c.key));
2050
+ if (canRevise) {
2051
+ const revise = el('button', ui.editing === reviseKey ? null : 'add', ui.editing === reviseKey ? 'Cancel' : '+ New revision');
2052
+ revise.type = 'button';
2053
+ revise.onclick = () => { ui.editing = ui.editing === reviseKey ? null : reviseKey; ui.notice = null; renderDrawer(); };
2054
+ s.append(revise);
2055
+ if (ui.editing === reviseKey) s.append(newRevisionForm(item));
2056
+ }
1986
2057
  body.append(s);
1987
2058
  }
1988
2059
 
package/dist/index.cjs CHANGED
@@ -188,6 +188,7 @@ __export(index_exports, {
188
188
  mountStyle: () => mountStyle,
189
189
  normalizeLockOutputPaths: () => normalizeLockOutputPaths,
190
190
  normalizeRefinementPalette: () => normalizeRefinementPalette,
191
+ normalizeTerrainTiles: () => normalizeTerrainTiles,
191
192
  normalizeTileRules: () => normalizeTileRules,
192
193
  openExternal: () => openExternal,
193
194
  openInEditor: () => openInEditor,
@@ -10486,6 +10487,25 @@ function normalizeTileRules(raw) {
10486
10487
  raw
10487
10488
  };
10488
10489
  }
10490
+ function normalizeTerrainTiles(metadata) {
10491
+ if (!isRecord2(metadata) || !Array.isArray(metadata.terrainTiles)) return null;
10492
+ const terrains = Array.isArray(metadata.terrainTypes) ? metadata.terrainTypes.filter((item) => typeof item === "string") : [];
10493
+ if (terrains.length < 2) return null;
10494
+ const masks = {};
10495
+ metadata.terrainTiles.forEach((tile, index) => {
10496
+ if (!isRecord2(tile) || !isRecord2(tile.corners)) return;
10497
+ const corners = tile.corners;
10498
+ const bit = (corner) => corners[corner] === terrains[0] ? 1 : 0;
10499
+ masks[index] = bit("NW") << 3 | bit("NE") << 2 | bit("SW") << 1 | bit("SE");
10500
+ });
10501
+ return {
10502
+ ruleType: "corner",
10503
+ arity: 4,
10504
+ terrains,
10505
+ masks,
10506
+ raw: metadata
10507
+ };
10508
+ }
10489
10509
  function exportTileset(entry, spec, options) {
10490
10510
  let outputs = resolveSpecEntryOutputs(entry, spec);
10491
10511
  if (!outputs.length) throw new Error(`${spec.styleId}/${spec.assetId} has no downloaded outputs`);
@@ -10506,14 +10526,16 @@ function exportTileset(entry, spec, options) {
10506
10526
  }
10507
10527
  const metadata = entry.providerMetadata?.[entry.provider];
10508
10528
  const rawRules = isRecord2(metadata) ? metadata.tileRules : void 0;
10509
- const rules = normalizeTileRules(rawRules);
10529
+ const terrainRules = normalizeTerrainTiles(metadata);
10530
+ const rules = normalizeTileRules(rawRules) ?? terrainRules;
10531
+ const providerRules = isRecord2(rawRules) ? rawRules : terrainRules ? metadata : null;
10510
10532
  const frames = packed.atlas.frames;
10511
10533
  const generic = buildGeneric(
10512
10534
  spec,
10513
10535
  options.imageName,
10514
10536
  packed.atlas,
10515
10537
  frames,
10516
- isRecord2(rawRules) ? rawRules : null,
10538
+ providerRules,
10517
10539
  rules
10518
10540
  );
10519
10541
  if (options.format === "generic") {
@@ -10526,7 +10548,7 @@ function exportTileset(entry, spec, options) {
10526
10548
  };
10527
10549
  }
10528
10550
  assertUniformTiles(generic);
10529
- if ((rawRules !== void 0 || entry.tileFeature) && !rules) {
10551
+ if ((rawRules !== void 0 || entry.tileFeature || entry.generator === "terrain") && !rules) {
10530
10552
  throw new Error(
10531
10553
  `${options.format} export needs recognized adjacency rules; use --format generic to retain this provider rule object without guessing`
10532
10554
  );
@@ -12341,7 +12363,19 @@ var NewAssetSchema = import_zod7.z.object({
12341
12363
  size: import_zod7.z.number().int().optional(),
12342
12364
  tags: import_zod7.z.array(import_zod7.z.string()).optional(),
12343
12365
  /** Restrict to these styles; empty or omitted means every style. */
12344
- styles: import_zod7.z.array(import_zod7.z.string().min(1)).optional()
12366
+ styles: import_zod7.z.array(import_zod7.z.string().min(1)).optional(),
12367
+ /**
12368
+ * A controlled regeneration of another asset in the same manifest,
12369
+ * instead of a fresh generation. `inpaint` needs a mask upload the
12370
+ * gallery does not offer yet, so only `image-to-image` can be created
12371
+ * here; hand-edit the manifest for the other modes.
12372
+ */
12373
+ revision: import_zod7.z.object({
12374
+ mode: import_zod7.z.literal("image-to-image"),
12375
+ /** Asset id of the parent this revises; must already exist. */
12376
+ from: import_zod7.z.string().min(1),
12377
+ strength: import_zod7.z.number().min(0).max(1).optional()
12378
+ }).strict().optional()
12345
12379
  }).strict();
12346
12380
  var CANDIDATE_OPTION = {
12347
12381
  retrodiffusion: "numImages",
@@ -12507,6 +12541,9 @@ function applyEdit(raw, edit) {
12507
12541
  }
12508
12542
  asset2.styles = styles;
12509
12543
  }
12544
+ if (edit.asset.revision && !Object.hasOwn(raw.assets, edit.asset.revision.from)) {
12545
+ throw new ManifestEditError(`revision parent "${edit.asset.revision.from}" is not declared by the manifest`);
12546
+ }
12510
12547
  raw.assets[edit.assetId] = asset2;
12511
12548
  return;
12512
12549
  }
@@ -15201,6 +15238,7 @@ async function workspaceStatus(ws, dir) {
15201
15238
  mountStyle,
15202
15239
  normalizeLockOutputPaths,
15203
15240
  normalizeRefinementPalette,
15241
+ normalizeTerrainTiles,
15204
15242
  normalizeTileRules,
15205
15243
  openExternal,
15206
15244
  openInEditor,