pixelkiln 0.70.1 → 0.71.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.
@@ -326,3 +326,18 @@ form.edit .pal { margin-top:4px; }
326
326
  .poses svg.pose { width:112px; height:112px; background:var(--panel-deep); border:1px solid var(--line); image-rendering:pixelated; }
327
327
  .poses figcaption { font:10.5px/1 var(--mono); color:var(--dim); }
328
328
  textarea.mono { font-family:var(--mono); font-size:11.5px; }
329
+ .pose-editor { display:grid; grid-template-columns:minmax(200px, 320px) 1fr; gap:12px; align-items:start; margin-top:10px; }
330
+ .pose-stage svg.stage { width:100%; aspect-ratio:1; background:var(--panel-deep); border:1px solid var(--line); image-rendering:pixelated; touch-action:none; display:block; }
331
+ .pose-stage svg.stage:focus { outline:1px solid var(--accent); }
332
+ .pose-stage .joint { cursor:grab; }
333
+ .pose-stage .joint:active { cursor:grabbing; }
334
+ .pose-side { display:grid; gap:8px; align-content:start; }
335
+ .pose-tools { display:flex; flex-wrap:wrap; gap:6px; }
336
+ .pose-tools button { font:11.5px/1 var(--mono); padding:6px 8px; }
337
+ .pose-status { font:11.5px/1.45 var(--mono); color:var(--dim); }
338
+ .pose-strip { grid-column:1 / -1; }
339
+ .pose-strip button { flex:none; padding:3px; display:grid; gap:3px; justify-items:center; }
340
+ .pose-strip button.on { border-color:var(--accent); }
341
+ .pose-strip svg.pose { width:64px; height:64px; }
342
+ .pose-strip span { font:10.5px/1 var(--mono); color:var(--dim); }
343
+ @media (max-width: 640px) { .pose-editor { grid-template-columns:1fr; } }
@@ -2107,6 +2107,291 @@
2107
2107
  return row2;
2108
2108
  }
2109
2109
 
2110
+ // src/gallery/client/src/pose-editor.ts
2111
+ var MIN_FRAMES = 3;
2112
+ var MAX_FRAMES = 15;
2113
+ var HISTORY = 100;
2114
+ var NUDGE = 1 / 128;
2115
+ var PLAY_MS = 125;
2116
+ var clone = (set) => JSON.parse(JSON.stringify(set));
2117
+ var clamp = (v) => Math.min(1, Math.max(0, Math.round(v * 1e3) / 1e3));
2118
+ var mirrorLabel = (label) => label.startsWith("RIGHT ") ? "LEFT " + label.slice(6) : label.startsWith("LEFT ") ? "RIGHT " + label.slice(5) : label;
2119
+ function poseEditor(initial, imageUrl, onChange) {
2120
+ let set = clone(initial);
2121
+ let index = set.frames.length ? 1 : 0;
2122
+ let selected = null;
2123
+ let playing = null;
2124
+ const past = [];
2125
+ const future = [];
2126
+ const root = el("div", "pose-editor");
2127
+ const stageWrap = el("div", "pose-stage");
2128
+ const side = el("div", "pose-side");
2129
+ const status = el("div", "pose-status");
2130
+ const strip = el("div", "poses pose-strip");
2131
+ const tools = el("div", "pose-tools");
2132
+ side.append(tools, status);
2133
+ root.append(stageWrap, side, strip);
2134
+ const poseAt = (i) => i === 0 ? set.firstFrameKeypoints : set.frames[i - 1];
2135
+ const count = () => set.frames.length + 1;
2136
+ const name = (i) => i === 0 ? "start" : "frame " + i;
2137
+ const remember = (before) => {
2138
+ past.push(before);
2139
+ if (past.length > HISTORY) past.shift();
2140
+ future.length = 0;
2141
+ };
2142
+ const commit = (before) => {
2143
+ if (JSON.stringify(set) === before) return;
2144
+ remember(before);
2145
+ onChange(clone(set));
2146
+ drawStrip();
2147
+ drawTools();
2148
+ };
2149
+ const edit = (change) => {
2150
+ const before = JSON.stringify(set);
2151
+ change();
2152
+ commit(before);
2153
+ drawStage();
2154
+ };
2155
+ let svg;
2156
+ let joints = /* @__PURE__ */ new Map();
2157
+ let bones = [];
2158
+ function drawStage() {
2159
+ stageWrap.textContent = "";
2160
+ svg = svgEl("svg", { viewBox: "0 0 1 1", class: "pose stage", tabindex: 0 });
2161
+ svg.setAttribute("aria-label", "Pose " + name(index) + ": drag joints, arrow keys nudge the selected one");
2162
+ if (imageUrl) svg.append(svgEl("image", { href: imageUrl, x: 0, y: 0, width: 1, height: 1, preserveAspectRatio: "none" }));
2163
+ if (index > 0) {
2164
+ const prev = new Map(poseAt(index - 1).map((j) => [j.label, j]));
2165
+ for (const [a, b] of SKELETON_BONES) {
2166
+ const p = prev.get(a), q = prev.get(b);
2167
+ if (p && q) svg.append(svgEl("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y, stroke: boneColor(b), "stroke-width": 0.012, opacity: 0.3, "stroke-linecap": "round" }));
2168
+ }
2169
+ }
2170
+ const at = new Map(poseAt(index).map((j) => [j.label, j]));
2171
+ joints = /* @__PURE__ */ new Map();
2172
+ bones = [];
2173
+ for (const [a, b] of SKELETON_BONES) {
2174
+ const p = at.get(a), q = at.get(b);
2175
+ if (!p || !q) continue;
2176
+ const lines = [
2177
+ svgEl("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y, stroke: "#000", "stroke-width": 0.03, "stroke-linecap": "round" }),
2178
+ svgEl("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y, stroke: boneColor(b), "stroke-width": 0.015, "stroke-linecap": "round" })
2179
+ ];
2180
+ svg.append(...lines);
2181
+ bones.push({ a, b, lines });
2182
+ }
2183
+ for (const j of poseAt(index)) {
2184
+ const on = j.label === selected;
2185
+ const ring = svgEl("circle", { cx: j.x, cy: j.y, r: on ? 0.034 : 0.026, fill: "#000" });
2186
+ const dot = svgEl("circle", { cx: j.x, cy: j.y, r: on ? 0.022 : 0.016, fill: on ? "#fff" : boneColor(j.label) });
2187
+ const hit = svgEl("circle", { cx: j.x, cy: j.y, r: 0.045, fill: "transparent", class: "joint" });
2188
+ const title = svgEl("title", {});
2189
+ title.textContent = j.label;
2190
+ hit.append(title);
2191
+ hit.addEventListener("pointerdown", (e) => startDrag(e, j.label));
2192
+ svg.append(ring, dot, hit);
2193
+ joints.set(j.label, [ring, dot, hit]);
2194
+ }
2195
+ svg.addEventListener("keydown", onKey);
2196
+ stageWrap.append(svg);
2197
+ drawStatus();
2198
+ }
2199
+ function place(label, x, y) {
2200
+ for (const c of joints.get(label) || []) {
2201
+ c.setAttribute("cx", String(x));
2202
+ c.setAttribute("cy", String(y));
2203
+ }
2204
+ for (const bone of bones) {
2205
+ for (const line of bone.lines) {
2206
+ if (bone.a === label) {
2207
+ line.setAttribute("x1", String(x));
2208
+ line.setAttribute("y1", String(y));
2209
+ }
2210
+ if (bone.b === label) {
2211
+ line.setAttribute("x2", String(x));
2212
+ line.setAttribute("y2", String(y));
2213
+ }
2214
+ }
2215
+ }
2216
+ }
2217
+ function toUnit(e) {
2218
+ const m = svg.getScreenCTM();
2219
+ if (!m) return { x: 0, y: 0 };
2220
+ const p = new DOMPoint(e.clientX, e.clientY).matrixTransform(m.inverse());
2221
+ return { x: clamp(p.x), y: clamp(p.y) };
2222
+ }
2223
+ function startDrag(e, label) {
2224
+ if (playing) return;
2225
+ e.preventDefault();
2226
+ const before = JSON.stringify(set);
2227
+ const joint = poseAt(index).find((j) => j.label === label);
2228
+ const target = e.target;
2229
+ target.setPointerCapture(e.pointerId);
2230
+ const wasSelected = selected;
2231
+ selected = label;
2232
+ if (wasSelected !== label) {
2233
+ for (const [l, [ring, dot]] of joints) {
2234
+ const on = l === label;
2235
+ ring.setAttribute("r", String(on ? 0.034 : 0.026));
2236
+ dot.setAttribute("r", String(on ? 0.022 : 0.016));
2237
+ dot.setAttribute("fill", on ? "#fff" : boneColor(l));
2238
+ }
2239
+ }
2240
+ const move = (ev) => {
2241
+ const { x, y } = toUnit(ev);
2242
+ joint.x = x;
2243
+ joint.y = y;
2244
+ place(label, x, y);
2245
+ drawStatus();
2246
+ };
2247
+ const end = () => {
2248
+ target.removeEventListener("pointermove", move);
2249
+ target.removeEventListener("pointerup", end);
2250
+ target.removeEventListener("pointercancel", end);
2251
+ commit(before);
2252
+ svg.focus({ preventScroll: true });
2253
+ };
2254
+ target.addEventListener("pointermove", move);
2255
+ target.addEventListener("pointerup", end);
2256
+ target.addEventListener("pointercancel", end);
2257
+ drawStatus();
2258
+ }
2259
+ function onKey(e) {
2260
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") {
2261
+ e.preventDefault();
2262
+ if (e.shiftKey) redo();
2263
+ else undo();
2264
+ return;
2265
+ }
2266
+ if (e.key === ",") {
2267
+ e.preventDefault();
2268
+ go(index - 1);
2269
+ return;
2270
+ }
2271
+ if (e.key === ".") {
2272
+ e.preventDefault();
2273
+ go(index + 1);
2274
+ return;
2275
+ }
2276
+ const step2 = (e.shiftKey ? 4 : 1) * NUDGE;
2277
+ const d = { ArrowLeft: [-step2, 0], ArrowRight: [step2, 0], ArrowUp: [0, -step2], ArrowDown: [0, step2] }[e.key];
2278
+ if (!d || !selected || playing) return;
2279
+ e.preventDefault();
2280
+ const label = selected;
2281
+ edit(() => {
2282
+ const joint = poseAt(index).find((j) => j.label === label);
2283
+ joint.x = clamp(joint.x + d[0]);
2284
+ joint.y = clamp(joint.y + d[1]);
2285
+ });
2286
+ svg.focus({ preventScroll: true });
2287
+ }
2288
+ function undo() {
2289
+ const before = past.pop();
2290
+ if (!before) return;
2291
+ future.push(JSON.stringify(set));
2292
+ set = JSON.parse(before);
2293
+ index = Math.min(index, count() - 1);
2294
+ onChange(clone(set));
2295
+ redraw();
2296
+ }
2297
+ function redo() {
2298
+ const next = future.pop();
2299
+ if (!next) return;
2300
+ past.push(JSON.stringify(set));
2301
+ set = JSON.parse(next);
2302
+ index = Math.min(index, count() - 1);
2303
+ onChange(clone(set));
2304
+ redraw();
2305
+ }
2306
+ function go(i) {
2307
+ if (i < 0 || i >= count()) return;
2308
+ index = i;
2309
+ redraw();
2310
+ svg.focus({ preventScroll: true });
2311
+ }
2312
+ const button = (label, title, onClick, disabled = false) => {
2313
+ const b = el("button", null, label);
2314
+ b.type = "button";
2315
+ b.title = title;
2316
+ b.disabled = disabled || !!playing && label !== "Stop";
2317
+ b.onclick = onClick;
2318
+ return b;
2319
+ };
2320
+ function drawTools() {
2321
+ tools.textContent = "";
2322
+ const frameOnly = index === 0;
2323
+ tools.append(
2324
+ button("◀", "Previous pose (,)", () => go(index - 1), index === 0),
2325
+ button("▶", "Next pose (.)", () => go(index + 1), index >= count() - 1),
2326
+ button(playing ? "Stop" : "Play", "Play the frames in a loop", () => togglePlay(), set.frames.length < 2 && !playing),
2327
+ button("Copy previous", "Replace this pose with the one before it", () => edit(() => {
2328
+ set.frames[index - 1] = poseAt(index - 1).map((j) => ({ ...j }));
2329
+ }), frameOnly),
2330
+ button("Mirror", "Flip this pose left to right, swapping left and right joints", () => edit(() => {
2331
+ const flipped = poseAt(index).map((j) => ({ ...j, label: mirrorLabel(j.label), x: clamp(1 - j.x) }));
2332
+ if (index === 0) set.firstFrameKeypoints = flipped;
2333
+ else set.frames[index - 1] = flipped;
2334
+ })),
2335
+ button("+ Frame", "Add a copy of this pose after it", () => edit(() => {
2336
+ set.frames.splice(index, 0, poseAt(index).map((j) => ({ ...j })));
2337
+ index += 1;
2338
+ }), set.frames.length >= MAX_FRAMES),
2339
+ button("− Frame", "Remove this frame", () => edit(() => {
2340
+ set.frames.splice(index - 1, 1);
2341
+ index = Math.min(index, count() - 1);
2342
+ }), frameOnly || set.frames.length <= MIN_FRAMES),
2343
+ button("Undo", "Undo (Ctrl+Z)", undo, !past.length),
2344
+ button("Redo", "Redo (Ctrl+Shift+Z)", redo, !future.length)
2345
+ );
2346
+ }
2347
+ function togglePlay() {
2348
+ if (playing) {
2349
+ clearInterval(playing);
2350
+ playing = null;
2351
+ redraw();
2352
+ return;
2353
+ }
2354
+ let i = index > 0 ? index : 1;
2355
+ playing = setInterval(() => {
2356
+ if (!root.isConnected) {
2357
+ clearInterval(playing);
2358
+ playing = null;
2359
+ return;
2360
+ }
2361
+ i = i >= count() - 1 ? 1 : i + 1;
2362
+ index = i;
2363
+ drawStage();
2364
+ drawStrip();
2365
+ }, PLAY_MS);
2366
+ drawTools();
2367
+ }
2368
+ function drawStatus() {
2369
+ const j = selected && poseAt(index).find((x) => x.label === selected);
2370
+ status.textContent = name(index) + " of " + set.frames.length + " frames" + (j ? " · " + j.label + " (" + j.x.toFixed(3) + ", " + j.y.toFixed(3) + ")" : " · drag a joint to move it") + (index === 0 ? " · the starting pose must match the sprite as drawn" : "");
2371
+ }
2372
+ function drawStrip() {
2373
+ strip.textContent = "";
2374
+ for (let i = 0; i < count(); i++) {
2375
+ const cell = el("button", i === index ? "on" : null);
2376
+ cell.type = "button";
2377
+ cell.title = name(i);
2378
+ cell.append(poseSvg(poseAt(i), imageUrl, i > 0), el("span", null, i === 0 ? "start" : String(i)));
2379
+ const target = i;
2380
+ cell.onclick = () => {
2381
+ if (!playing) go(target);
2382
+ };
2383
+ strip.append(cell);
2384
+ }
2385
+ }
2386
+ function redraw() {
2387
+ drawStage();
2388
+ drawStrip();
2389
+ drawTools();
2390
+ }
2391
+ redraw();
2392
+ return root;
2393
+ }
2394
+
2110
2395
  // src/gallery/client/src/skeleton.ts
2111
2396
  var SKELETON_BONES = [
2112
2397
  ["NOSE", "NECK"],
@@ -2260,7 +2545,7 @@
2260
2545
  const preview = el("div");
2261
2546
  const row2 = el("div", "row");
2262
2547
  row2.append(field("poses from", source), field("keypoints file", file, "Inside the project. The manifest records this path."));
2263
- form.append(row2, estimateRow, field("poses (JSON)", json, "Move joints frame by frame; x and y are fractions of the sprite."), overwriteField, status, preview);
2548
+ form.append(row2, estimateRow, field("poses (JSON)", json, "Drag joints in the editor below, or edit the JSON; x and y are fractions of the sprite."), overwriteField, status, preview);
2264
2549
  const imageUrl = displayUrl(item);
2265
2550
  let set = null;
2266
2551
  const showPoses = () => {
@@ -2286,7 +2571,10 @@
2286
2571
  set = null;
2287
2572
  return;
2288
2573
  }
2289
- preview.append(poseStrip(set, imageUrl));
2574
+ preview.append(poseEditor(set, imageUrl, (next) => {
2575
+ set = next;
2576
+ json.value = JSON.stringify(next, null, 2);
2577
+ }));
2290
2578
  };
2291
2579
  const sync = () => {
2292
2580
  const pasting = source.value !== "file";
@@ -2373,6 +2661,51 @@
2373
2661
  setTimeout(() => id.focus(), 0);
2374
2662
  return form;
2375
2663
  }
2664
+ function skeletonPoseForm(item, parentUrl) {
2665
+ const form = el("form", "edit");
2666
+ form.append(el("h3", null, "Edit poses of " + item.assetId));
2667
+ let draft = item.skeleton.set;
2668
+ let dirty = false;
2669
+ const msg = el("span", "msg");
2670
+ const save = el("button", "primary", "Save poses");
2671
+ save.type = "submit";
2672
+ save.disabled = true;
2673
+ form.append(poseEditor(draft, parentUrl, (next) => {
2674
+ draft = next;
2675
+ dirty = true;
2676
+ save.disabled = false;
2677
+ msg.className = "msg";
2678
+ msg.textContent = "unsaved";
2679
+ }));
2680
+ form.append(el("small", "state-dim", "Saving rewrites " + item.skeleton.keypointsFile + "; the animation is then stale until it is generated again."));
2681
+ const cancel = el("button", null, "Cancel");
2682
+ cancel.type = "button";
2683
+ cancel.onclick = () => {
2684
+ if (dirty && !confirm("Discard the pose changes?")) return;
2685
+ ui.editing = null;
2686
+ renderDrawer();
2687
+ };
2688
+ const actions = el("div", "actions");
2689
+ actions.append(save, cancel, msg);
2690
+ form.append(actions);
2691
+ form.onsubmit = async (e) => {
2692
+ e.preventDefault();
2693
+ save.disabled = true;
2694
+ msg.className = "msg";
2695
+ msg.textContent = "saving…";
2696
+ const body = { action: "update-skeleton-keypoints", styleId: item.styleId, assetId: item.assetId, expectedSha256: item.skeleton.sha256, set: draft };
2697
+ if (item.project) body.project = item.project;
2698
+ try {
2699
+ S.snap = await postEdit(body);
2700
+ ui.editing = null;
2701
+ ui.notice = { id: item.id, text: "Poses saved to " + item.skeleton.keypointsFile + ". " + nextStep().replace("it", "the animation") };
2702
+ render();
2703
+ } catch (err) {
2704
+ showSaveError(msg, save, err);
2705
+ }
2706
+ };
2707
+ return form;
2708
+ }
2376
2709
 
2377
2710
  // src/gallery/client/src/drawer.ts
2378
2711
  function row(dl, label, value, opts = {}) {
@@ -2737,8 +3070,23 @@
2737
3070
  row(dl, "keypoints", item.skeleton.keypointsFile, { mono: true });
2738
3071
  if (item.skeleton.set) {
2739
3072
  const parent = S.snap.items.find((i) => i.key === item.revisionParentKey && i.project === item.project);
2740
- s.append(poseStrip(item.skeleton.set, displayUrl(parent)));
2741
- s.append(el("small", "state-dim", "The starting pose must match the sprite; each frame is drawn over a dimmed copy. `pixelkiln skeleton-preview " + item.assetId + "` writes the same sheet as a PNG."));
3073
+ const posesKey = "poses:" + item.id;
3074
+ if (ui.editing === posesKey) {
3075
+ s.append(skeletonPoseForm(item, displayUrl(parent)));
3076
+ } else {
3077
+ s.append(poseStrip(item.skeleton.set, displayUrl(parent)));
3078
+ s.append(el("small", "state-dim", "The starting pose must match the sprite; each frame is drawn over a dimmed copy. `pixelkiln skeleton-preview " + item.assetId + "` writes the same sheet as a PNG."));
3079
+ if (canEdit && item.skeleton.sha256) {
3080
+ const b = el("button", "add", "Edit poses");
3081
+ b.type = "button";
3082
+ b.onclick = () => {
3083
+ ui.editing = posesKey;
3084
+ ui.notice = null;
3085
+ renderDrawer();
3086
+ };
3087
+ s.append(b);
3088
+ }
3089
+ }
2742
3090
  } else {
2743
3091
  row(dl, "poses", el("span", "state-warn", "the keypoints file does not exist yet"));
2744
3092
  }
package/dist/index.cjs CHANGED
@@ -15947,6 +15947,15 @@ var SkeletonAnimationRequestSchema = import_zod10.z.object({
15947
15947
  /** Replace an existing `keypointsFile` with `set`. */
15948
15948
  overwrite: import_zod10.z.boolean().optional()
15949
15949
  }).strict();
15950
+ var SkeletonKeypointsUpdateSchema = import_zod10.z.object({
15951
+ action: import_zod10.z.literal("update-skeleton-keypoints"),
15952
+ project: import_zod10.z.string().min(1).optional(),
15953
+ styleId: import_zod10.z.string().min(1),
15954
+ assetId: import_zod10.z.string().min(1),
15955
+ /** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
15956
+ expectedSha256: import_zod10.z.string().regex(/^[0-9a-f]{64}$/),
15957
+ set: SkeletonSetSchema
15958
+ }).strict();
15950
15959
  async function createSkeletonAnimation(manifestPath, request) {
15951
15960
  const root = import_node_path29.default.dirname(import_node_path29.default.resolve(manifestPath));
15952
15961
  const file = import_node_path29.default.resolve(root, request.keypointsFile);
@@ -15992,6 +16001,26 @@ async function createSkeletonAnimation(manifestPath, request) {
15992
16001
  throw error;
15993
16002
  }
15994
16003
  }
16004
+ async function updateSkeletonKeypoints(ctx, request) {
16005
+ const key = lockKey(request.styleId, request.assetId);
16006
+ const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
16007
+ if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
16008
+ const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
16009
+ if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
16010
+ const relative = import_node_path29.default.relative(ctx.loaded.root, file);
16011
+ if (relative.startsWith("..") || import_node_path29.default.isAbsolute(relative)) {
16012
+ throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
16013
+ }
16014
+ const current = (0, import_node_fs25.existsSync)(file) ? sha256(await (0, import_promises22.readFile)(file)) : null;
16015
+ if (current !== request.expectedSha256) {
16016
+ throw Object.assign(
16017
+ new Error(`${relative.split(import_node_path29.default.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16018
+ { status: 409 }
16019
+ );
16020
+ }
16021
+ await (0, import_promises22.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16022
+ return relative.split(import_node_path29.default.sep).join("/");
16023
+ }
15995
16024
 
15996
16025
  // src/gallery/edit.ts
15997
16026
  var Base64 = import_zod11.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
@@ -16068,6 +16097,18 @@ function createGalleryEditHandler(opts) {
16068
16097
  }
16069
16098
  return opts.reload();
16070
16099
  }
16100
+ const poses = SkeletonKeypointsUpdateSchema.safeParse(body);
16101
+ if (poses.success) {
16102
+ if (!opts.loadProject) throw new ManifestEditError("pose edits are not available in this gallery");
16103
+ const written = await updateSkeletonKeypoints(await opts.loadProject(poses.data.project), poses.data);
16104
+ log(` poses saved: ${written} (${poses.data.styleId}/${poses.data.assetId} is stale until generated again)`);
16105
+ return opts.reload();
16106
+ }
16107
+ if (body?.action === "update-skeleton-keypoints") {
16108
+ throw new ManifestEditError(
16109
+ "invalid pose edit: " + poses.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16110
+ );
16111
+ }
16071
16112
  const skeleton = SkeletonAnimationRequestSchema.safeParse(body);
16072
16113
  if (skeleton.success) {
16073
16114
  let manifestPath2;
@@ -16441,7 +16482,8 @@ async function buildGallerySnapshot(opts) {
16441
16482
  mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
16442
16483
  skeleton: spec.revision?.mode === "animate-skeleton" && spec.revision.keypointsFile ? {
16443
16484
  keypointsFile: portableOutputPath(spec.revision.keypointsFile, root),
16444
- set: spec.revision.skeleton ?? null
16485
+ set: spec.revision.skeleton ?? null,
16486
+ sha256: spec.revision.keypointsSha256 ?? null
16445
16487
  } : null,
16446
16488
  character: describeCharacter(spec, entry),
16447
16489
  outputs,