pixelkiln 0.40.0 → 0.41.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.
@@ -93,6 +93,7 @@ main { width:min(100%,var(--content)); margin:0 auto; border-inline:1px solid va
93
93
  .aid { font:600 13px/1.3 var(--mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
94
94
  .cmeta { display:flex; gap:8px; align-items:center; color:var(--dim); font-size:11.5px;
95
95
  font-variant-numeric:tabular-nums; white-space:nowrap; overflow:hidden; }
96
+ .family { font-size:11px; color:var(--dim); margin-top:2px; }
96
97
  .cmeta .st { display:inline-flex; align-items:center; gap:5px; }
97
98
  .empty { padding:64px 22px; color:var(--dim); text-align:center; }
98
99
  .empty p { margin:0 0 12px; }
@@ -97,6 +97,14 @@ const ACTIVE_PHASES = new Set(['queued', 'submitting', 'polling', 'fetching', 'r
97
97
  const PHASE_TONE = { queued: 'cool', submitting: 'cool', polling: 'cool', fetching: 'cool', review: 'warn', done: 'ok', failed: 'bad' };
98
98
  // The job still working on this record, if any: what the drawer and its card
99
99
  // show while a regeneration, restore, or pull is in flight.
100
+ /** "state of mira", "loop of mira.chair_spin, east": where a character family member sits. */
101
+ const familyLabel = (item) => {
102
+ const c = item.character;
103
+ if (!c || c.kind === 'base') return null;
104
+ const parent = c.parentKey ? c.parentKey.split('/').slice(1).join('/') : null;
105
+ if (c.kind === 'state') return parent ? 'state of ' + parent : 'state';
106
+ return (parent ? 'loop of ' + parent : 'loop') + (c.direction ? ', ' + c.direction : '');
107
+ };
100
108
  /** The style's palette rule as the manifest has it now, or null. */
101
109
  const paletteRuleOf = (item) => {
102
110
  const style = snap.styles.find((s) => s.id === item.styleId && s.project === item.project);
@@ -819,6 +827,8 @@ function card(item) {
819
827
  meta.append(st, el('span', null, item.width + '×' + item.height));
820
828
  if (item.cost) meta.append(el('span', null, fmtCost(item.costUnit, item.cost)));
821
829
  body.append(meta);
830
+ const family = familyLabel(item);
831
+ if (family) body.append(el('div', 'family', family));
822
832
  const cell = thumb(item);
823
833
  if (slot >= 0) cell.append(el('span', 'slot', String(slot + 1)));
824
834
  if (item.editStatus === 'edited' || item.editStatus === 'regenerated-since') {
@@ -899,6 +909,13 @@ function renderMain(items) {
899
909
  meta.append(sw);
900
910
  }
901
911
  if (s.quality) meta.append(el('span', null, 'quality profile'));
912
+ if (s.characters) {
913
+ const parts = [];
914
+ parts.push(s.characters.bases + (s.characters.bases === 1 ? ' character' : ' characters'));
915
+ if (s.characters.states) parts.push(s.characters.states + (s.characters.states === 1 ? ' state' : ' states'));
916
+ if (s.characters.animations) parts.push(s.characters.animations + (s.characters.animations === 1 ? ' loop' : ' loops'));
917
+ meta.append(el('span', null, parts.join(', ')));
918
+ }
902
919
  meta.append(el('span', null, sec.items.length + ' of ' + s.items + (s.items === 1 ? ' asset' : ' assets')));
903
920
  if (Object.keys(s.spendByUnit).length) meta.append(el('span', null, fmtSpend(s.spendByUnit)));
904
921
  head.append(meta);
@@ -1910,6 +1927,28 @@ function renderDrawer() {
1910
1927
  const versions = historySection(item);
1911
1928
  if (versions) body.append(versions);
1912
1929
 
1930
+ if (item.character) {
1931
+ const c = item.character;
1932
+ const { s, dl } = section('Character');
1933
+ row(dl, 'kind', c.kind === 'animation' ? 'animation' : c.kind === 'state' ? 'state (a pose or outfit of its parent)' : 'base');
1934
+ if (c.parentKey) row(dl, 'parent', keyLink(item, c.parentKey));
1935
+ row(dl, 'engine', c.mode);
1936
+ if (c.kind === 'animation') row(dl, 'direction', c.direction || '—');
1937
+ else row(dl, 'directions', String(c.directions));
1938
+ if (c.characterId) row(dl, 'character id', c.characterId, { mono: true, copy: c.characterId });
1939
+ const family = snap.items.filter((i) => i.character && i.character.parentKey === item.key && i.project === item.project);
1940
+ if (family.length) {
1941
+ const list = el('div');
1942
+ for (const f of family) {
1943
+ const line = el('div');
1944
+ line.append(document.createTextNode((f.character.kind === 'state' ? 'state ' : 'loop ') + (f.character.direction ? f.character.direction + ' ' : '')), keyLink(item, f.key));
1945
+ list.append(line);
1946
+ }
1947
+ row(dl, family.length === 1 ? 'depends on this' : family.length + ' depend on this', list);
1948
+ }
1949
+ body.append(s);
1950
+ }
1951
+
1913
1952
  if (item.revision || item.revisionParentKey) {
1914
1953
  const { s, dl } = section('Lineage');
1915
1954
  row(dl, 'mode', item.revision ? item.revision.mode : (item.asset && item.asset.revision && item.asset.revision.mode));
package/dist/index.cjs CHANGED
@@ -2297,12 +2297,7 @@ var PixelLabProvider = class _PixelLabProvider {
2297
2297
  const character = await this.client.getCharacter(jobId);
2298
2298
  if (character.status === "failed") return { status: "failed", error: "character generation failed upstream" };
2299
2299
  if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
2300
- const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
2301
- const sources = [];
2302
- for (const direction of order) {
2303
- const url = character.rotation_urls[direction];
2304
- if (url) sources.push({ url, role: direction });
2305
- }
2300
+ const sources = rotationSources(character);
2306
2301
  if (!sources.length) return { status: "failed", error: "character completed with no rotation images" };
2307
2302
  return {
2308
2303
  status: "ready",
@@ -2594,6 +2589,27 @@ var PixelLabProvider = class _PixelLabProvider {
2594
2589
  })))
2595
2590
  };
2596
2591
  }
2592
+ /**
2593
+ * A character's rotation and frame URLs carry its last edit time as a
2594
+ * cache-buster, so the record's URLs can name bytes a cache still holds
2595
+ * after an edit in PixelLab's editor. Objects keep stable URLs.
2596
+ */
2597
+ async refreshSources(objectId, context) {
2598
+ if (context.generator !== "character") return void 0;
2599
+ const [characterId, groupId] = objectId.split("#");
2600
+ const character = await this.client.getCharacter(characterId).catch((err) => {
2601
+ if (err instanceof PixelLabError && err.status === 404) return null;
2602
+ throw err;
2603
+ });
2604
+ if (!character) return null;
2605
+ if (!groupId) return character.status === "completed" ? rotationSources(character) : void 0;
2606
+ const recorded = context.metadata?.character ?? {};
2607
+ const direction = recorded.direction ?? "south";
2608
+ const group = character.animations.find((g) => g.animation_group_id === groupId);
2609
+ const frames = group?.directions.find((d) => d.direction === direction && d.frames.length)?.frames ?? (recorded.animationName ? findAnimation(character, recorded.animationName, direction, recorded.animationId ?? null)?.frames : void 0);
2610
+ if (!frames) return null;
2611
+ return frames.map((url, index) => ({ url, role: `frame-${String(index).padStart(2, "0")}` }));
2612
+ }
2597
2613
  };
2598
2614
  function remoteCharacter(character) {
2599
2615
  return {
@@ -2624,6 +2640,15 @@ function firstUrl(urls) {
2624
2640
  if (!urls) return null;
2625
2641
  return Object.values(urls).find((u) => typeof u === "string") ?? null;
2626
2642
  }
2643
+ function rotationSources(character) {
2644
+ const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
2645
+ const sources = [];
2646
+ for (const direction of order) {
2647
+ const url = character.rotation_urls?.[direction];
2648
+ if (url) sources.push({ url, role: direction });
2649
+ }
2650
+ return sources;
2651
+ }
2627
2652
  function findAnimation(character, name, direction, animationId) {
2628
2653
  for (const group of character.animations) {
2629
2654
  const match = group.directions.find((d) => d.direction === direction && d.frames.length > 0);
@@ -4554,6 +4579,18 @@ var FakeProvider = class {
4554
4579
  if (!character) throw new Error(`fake provider has no character ${id}`);
4555
4580
  return character;
4556
4581
  }
4582
+ /** Characters answer with whatever the store holds now; other objects keep their URLs. */
4583
+ async refreshSources(objectId, context) {
4584
+ if (context.generator !== "character") return void 0;
4585
+ const [characterId, groupId] = objectId.split("#");
4586
+ const character = this.characters.get(characterId);
4587
+ if (!character) return null;
4588
+ if (!groupId) return character.rotations;
4589
+ const direction = context.metadata?.character?.direction ?? "south";
4590
+ const animation = character.animations.find((a) => a.groupId === groupId && a.direction === direction);
4591
+ if (!animation) return null;
4592
+ return animation.frames.map((url, index) => ({ url, role: `frame-${String(index).padStart(2, "0")}` }));
4593
+ }
4557
4594
  async delete(assetId) {
4558
4595
  this.deleted.push(assetId);
4559
4596
  this.assets.delete(assetId);
@@ -8232,7 +8269,40 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8232
8269
  result.skipped++;
8233
8270
  continue;
8234
8271
  }
8235
- const sources = entry.sourceUrls?.length ? entry.sourceUrls : entry.sourceUrl ? [{ url: entry.sourceUrl }] : opts.repair && entry.outputs.length ? entry.outputs.map((o) => ({ url: "", role: o.role, mediaType: o.mediaType })) : [];
8272
+ const recordedSources = entry.sourceUrls?.length ? entry.sourceUrls : entry.sourceUrl ? [{ url: entry.sourceUrl }] : opts.repair && entry.outputs.length ? entry.outputs.map((o) => ({ url: "", role: o.role, mediaType: o.mediaType })) : [];
8273
+ let sources = recordedSources;
8274
+ let rotated = false;
8275
+ if (opts.refresh && entry.status === "downloaded" && entry.objectId && provider.refreshSources) {
8276
+ let current;
8277
+ try {
8278
+ current = await provider.refreshSources(entry.objectId, {
8279
+ generator: entry.generator,
8280
+ metadata: entry.providerMetadata?.[provider.id],
8281
+ sources: recordedSources
8282
+ });
8283
+ } catch (err) {
8284
+ const message6 = err instanceof Error ? err.message : String(err);
8285
+ upsert(lock, key, { status: "download-failed", error: `download failed: ${message6}` });
8286
+ result.failed++;
8287
+ log(` FAILED ${key}: ${message6}`);
8288
+ await saveLock(lockPath, lock);
8289
+ continue;
8290
+ }
8291
+ if (current === null) {
8292
+ upsert(lock, key, { status: "download-failed", error: `download failed: ${entry.objectId} no longer exists upstream` });
8293
+ result.failed++;
8294
+ log(` FAILED ${key}: ${entry.objectId} no longer exists upstream`);
8295
+ await saveLock(lockPath, lock);
8296
+ continue;
8297
+ }
8298
+ if (current) {
8299
+ sources = current.map((source) => ({
8300
+ ...source,
8301
+ mediaType: source.mediaType ?? recordedSources.find((r) => r.role === source.role)?.mediaType
8302
+ }));
8303
+ rotated = sources.length !== recordedSources.length || sources.some((source, index) => source.url !== recordedSources[index]?.url || source.role !== recordedSources[index]?.role);
8304
+ }
8305
+ }
8236
8306
  if (!sources.length) {
8237
8307
  upsert(lock, key, {
8238
8308
  status: "download-failed",
@@ -8361,6 +8431,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8361
8431
  log(` wrote ${import_node_path17.default.relative(process.cwd(), target)}`);
8362
8432
  }
8363
8433
  if (opts.refresh && !wrote) {
8434
+ if (rotated) {
8435
+ const persistent = sources.filter((source) => shouldPersistSourceUrl(source.url));
8436
+ upsert(lock, key, { sourceUrl: persistent[0]?.url ?? null, sourceUrls: persistent });
8437
+ }
8364
8438
  result.unchanged = (result.unchanged ?? 0) + 1;
8365
8439
  continue;
8366
8440
  }
@@ -11509,6 +11583,32 @@ async function samePixels(a, b) {
11509
11583
  }
11510
11584
  }
11511
11585
  var metadataFps = (entry) => entry ? frameSetFps(entry) : null;
11586
+ function describeCharacter(spec, entry) {
11587
+ const recorded = entry?.providerMetadata?.[entry.provider]?.character;
11588
+ if (spec?.character) {
11589
+ const character = spec.character;
11590
+ return {
11591
+ kind: character.kind,
11592
+ parentKey: character.parentAssetId ? lockKey(spec.styleId, character.parentAssetId) : null,
11593
+ mode: character.kind === "animation" ? character.animation.mode : character.mode,
11594
+ directions: character.directions,
11595
+ direction: character.animation?.direction ?? null,
11596
+ characterId: recorded?.characterId ?? entry?.objectId?.split("#")[0] ?? null
11597
+ };
11598
+ }
11599
+ if (entry?.generator === "character") {
11600
+ const kind = recorded?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
11601
+ return {
11602
+ kind,
11603
+ parentKey: null,
11604
+ mode: recorded?.mode ?? "standard",
11605
+ directions: recorded?.directions ?? entry.outputs.length,
11606
+ direction: recorded?.direction ?? null,
11607
+ characterId: recorded?.characterId ?? entry.objectId?.split("#")[0] ?? null
11608
+ };
11609
+ }
11610
+ return null;
11611
+ }
11512
11612
  async function fileInfo(absolutePath) {
11513
11613
  try {
11514
11614
  const info = await (0, import_promises20.stat)(absolutePath);
@@ -11717,6 +11817,7 @@ async function buildGallerySnapshot(opts) {
11717
11817
  currentSpecHash: spec.specHash,
11718
11818
  revision: entry?.revision ?? null,
11719
11819
  revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
11820
+ character: describeCharacter(spec, entry),
11720
11821
  outputs,
11721
11822
  quality,
11722
11823
  providerMetadata: entry?.providerMetadata ?? {},
@@ -11778,6 +11879,7 @@ async function buildGallerySnapshot(opts) {
11778
11879
  currentSpecHash: null,
11779
11880
  revision: entry.revision,
11780
11881
  revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
11882
+ character: describeCharacter(void 0, entry),
11781
11883
  outputs,
11782
11884
  quality: null,
11783
11885
  providerMetadata: entry.providerMetadata ?? {},
@@ -11834,6 +11936,11 @@ async function buildGallerySnapshot(opts) {
11834
11936
  view: style?.view ?? null,
11835
11937
  noBackground: style?.noBackground ?? true,
11836
11938
  quality: Boolean(style?.quality),
11939
+ characters: style?.generator === "character" ? {
11940
+ bases: styleItems.filter((item) => item.character?.kind === "base").length,
11941
+ states: styleItems.filter((item) => item.character?.kind === "state").length,
11942
+ animations: styleItems.filter((item) => item.character?.kind === "animation").length
11943
+ } : null,
11837
11944
  tags: style?.tags ?? [],
11838
11945
  extends: typeof rawStyle?.extends === "string" ? rawStyle.extends : null,
11839
11946
  ownFields: rawStyle ? Object.keys(rawStyle).filter((key) => key !== "extends") : [],