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.
- package/dist/cli.js +43 -1
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +15 -0
- package/dist/client/gallery.js +352 -4
- package/dist/index.cjs +43 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +43 -1
- package/dist/index.js.map +1 -1
- package/docs/REVISIONS.md +11 -3
- package/package.json +1 -1
- package/skills/pixelkiln/references/revisions.md +3 -2
package/dist/client/gallery.css
CHANGED
|
@@ -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; } }
|
package/dist/client/gallery.js
CHANGED
|
@@ -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, "
|
|
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(
|
|
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
|
-
|
|
2741
|
-
|
|
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,
|