pixelkiln 0.78.0 → 0.80.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.
@@ -466,3 +466,21 @@ td.col-on { background:rgba(255,107,53,.08); }
466
466
  .selbar .msg { font-size:12px; color:var(--dim); }
467
467
  .selbar .msg.bad { color:var(--bad); } .selbar .msg.ok { color:var(--ok); }
468
468
  @media (max-width: 640px) { .selbar { padding:10px 12px; } }
469
+ .lg-add { width:76px; height:76px; padding:0; font:400 22px/1 var(--mono); color:var(--dim); border:1px dashed var(--line-strong); background:transparent; }
470
+ .lg-add:hover, .lg-add.on { color:var(--accent-soft); border-color:var(--accent); }
471
+ .gap-panel { margin-top:10px; border:1px solid var(--accent); padding:12px; display:grid; gap:8px; max-width:720px; }
472
+ .gap-panel h4 { margin:0; font:650 12.5px/1.2 var(--mono); color:var(--accent-soft); }
473
+ .gap-options { display:grid; gap:4px; }
474
+ .gap-options .field.check { display:flex; align-items:baseline; gap:6px; font-size:12.5px; }
475
+ .gap-price { font:11.5px/1.45 var(--mono); }
476
+ .gap-panel .actions { display:flex; gap:6px; flex-wrap:wrap; }
477
+ @media (max-width: 640px) { .lg-add { width:56px; height:56px; } }
478
+
479
+ /* ---- drawer tabs ---- */
480
+ .dtabs { margin-top:14px; }
481
+ .dtabs .tablist { display:flex; gap:0; border-bottom:1px solid var(--line-strong); position:sticky; top:0; z-index:2; background:var(--panel); }
482
+ .dtabs .tablist button { border:0; border-bottom:2px solid transparent; margin-bottom:-1px; padding:8px 12px; font:600 12.5px/1 var(--mono); color:var(--dim); }
483
+ .dtabs .tablist button:hover { color:var(--text); }
484
+ .dtabs .tablist button.on { color:var(--accent-soft); border-bottom-color:var(--accent); }
485
+ .dtabs .tabpanel[hidden] { display:none; }
486
+ .dtabs .tabpanel > section.meta:first-child { border-top:0; }
@@ -46,10 +46,11 @@
46
46
  fps: null,
47
47
  onion: false,
48
48
  selecting: false,
49
- selected: /* @__PURE__ */ new Set()
49
+ selected: /* @__PURE__ */ new Set(),
50
+ drawerTab: "overview"
50
51
  };
51
52
  var PREFS_KEY = "pixelkiln.gallery.prefs";
52
- var PREF_NAMES = ["playLoops", "backdrop", "notify", "onion"];
53
+ var PREF_NAMES = ["playLoops", "backdrop", "notify", "onion", "drawerTab"];
53
54
  function loadPrefs() {
54
55
  try {
55
56
  const saved = JSON.parse(localStorage.getItem(PREFS_KEY) || "{}");
@@ -2645,7 +2646,8 @@
2645
2646
  panel: null,
2646
2647
  anims: [],
2647
2648
  columns: /* @__PURE__ */ new Map(),
2648
- turn: null
2649
+ turn: null,
2650
+ gap: null
2649
2651
  };
2650
2652
  renderFamily();
2651
2653
  }
@@ -2938,7 +2940,20 @@
2938
2940
  const td = el("td");
2939
2941
  column(d, td);
2940
2942
  const item = cells.find((l) => l.character.direction === d);
2941
- td.append(item ? loopCell(item, parent, d) : el("span", "lg-none", "·"));
2943
+ if (item) td.append(loopCell(item, parent, d));
2944
+ else if (fillable(root, parent, cells, d)) {
2945
+ const open = F.gap && F.gap.row === k && F.gap.direction === d;
2946
+ const add = el("button", "lg-add" + (open ? " on" : ""), "+");
2947
+ add.type = "button";
2948
+ add.title = "Add " + name + " facing " + d;
2949
+ add.setAttribute("aria-label", add.title);
2950
+ add.onclick = () => {
2951
+ F.gap = open ? null : { row: k, direction: d };
2952
+ F.drawn = null;
2953
+ renderFamily();
2954
+ };
2955
+ td.append(add);
2956
+ } else td.append(el("span", "lg-none", "·"));
2942
2957
  tr.append(td);
2943
2958
  }
2944
2959
  tbody.append(tr);
@@ -2947,9 +2962,167 @@
2947
2962
  const scroller = el("div", "lg-scroll");
2948
2963
  scroller.append(table);
2949
2964
  wrap.append(scroller);
2965
+ const gap = F.gap && rows.get(F.gap.row);
2966
+ if (gap) {
2967
+ wrap.append(gapPanel(root, gap, F.gap.direction));
2968
+ } else F.gap = null;
2950
2969
  wrap.append(el("small", "state-dim", "Every loop plays on one clock. ⇋ marks a mirror, flipped locally at no cost. A dimmed cell is the parent's rotation, standing in until that loop is generated."));
2951
2970
  return wrap;
2952
2971
  }
2972
+ var MIRRORED = {
2973
+ south: "south",
2974
+ north: "north",
2975
+ east: "west",
2976
+ west: "east",
2977
+ "south-east": "south-west",
2978
+ "south-west": "south-east",
2979
+ "north-east": "north-west",
2980
+ "north-west": "north-east"
2981
+ };
2982
+ var ANIMATION_FIELDS = ["of", "template", "frames", "fps", "mode", "subject", "enhancePrompt"];
2983
+ function fillable(root, parent, cells, direction) {
2984
+ if (!EDITABLE || !projectOf(root)?.manifestSha256) return false;
2985
+ if (!turnOrder(parent).includes(direction)) return false;
2986
+ return cells.some((l) => !l.mirrorOfKey && l.declaredAs && l.asset);
2987
+ }
2988
+ function gapOptions(cells, direction) {
2989
+ const drawn = cells.filter((l) => !l.mirrorOfKey);
2990
+ const sample = drawn[0];
2991
+ const name = loopName(sample);
2992
+ const shorthand = sample.declaredAs !== sample.assetId ? sample.declaredAs : null;
2993
+ const flip = MIRRORED[direction];
2994
+ const options = [];
2995
+ if (shorthand) {
2996
+ const now = drawn.map((l) => l.character.direction);
2997
+ const next = COMPASS.filter((d) => d === direction || now.includes(d));
2998
+ const freeFlip = flip !== direction && !cells.some((l) => l.character.direction === flip);
2999
+ options.push({
3000
+ id: "draw",
3001
+ label: "Draw it",
3002
+ note: "adds " + direction + " to " + shorthand + "'s directions" + (freeFlip ? "; " + flip + " comes with it as a free mirror" : ""),
3003
+ edit: { action: "patch-asset", assetId: shorthand, patch: { loopDirections: next } }
3004
+ });
3005
+ return options;
3006
+ }
3007
+ const newId = name + "." + direction;
3008
+ const flipped = drawn.find((l) => l.character.direction === flip && flip !== direction);
3009
+ if (flipped) {
3010
+ const mirror = { mirror: flipped.declaredAs };
3011
+ if (flipped.asset?.styles) mirror.styles = flipped.asset.styles;
3012
+ options.push({ id: "mirror", label: "Mirror " + flip, note: "flips " + flipped.assetId + " locally; free", edit: { action: "add-asset", assetId: newId, asset: mirror } });
3013
+ }
3014
+ const base = sample.asset || {};
3015
+ const animation = { direction };
3016
+ for (const key of ANIMATION_FIELDS) if (base.animation && base.animation[key] !== void 0) animation[key] = base.animation[key];
3017
+ const asset = { animation };
3018
+ for (const key of ["prompt", "styles", "tags", "category"]) if (base[key] !== void 0) asset[key] = base[key];
3019
+ options.push({ id: "draw", label: "Draw it", note: "a copy of " + sample.assetId + " facing " + direction, edit: { action: "add-asset", assetId: newId, asset } });
3020
+ return options;
3021
+ }
3022
+ function gapPanel(root, cells, direction) {
3023
+ const panel = el("div", "gap-panel");
3024
+ const name = loopName(cells[0]);
3025
+ panel.append(el("h4", null, "Add " + (name.startsWith(root.assetId + ".") ? name.slice(root.assetId.length + 1) : name) + " facing " + direction));
3026
+ const options = gapOptions(cells, direction);
3027
+ let chosen = options[0];
3028
+ const list = el("div", "gap-options");
3029
+ const priceLine = el("div", "gap-price state-dim", "pricing…");
3030
+ const msg = el("div", "msg");
3031
+ for (const option of options) {
3032
+ const label = el("label", "field check");
3033
+ const radio = el("input");
3034
+ radio.type = "radio";
3035
+ radio.name = "gap-option";
3036
+ radio.checked = option === chosen;
3037
+ radio.onchange = () => {
3038
+ chosen = option;
3039
+ price();
3040
+ };
3041
+ label.append(radio, el("b", null, " " + option.label), el("span", "state-dim", " — " + option.note));
3042
+ list.append(label);
3043
+ }
3044
+ const actions = el("div", "actions");
3045
+ const add = el("button", null, "Add");
3046
+ add.type = "button";
3047
+ const addGen = el("button", "primary", "Add & generate");
3048
+ addGen.type = "button";
3049
+ const cancel = el("button", null, "Cancel");
3050
+ cancel.type = "button";
3051
+ cancel.onclick = () => {
3052
+ F.gap = null;
3053
+ F.drawn = null;
3054
+ renderFamily();
3055
+ };
3056
+ actions.append(add);
3057
+ if (GENERATION) actions.append(addGen);
3058
+ actions.append(cancel);
3059
+ panel.append(list, priceLine, actions, msg);
3060
+ const pr = projectOf(root);
3061
+ const request = () => {
3062
+ const body = { ...chosen.edit, expectedSha256: pr.manifestSha256 };
3063
+ if (root.project) body.project = root.project;
3064
+ return body;
3065
+ };
3066
+ let quote = null;
3067
+ let seq = 0;
3068
+ async function price() {
3069
+ const mine = ++seq;
3070
+ quote = null;
3071
+ priceLine.textContent = "pricing…";
3072
+ add.disabled = addGen.disabled = true;
3073
+ try {
3074
+ const res = await fetch("/api/price", {
3075
+ method: "POST",
3076
+ headers: { "Content-Type": "application/json", "X-Pixelkiln-Session": SESSION ?? "" },
3077
+ body: JSON.stringify(request())
3078
+ });
3079
+ if (mine !== seq) return;
3080
+ if (!res.ok) throw new Error(await res.text());
3081
+ quote = await res.json();
3082
+ const rows = quote.items.map((i) => i.key.split("/").slice(1).join("/") + " " + (i.cost ? fmtCost(i.costUnit, i.cost) : "free"));
3083
+ const totals = Object.entries(quote.totals).filter(([, n]) => n);
3084
+ priceLine.textContent = (rows.length ? rows.join(" · ") : "nothing new") + " — total " + (totals.map(([u, n]) => fmtCost(u, Math.round(n * 100) / 100)).join(" + ") || "free");
3085
+ const over = totals.some(([unit, n]) => {
3086
+ const provider = quote.items.find((i) => i.costUnit === unit)?.provider;
3087
+ const left = provider ? remainingBudget(provider) : null;
3088
+ return left !== null && n > left;
3089
+ });
3090
+ add.disabled = !quote.items.length;
3091
+ addGen.disabled = !quote.items.length || over;
3092
+ addGen.textContent = "Add & generate" + (totals.length ? " · " + totals.map(([u, n]) => fmtCost(u, Math.round(n * 100) / 100)).join(" + ") : "");
3093
+ if (over) addGen.title = "More than the session budget has left";
3094
+ } catch (err) {
3095
+ if (mine !== seq) return;
3096
+ priceLine.className = "gap-price msg bad";
3097
+ priceLine.textContent = err.message;
3098
+ }
3099
+ }
3100
+ const save = async (generate) => {
3101
+ if (!quote) return;
3102
+ const keys = quote.items.map((i) => i.key);
3103
+ add.disabled = addGen.disabled = true;
3104
+ msg.className = "msg";
3105
+ msg.textContent = "saving…";
3106
+ try {
3107
+ S.snap = await postEdit(request());
3108
+ if (generate) {
3109
+ await postGenerate({ keys, ...root.project ? { project: root.project } : {} });
3110
+ pollJobs();
3111
+ }
3112
+ F.gap = null;
3113
+ F.drawn = null;
3114
+ render();
3115
+ } catch (err) {
3116
+ msg.className = "msg bad";
3117
+ msg.textContent = err.status === 409 ? "The manifest changed on disk; refresh and try again." : err.message;
3118
+ add.disabled = addGen.disabled = false;
3119
+ }
3120
+ };
3121
+ add.onclick = () => save(false);
3122
+ addGen.onclick = () => save(true);
3123
+ price();
3124
+ return panel;
3125
+ }
2953
3126
  function column(direction, node) {
2954
3127
  if (!F.columns.has(direction)) F.columns.set(direction, []);
2955
3128
  F.columns.get(direction).push(node);
@@ -3880,6 +4053,66 @@
3880
4053
  const host = document.querySelector(".drawer .dbody > div");
3881
4054
  return host ? host.__frames ?? null : null;
3882
4055
  }
4056
+ var TAB_LABEL = { overview: "Overview", files: "Files", history: "History", details: "Details" };
4057
+ var OVERVIEW_FORMS = /^(state|anim|poses|revise|skeleton):/;
4058
+ function drawerTabs(item, panels) {
4059
+ const wrap = el("div", "dtabs");
4060
+ const available = Object.keys(panels).filter((name) => panels[name].childNodes.length);
4061
+ if (ui.editing && OVERVIEW_FORMS.test(ui.editing)) ui.drawerTab = "overview";
4062
+ const current = available.includes(ui.drawerTab) ? ui.drawerTab : "overview";
4063
+ const list = el("div", "tablist");
4064
+ list.setAttribute("role", "tablist");
4065
+ const count = {
4066
+ files: item.outputs.length,
4067
+ history: item.history.length
4068
+ };
4069
+ const buttons = [];
4070
+ for (const name of available) {
4071
+ const b = el("button", name === current ? "on" : null, TAB_LABEL[name] + (count[name] ? " " + count[name] : ""));
4072
+ b.type = "button";
4073
+ b.id = "dtab-" + name;
4074
+ b.setAttribute("role", "tab");
4075
+ b.setAttribute("aria-selected", String(name === current));
4076
+ b.setAttribute("aria-controls", "dpanel-" + name);
4077
+ b.tabIndex = name === current ? 0 : -1;
4078
+ b.onclick = () => {
4079
+ ui.drawerTab = name;
4080
+ show(name);
4081
+ };
4082
+ b.onkeydown = (e) => {
4083
+ const at = available.indexOf(name);
4084
+ const next = e.key === "ArrowRight" ? available[(at + 1) % available.length] : e.key === "ArrowLeft" ? available[(at - 1 + available.length) % available.length] : null;
4085
+ if (!next) return;
4086
+ e.preventDefault();
4087
+ e.stopPropagation();
4088
+ ui.drawerTab = next;
4089
+ show(next);
4090
+ buttons[available.indexOf(next)].focus();
4091
+ };
4092
+ buttons.push(b);
4093
+ list.append(b);
4094
+ }
4095
+ const show = (name) => {
4096
+ for (const [i, b] of buttons.entries()) {
4097
+ const on = available[i] === name;
4098
+ b.classList.toggle("on", on);
4099
+ b.setAttribute("aria-selected", String(on));
4100
+ b.tabIndex = on ? 0 : -1;
4101
+ }
4102
+ for (const n of available) panels[n].hidden = n !== name;
4103
+ };
4104
+ wrap.append(list);
4105
+ for (const name of available) {
4106
+ const panel = panels[name];
4107
+ panel.className = "tabpanel";
4108
+ panel.id = "dpanel-" + name;
4109
+ panel.setAttribute("role", "tabpanel");
4110
+ panel.setAttribute("aria-labelledby", "dtab-" + name);
4111
+ wrap.append(panel);
4112
+ }
4113
+ show(current);
4114
+ return wrap;
4115
+ }
3883
4116
  var CHARACTER_KIND = {
3884
4117
  base: "base",
3885
4118
  state: "state (a pose or outfit of its parent)",
@@ -3957,11 +4190,12 @@
3957
4190
  }
3958
4191
  if (canEdit && ui.editing === item.id) body.append(editForm(item));
3959
4192
  if (item.error && !item.reason.includes(item.error)) body.append(stateNode("failed", item.error));
4193
+ const tab = { overview: el("div"), files: el("div"), history: el("div"), details: el("div") };
4194
+ let pane = tab.overview;
3960
4195
  {
3961
4196
  const { s, dl } = section("Generation");
3962
4197
  row(dl, "provider", item.provider);
3963
4198
  row(dl, "generator", item.generator + (item.tileFeature ? " · " + item.tileFeature : ""));
3964
- row(dl, "lock status", item.status || "none (nothing submitted)");
3965
4199
  const pr = el("div", "prompt", item.prompt);
3966
4200
  row(dl, item.status ? "prompt sent" : "prompt", pr);
3967
4201
  if (item.currentPrompt) row(dl, "prompt now", el("div", "prompt state-warn", item.currentPrompt));
@@ -3987,13 +4221,19 @@
3987
4221
  row(dl, "estimate now", fmtCost(item.costUnit, item.estimatedCost) + (item.candidates && item.candidates > 1 ? " for " + item.candidates + " candidates" : ""));
3988
4222
  }
3989
4223
  if (item.candidateIndex !== null) row(dl, "candidate", "#" + (item.candidateIndex + 1) + " chosen in review");
4224
+ pane.append(s);
4225
+ }
4226
+ {
4227
+ const { s, dl } = section("Provider record");
4228
+ row(dl, "lock status", item.status || "none (nothing submitted)");
3990
4229
  row(dl, "submitted", fmtWhen(item.submittedAt));
3991
4230
  row(dl, "downloaded", fmtWhen(item.downloadedAt));
3992
4231
  row(dl, "job id", item.jobId, { mono: true, copy: item.jobId });
3993
4232
  row(dl, "object id", item.objectId, { mono: true, copy: item.objectId });
3994
4233
  row(dl, "review object", item.reviewObjectId, { mono: true, copy: item.reviewObjectId });
3995
- body.append(s);
4234
+ tab.details.append(s);
3996
4235
  }
4236
+ pane = tab.files;
3997
4237
  {
3998
4238
  const s = el("section", "meta");
3999
4239
  s.append(el("h3", null, item.outputs.length === 1 ? "Output" : "Outputs (" + item.outputs.length + ")"));
@@ -4019,14 +4259,15 @@
4019
4259
  box.append(dl);
4020
4260
  s.append(box);
4021
4261
  }
4022
- body.append(s);
4262
+ pane.append(s);
4023
4263
  }
4024
4264
  const hand = handEditSection(item);
4025
- if (hand) body.append(hand);
4265
+ if (hand) pane.append(hand);
4026
4266
  const upstream = upstreamSection(item);
4027
- if (upstream) body.append(upstream);
4267
+ if (upstream) pane.append(upstream);
4028
4268
  const versions = historySection(item);
4029
- if (versions) body.append(versions);
4269
+ if (versions) tab.history.append(versions);
4270
+ pane = tab.overview;
4030
4271
  if (item.character) {
4031
4272
  const c = item.character;
4032
4273
  const { s, dl } = section("Character");
@@ -4077,19 +4318,19 @@
4077
4318
  if (ui.editing === stateKey) s.append(newStateForm(item));
4078
4319
  if (ui.editing === animKey) s.append(newAnimationForm(item));
4079
4320
  }
4080
- body.append(s);
4321
+ pane.append(s);
4081
4322
  }
4082
4323
  if (item.mirrorOfKey) {
4083
4324
  const { s, dl } = section("Mirror");
4084
4325
  row(dl, "flipped from", keyLink(item, item.mirrorOfKey));
4085
4326
  row(dl, "cost", "none; made locally from that asset's files");
4086
- body.append(s);
4327
+ pane.append(s);
4087
4328
  }
4088
4329
  const mirrors = S.snap.items.filter((i) => i.mirrorOfKey === item.key && i.project === item.project);
4089
4330
  if (mirrors.length) {
4090
4331
  const { s, dl } = section(mirrors.length === 1 ? "Mirror of this asset" : "Mirrors of this asset");
4091
4332
  for (const m of mirrors) row(dl, m.character && m.character.direction ? m.character.direction : "flipped", keyLink(item, m.key));
4092
- body.append(s);
4333
+ pane.append(s);
4093
4334
  }
4094
4335
  if (item.revision || item.revisionParentKey) {
4095
4336
  const { s, dl } = section("Lineage");
@@ -4125,7 +4366,7 @@
4125
4366
  row(dl, "poses", el("span", "state-warn", "the keypoints file does not exist yet"));
4126
4367
  }
4127
4368
  }
4128
- body.append(s);
4369
+ pane.append(s);
4129
4370
  }
4130
4371
  const children = S.snap.items.filter((i) => i.revisionParentKey === item.key && i.project === item.project);
4131
4372
  const reviseKey = "revise:" + item.id;
@@ -4158,8 +4399,9 @@
4158
4399
  if (ui.editing === reviseKey) s.append(newRevisionForm(item));
4159
4400
  if (ui.editing === skeletonKey) s.append(skeletonAnimationForm(item));
4160
4401
  }
4161
- body.append(s);
4402
+ pane.append(s);
4162
4403
  }
4404
+ pane = tab.details;
4163
4405
  if (item.quality) {
4164
4406
  const q = item.quality;
4165
4407
  const { s, dl } = section("Quality");
@@ -4191,7 +4433,7 @@
4191
4433
  row(dl, "output", q.output, { mono: true });
4192
4434
  row(dl, "record", q.record + (q.recordExists ? "" : " (not written yet)"), { mono: true });
4193
4435
  if (q.check && !q.check.safe) row(dl, "check", el("div", "state-warn", q.check.reasons.join("\n")));
4194
- body.append(s);
4436
+ pane.append(s);
4195
4437
  if (q.outputs.some((o) => o.url)) {
4196
4438
  const strip = el("div", "members");
4197
4439
  for (const o of q.outputs.filter((o2) => o2.url)) {
@@ -4226,15 +4468,16 @@
4226
4468
  row(dl, "category", item.category);
4227
4469
  row(dl, "tags", item.tags && item.tags.length ? item.tags.join(", ") : null);
4228
4470
  row(dl, "source art", item.source, { mono: true });
4229
- body.append(s);
4471
+ pane.append(s);
4230
4472
  }
4231
4473
  if (item.asset) {
4232
4474
  const declared = Object.fromEntries(Object.entries(item.asset).filter(([, v]) => !(Array.isArray(v) && !v.length) && !(v && typeof v === "object" && !Array.isArray(v) && !Object.keys(v).length)));
4233
- body.append(jsonDetails("Manifest asset", declared));
4475
+ pane.append(jsonDetails("Manifest asset", declared));
4234
4476
  }
4235
4477
  if (item.providerMetadata && Object.keys(item.providerMetadata).length) {
4236
- body.append(jsonDetails("Provider metadata", item.providerMetadata));
4478
+ pane.append(jsonDetails("Provider metadata", item.providerMetadata));
4237
4479
  }
4480
+ body.append(drawerTabs(item, tab));
4238
4481
  drawer.append(head, body);
4239
4482
  host.append(scrim, drawer);
4240
4483
  close.focus({ preventScroll: true });
package/dist/index.cjs CHANGED
@@ -15409,7 +15409,12 @@ var AssetPatchSchema = import_zod8.z.object({
15409
15409
  width: import_zod8.z.number().int().nullable().optional(),
15410
15410
  height: import_zod8.z.number().int().nullable().optional(),
15411
15411
  size: import_zod8.z.number().int().nullable().optional(),
15412
- tags: import_zod8.z.array(import_zod8.z.string()).optional()
15412
+ tags: import_zod8.z.array(import_zod8.z.string()).optional(),
15413
+ /**
15414
+ * A loop shorthand's `animation.directions`, replaced whole: each named
15415
+ * direction is drawn, and each one's flip comes free as a mirror.
15416
+ */
15417
+ loopDirections: import_zod8.z.array(CharacterDirectionSchema).min(1).optional()
15413
15418
  }).strict();
15414
15419
  var NewStateSchema = import_zod8.z.object({
15415
15420
  /** Asset id of the parent this states; must already exist. */
@@ -15758,6 +15763,13 @@ function applyEdit(raw, edit) {
15758
15763
  if (patch.tags.length) asset.tags = patch.tags;
15759
15764
  else delete asset.tags;
15760
15765
  }
15766
+ if (patch.loopDirections) {
15767
+ const animation = asset.animation;
15768
+ if (!animation || !Array.isArray(animation.directions)) {
15769
+ throw new ManifestEditError(`asset "${edit.assetId}" is not a loop declared with animation.directions`);
15770
+ }
15771
+ asset.animation = { ...animation, directions: patch.loopDirections };
15772
+ }
15761
15773
  }
15762
15774
  async function withCandidate(manifestPath, edit, use, unchanged) {
15763
15775
  const absolute = import_node_path27.default.resolve(manifestPath);