pixelkiln 0.78.0 → 0.79.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 +13 -1
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +9 -0
- package/dist/client/gallery.js +174 -2
- package/dist/index.cjs +13 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +12 -0
- package/package.json +1 -1
package/dist/client/gallery.css
CHANGED
|
@@ -466,3 +466,12 @@ 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; } }
|
package/dist/client/gallery.js
CHANGED
|
@@ -2645,7 +2645,8 @@
|
|
|
2645
2645
|
panel: null,
|
|
2646
2646
|
anims: [],
|
|
2647
2647
|
columns: /* @__PURE__ */ new Map(),
|
|
2648
|
-
turn: null
|
|
2648
|
+
turn: null,
|
|
2649
|
+
gap: null
|
|
2649
2650
|
};
|
|
2650
2651
|
renderFamily();
|
|
2651
2652
|
}
|
|
@@ -2938,7 +2939,20 @@
|
|
|
2938
2939
|
const td = el("td");
|
|
2939
2940
|
column(d, td);
|
|
2940
2941
|
const item = cells.find((l) => l.character.direction === d);
|
|
2941
|
-
td.append(
|
|
2942
|
+
if (item) td.append(loopCell(item, parent, d));
|
|
2943
|
+
else if (fillable(root, parent, cells, d)) {
|
|
2944
|
+
const open = F.gap && F.gap.row === k && F.gap.direction === d;
|
|
2945
|
+
const add = el("button", "lg-add" + (open ? " on" : ""), "+");
|
|
2946
|
+
add.type = "button";
|
|
2947
|
+
add.title = "Add " + name + " facing " + d;
|
|
2948
|
+
add.setAttribute("aria-label", add.title);
|
|
2949
|
+
add.onclick = () => {
|
|
2950
|
+
F.gap = open ? null : { row: k, direction: d };
|
|
2951
|
+
F.drawn = null;
|
|
2952
|
+
renderFamily();
|
|
2953
|
+
};
|
|
2954
|
+
td.append(add);
|
|
2955
|
+
} else td.append(el("span", "lg-none", "·"));
|
|
2942
2956
|
tr.append(td);
|
|
2943
2957
|
}
|
|
2944
2958
|
tbody.append(tr);
|
|
@@ -2947,9 +2961,167 @@
|
|
|
2947
2961
|
const scroller = el("div", "lg-scroll");
|
|
2948
2962
|
scroller.append(table);
|
|
2949
2963
|
wrap.append(scroller);
|
|
2964
|
+
const gap = F.gap && rows.get(F.gap.row);
|
|
2965
|
+
if (gap) {
|
|
2966
|
+
wrap.append(gapPanel(root, gap, F.gap.direction));
|
|
2967
|
+
} else F.gap = null;
|
|
2950
2968
|
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
2969
|
return wrap;
|
|
2952
2970
|
}
|
|
2971
|
+
var MIRRORED = {
|
|
2972
|
+
south: "south",
|
|
2973
|
+
north: "north",
|
|
2974
|
+
east: "west",
|
|
2975
|
+
west: "east",
|
|
2976
|
+
"south-east": "south-west",
|
|
2977
|
+
"south-west": "south-east",
|
|
2978
|
+
"north-east": "north-west",
|
|
2979
|
+
"north-west": "north-east"
|
|
2980
|
+
};
|
|
2981
|
+
var ANIMATION_FIELDS = ["of", "template", "frames", "fps", "mode", "subject", "enhancePrompt"];
|
|
2982
|
+
function fillable(root, parent, cells, direction) {
|
|
2983
|
+
if (!EDITABLE || !projectOf(root)?.manifestSha256) return false;
|
|
2984
|
+
if (!turnOrder(parent).includes(direction)) return false;
|
|
2985
|
+
return cells.some((l) => !l.mirrorOfKey && l.declaredAs && l.asset);
|
|
2986
|
+
}
|
|
2987
|
+
function gapOptions(cells, direction) {
|
|
2988
|
+
const drawn = cells.filter((l) => !l.mirrorOfKey);
|
|
2989
|
+
const sample = drawn[0];
|
|
2990
|
+
const name = loopName(sample);
|
|
2991
|
+
const shorthand = sample.declaredAs !== sample.assetId ? sample.declaredAs : null;
|
|
2992
|
+
const flip = MIRRORED[direction];
|
|
2993
|
+
const options = [];
|
|
2994
|
+
if (shorthand) {
|
|
2995
|
+
const now = drawn.map((l) => l.character.direction);
|
|
2996
|
+
const next = COMPASS.filter((d) => d === direction || now.includes(d));
|
|
2997
|
+
const freeFlip = flip !== direction && !cells.some((l) => l.character.direction === flip);
|
|
2998
|
+
options.push({
|
|
2999
|
+
id: "draw",
|
|
3000
|
+
label: "Draw it",
|
|
3001
|
+
note: "adds " + direction + " to " + shorthand + "'s directions" + (freeFlip ? "; " + flip + " comes with it as a free mirror" : ""),
|
|
3002
|
+
edit: { action: "patch-asset", assetId: shorthand, patch: { loopDirections: next } }
|
|
3003
|
+
});
|
|
3004
|
+
return options;
|
|
3005
|
+
}
|
|
3006
|
+
const newId = name + "." + direction;
|
|
3007
|
+
const flipped = drawn.find((l) => l.character.direction === flip && flip !== direction);
|
|
3008
|
+
if (flipped) {
|
|
3009
|
+
const mirror = { mirror: flipped.declaredAs };
|
|
3010
|
+
if (flipped.asset?.styles) mirror.styles = flipped.asset.styles;
|
|
3011
|
+
options.push({ id: "mirror", label: "Mirror " + flip, note: "flips " + flipped.assetId + " locally; free", edit: { action: "add-asset", assetId: newId, asset: mirror } });
|
|
3012
|
+
}
|
|
3013
|
+
const base = sample.asset || {};
|
|
3014
|
+
const animation = { direction };
|
|
3015
|
+
for (const key of ANIMATION_FIELDS) if (base.animation && base.animation[key] !== void 0) animation[key] = base.animation[key];
|
|
3016
|
+
const asset = { animation };
|
|
3017
|
+
for (const key of ["prompt", "styles", "tags", "category"]) if (base[key] !== void 0) asset[key] = base[key];
|
|
3018
|
+
options.push({ id: "draw", label: "Draw it", note: "a copy of " + sample.assetId + " facing " + direction, edit: { action: "add-asset", assetId: newId, asset } });
|
|
3019
|
+
return options;
|
|
3020
|
+
}
|
|
3021
|
+
function gapPanel(root, cells, direction) {
|
|
3022
|
+
const panel = el("div", "gap-panel");
|
|
3023
|
+
const name = loopName(cells[0]);
|
|
3024
|
+
panel.append(el("h4", null, "Add " + (name.startsWith(root.assetId + ".") ? name.slice(root.assetId.length + 1) : name) + " facing " + direction));
|
|
3025
|
+
const options = gapOptions(cells, direction);
|
|
3026
|
+
let chosen = options[0];
|
|
3027
|
+
const list = el("div", "gap-options");
|
|
3028
|
+
const priceLine = el("div", "gap-price state-dim", "pricing…");
|
|
3029
|
+
const msg = el("div", "msg");
|
|
3030
|
+
for (const option of options) {
|
|
3031
|
+
const label = el("label", "field check");
|
|
3032
|
+
const radio = el("input");
|
|
3033
|
+
radio.type = "radio";
|
|
3034
|
+
radio.name = "gap-option";
|
|
3035
|
+
radio.checked = option === chosen;
|
|
3036
|
+
radio.onchange = () => {
|
|
3037
|
+
chosen = option;
|
|
3038
|
+
price();
|
|
3039
|
+
};
|
|
3040
|
+
label.append(radio, el("b", null, " " + option.label), el("span", "state-dim", " — " + option.note));
|
|
3041
|
+
list.append(label);
|
|
3042
|
+
}
|
|
3043
|
+
const actions = el("div", "actions");
|
|
3044
|
+
const add = el("button", null, "Add");
|
|
3045
|
+
add.type = "button";
|
|
3046
|
+
const addGen = el("button", "primary", "Add & generate");
|
|
3047
|
+
addGen.type = "button";
|
|
3048
|
+
const cancel = el("button", null, "Cancel");
|
|
3049
|
+
cancel.type = "button";
|
|
3050
|
+
cancel.onclick = () => {
|
|
3051
|
+
F.gap = null;
|
|
3052
|
+
F.drawn = null;
|
|
3053
|
+
renderFamily();
|
|
3054
|
+
};
|
|
3055
|
+
actions.append(add);
|
|
3056
|
+
if (GENERATION) actions.append(addGen);
|
|
3057
|
+
actions.append(cancel);
|
|
3058
|
+
panel.append(list, priceLine, actions, msg);
|
|
3059
|
+
const pr = projectOf(root);
|
|
3060
|
+
const request = () => {
|
|
3061
|
+
const body = { ...chosen.edit, expectedSha256: pr.manifestSha256 };
|
|
3062
|
+
if (root.project) body.project = root.project;
|
|
3063
|
+
return body;
|
|
3064
|
+
};
|
|
3065
|
+
let quote = null;
|
|
3066
|
+
let seq = 0;
|
|
3067
|
+
async function price() {
|
|
3068
|
+
const mine = ++seq;
|
|
3069
|
+
quote = null;
|
|
3070
|
+
priceLine.textContent = "pricing…";
|
|
3071
|
+
add.disabled = addGen.disabled = true;
|
|
3072
|
+
try {
|
|
3073
|
+
const res = await fetch("/api/price", {
|
|
3074
|
+
method: "POST",
|
|
3075
|
+
headers: { "Content-Type": "application/json", "X-Pixelkiln-Session": SESSION ?? "" },
|
|
3076
|
+
body: JSON.stringify(request())
|
|
3077
|
+
});
|
|
3078
|
+
if (mine !== seq) return;
|
|
3079
|
+
if (!res.ok) throw new Error(await res.text());
|
|
3080
|
+
quote = await res.json();
|
|
3081
|
+
const rows = quote.items.map((i) => i.key.split("/").slice(1).join("/") + " " + (i.cost ? fmtCost(i.costUnit, i.cost) : "free"));
|
|
3082
|
+
const totals = Object.entries(quote.totals).filter(([, n]) => n);
|
|
3083
|
+
priceLine.textContent = (rows.length ? rows.join(" · ") : "nothing new") + " — total " + (totals.map(([u, n]) => fmtCost(u, Math.round(n * 100) / 100)).join(" + ") || "free");
|
|
3084
|
+
const over = totals.some(([unit, n]) => {
|
|
3085
|
+
const provider = quote.items.find((i) => i.costUnit === unit)?.provider;
|
|
3086
|
+
const left = provider ? remainingBudget(provider) : null;
|
|
3087
|
+
return left !== null && n > left;
|
|
3088
|
+
});
|
|
3089
|
+
add.disabled = !quote.items.length;
|
|
3090
|
+
addGen.disabled = !quote.items.length || over;
|
|
3091
|
+
addGen.textContent = "Add & generate" + (totals.length ? " · " + totals.map(([u, n]) => fmtCost(u, Math.round(n * 100) / 100)).join(" + ") : "");
|
|
3092
|
+
if (over) addGen.title = "More than the session budget has left";
|
|
3093
|
+
} catch (err) {
|
|
3094
|
+
if (mine !== seq) return;
|
|
3095
|
+
priceLine.className = "gap-price msg bad";
|
|
3096
|
+
priceLine.textContent = err.message;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
const save = async (generate) => {
|
|
3100
|
+
if (!quote) return;
|
|
3101
|
+
const keys = quote.items.map((i) => i.key);
|
|
3102
|
+
add.disabled = addGen.disabled = true;
|
|
3103
|
+
msg.className = "msg";
|
|
3104
|
+
msg.textContent = "saving…";
|
|
3105
|
+
try {
|
|
3106
|
+
S.snap = await postEdit(request());
|
|
3107
|
+
if (generate) {
|
|
3108
|
+
await postGenerate({ keys, ...root.project ? { project: root.project } : {} });
|
|
3109
|
+
pollJobs();
|
|
3110
|
+
}
|
|
3111
|
+
F.gap = null;
|
|
3112
|
+
F.drawn = null;
|
|
3113
|
+
render();
|
|
3114
|
+
} catch (err) {
|
|
3115
|
+
msg.className = "msg bad";
|
|
3116
|
+
msg.textContent = err.status === 409 ? "The manifest changed on disk; refresh and try again." : err.message;
|
|
3117
|
+
add.disabled = addGen.disabled = false;
|
|
3118
|
+
}
|
|
3119
|
+
};
|
|
3120
|
+
add.onclick = () => save(false);
|
|
3121
|
+
addGen.onclick = () => save(true);
|
|
3122
|
+
price();
|
|
3123
|
+
return panel;
|
|
3124
|
+
}
|
|
2953
3125
|
function column(direction, node) {
|
|
2954
3126
|
if (!F.columns.has(direction)) F.columns.set(direction, []);
|
|
2955
3127
|
F.columns.get(direction).push(node);
|
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);
|