pixelkiln 0.74.0 → 0.75.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/client/gallery.css +50 -0
- package/dist/client/gallery.js +1011 -538
- package/docs/CHARACTERS.md +14 -0
- package/docs/CLI.md +3 -1
- package/package.json +1 -1
package/dist/client/gallery.js
CHANGED
|
@@ -856,6 +856,555 @@
|
|
|
856
856
|
return form;
|
|
857
857
|
}
|
|
858
858
|
|
|
859
|
+
// src/gallery/client/src/grid.ts
|
|
860
|
+
function visibleItems() {
|
|
861
|
+
const q = ui.q.trim().toLowerCase();
|
|
862
|
+
let items = S.snap.items.filter((item) => {
|
|
863
|
+
if (ui.states.size && !ui.states.has(item.state)) return false;
|
|
864
|
+
if (ui.providers.size && !ui.providers.has(item.provider)) return false;
|
|
865
|
+
if (ui.generators.size && !ui.generators.has(item.generator)) return false;
|
|
866
|
+
if (ui.projects.size && !ui.projects.has(item.project ?? "")) return false;
|
|
867
|
+
if (!q) return true;
|
|
868
|
+
const hay = [
|
|
869
|
+
item.id,
|
|
870
|
+
item.prompt,
|
|
871
|
+
item.currentPrompt,
|
|
872
|
+
item.jobId,
|
|
873
|
+
item.objectId,
|
|
874
|
+
item.recordedSpecHash,
|
|
875
|
+
item.currentSpecHash,
|
|
876
|
+
item.category,
|
|
877
|
+
...item.tags || [],
|
|
878
|
+
...item.outputs.flatMap((o) => [o.path, o.sha256])
|
|
879
|
+
].filter(Boolean).join("\n").toLowerCase();
|
|
880
|
+
return hay.includes(q);
|
|
881
|
+
});
|
|
882
|
+
const when = (item) => item.downloadedAt || item.submittedAt || "";
|
|
883
|
+
const sorters = {
|
|
884
|
+
key: (a, b) => a.id.localeCompare(b.id),
|
|
885
|
+
newest: (a, b) => when(b).localeCompare(when(a)) || a.id.localeCompare(b.id),
|
|
886
|
+
oldest: (a, b) => when(a).localeCompare(when(b)) || a.id.localeCompare(b.id),
|
|
887
|
+
cost: (a, b) => b.cost - a.cost || a.id.localeCompare(b.id),
|
|
888
|
+
size: (a, b) => b.width * b.height - a.width * a.height || a.id.localeCompare(b.id)
|
|
889
|
+
};
|
|
890
|
+
items.sort(sorters[ui.sort] || sorters.key);
|
|
891
|
+
return items;
|
|
892
|
+
}
|
|
893
|
+
function renderHeader() {
|
|
894
|
+
$("editing").hidden = !EDITABLE;
|
|
895
|
+
$("foot-edit").hidden = EDITABLE;
|
|
896
|
+
$("foot-editing").hidden = !EDITABLE;
|
|
897
|
+
$("foot-gen").hidden = !GENERATION;
|
|
898
|
+
const p = $("project");
|
|
899
|
+
p.textContent = "";
|
|
900
|
+
if (S.snap.workspace) {
|
|
901
|
+
p.append(el("b", null, "workspace"), " ", document.createTextNode(shortPath(S.snap.workspace.path)));
|
|
902
|
+
p.title = S.snap.workspace.path;
|
|
903
|
+
} else {
|
|
904
|
+
p.append(el("b", null, S.snap.project.name), " ", document.createTextNode(shortPath(S.snap.project.manifest)));
|
|
905
|
+
p.title = S.snap.project.manifest + "\nlockfile: " + S.snap.project.lock;
|
|
906
|
+
}
|
|
907
|
+
const t = $("totals");
|
|
908
|
+
t.textContent = "";
|
|
909
|
+
if (S.snap.workspace) {
|
|
910
|
+
const broken = S.snap.workspace.projects.filter((pr) => pr.error).length;
|
|
911
|
+
const w = el("span", null, "");
|
|
912
|
+
w.append(el("b", null, S.snap.workspace.projects.length), document.createTextNode(" project" + (S.snap.workspace.projects.length === 1 ? "" : "s") + (broken ? " (" + broken + " unreadable)" : "")));
|
|
913
|
+
t.append(w);
|
|
914
|
+
}
|
|
915
|
+
const n = S.snap.totals.entries;
|
|
916
|
+
t.append(el("span", null, ""));
|
|
917
|
+
t.lastChild.append(el("b", null, n), document.createTextNode(" lock " + (n === 1 ? "entry" : "entries")));
|
|
918
|
+
const declared = S.snap.items.filter((i) => i.declared).length;
|
|
919
|
+
t.append(el("span", null, declared + " declared by " + (S.snap.workspace ? "a manifest" : "the manifest")));
|
|
920
|
+
const recorded = fmtSpend(S.snap.totals.spendByUnit);
|
|
921
|
+
t.append(el("span", null, recorded === "no spend recorded" ? recorded : recorded + " recorded"));
|
|
922
|
+
if (S.snap.filter.styles.length || S.snap.filter.assets.length) {
|
|
923
|
+
t.append(el("span", "state-warn", "filtered: " + [
|
|
924
|
+
S.snap.filter.styles.length ? "--style " + S.snap.filter.styles.join(",") : "",
|
|
925
|
+
S.snap.filter.assets.length ? "--only " + S.snap.filter.assets.join(",") : ""
|
|
926
|
+
].filter(Boolean).join(" ")));
|
|
927
|
+
}
|
|
928
|
+
t.append(el("span", null, "snapshot " + fmtWhen(S.snap.generatedAt)));
|
|
929
|
+
if (GENERATION) {
|
|
930
|
+
const line = budgetLine();
|
|
931
|
+
if (line) t.append(el("span", "budget", line));
|
|
932
|
+
}
|
|
933
|
+
const chips = $("chips");
|
|
934
|
+
chips.textContent = "";
|
|
935
|
+
const counts = (pick) => {
|
|
936
|
+
const m = /* @__PURE__ */ new Map();
|
|
937
|
+
for (const item of S.snap.items) {
|
|
938
|
+
const k = pick(item);
|
|
939
|
+
m.set(k, (m.get(k) || 0) + 1);
|
|
940
|
+
}
|
|
941
|
+
return m;
|
|
942
|
+
};
|
|
943
|
+
const chip = (label, count, set, key, tone) => {
|
|
944
|
+
const b = el("button", "chip" + (set.has(key) ? " on" : ""));
|
|
945
|
+
b.type = "button";
|
|
946
|
+
b.setAttribute("aria-pressed", String(set.has(key)));
|
|
947
|
+
if (tone) b.append(el("i", "dot " + tone));
|
|
948
|
+
b.append(document.createTextNode(label), el("span", "n", count));
|
|
949
|
+
b.onclick = () => {
|
|
950
|
+
set.has(key) ? set.delete(key) : set.add(key);
|
|
951
|
+
render();
|
|
952
|
+
};
|
|
953
|
+
return b;
|
|
954
|
+
};
|
|
955
|
+
if (S.snap.workspace) {
|
|
956
|
+
const byProject = counts((i) => i.project);
|
|
957
|
+
for (const pr of S.snap.workspace.projects) {
|
|
958
|
+
if (byProject.has(pr.id) || ui.projects.has(pr.id)) chips.append(chip(pr.id, byProject.get(pr.id) || 0, ui.projects, pr.id));
|
|
959
|
+
}
|
|
960
|
+
chips.append(el("span", "sep"));
|
|
961
|
+
}
|
|
962
|
+
const byState = counts((i) => i.state);
|
|
963
|
+
for (const s of STATE_ORDER) if (byState.has(s)) chips.append(chip(s, byState.get(s), ui.states, s, STATE_TONE[s]));
|
|
964
|
+
const byProvider = counts((i) => i.provider);
|
|
965
|
+
if (byProvider.size > 1 || ui.providers.size) {
|
|
966
|
+
chips.append(el("span", "sep"));
|
|
967
|
+
for (const [k, v] of [...byProvider].sort()) chips.append(chip(k, v, ui.providers, k));
|
|
968
|
+
}
|
|
969
|
+
const byGen = counts((i) => i.generator);
|
|
970
|
+
if (byGen.size > 1 || ui.generators.size) {
|
|
971
|
+
chips.append(el("span", "sep"));
|
|
972
|
+
for (const [k, v] of [...byGen].sort()) chips.append(chip(k, v, ui.generators, k));
|
|
973
|
+
}
|
|
974
|
+
if (ui.states.size || ui.providers.size || ui.generators.size || ui.projects.size || ui.q) {
|
|
975
|
+
const clear = el("button", "chip", "clear filters");
|
|
976
|
+
clear.type = "button";
|
|
977
|
+
clear.onclick = () => clearFilters();
|
|
978
|
+
chips.append(el("span", "sep"), clear);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function clearFilters() {
|
|
982
|
+
ui.states.clear();
|
|
983
|
+
ui.providers.clear();
|
|
984
|
+
ui.generators.clear();
|
|
985
|
+
ui.projects.clear();
|
|
986
|
+
ui.q = "";
|
|
987
|
+
$("q").value = "";
|
|
988
|
+
render();
|
|
989
|
+
}
|
|
990
|
+
function thumb(item) {
|
|
991
|
+
const cell = el("div", "cell");
|
|
992
|
+
const shown = item.edit && item.editStatus === "edited" && item.edits.every((e) => e.url) ? item.edits : item.outputs.filter((o) => o.url);
|
|
993
|
+
if (!shown.length) {
|
|
994
|
+
cell.classList.add("none");
|
|
995
|
+
cell.append(el("span", null, item.state === "missing" ? "not generated yet" : item.state === "in-flight" ? item.status === "review" ? "awaiting review" : "awaiting provider" : item.state === "recoverable" ? "ready to fetch" : item.state === "failed" ? "generation failed" : item.state === "blocked" ? "waiting on its parent" : item.outputs.length ? "file missing on disk" : "no output recorded"));
|
|
996
|
+
return cell;
|
|
997
|
+
}
|
|
998
|
+
const w = item.width, h = item.height;
|
|
999
|
+
if (shown.length > 1 && !isFrameSet(item)) {
|
|
1000
|
+
const multi = el("div", "multi");
|
|
1001
|
+
for (const o of shown.slice(0, 4)) {
|
|
1002
|
+
const box = el("div");
|
|
1003
|
+
const img2 = el("img");
|
|
1004
|
+
img2.src = o.url;
|
|
1005
|
+
img2.alt = o.role || item.assetId;
|
|
1006
|
+
img2.loading = "lazy";
|
|
1007
|
+
const s2 = displayScale(w, h, 66, 52);
|
|
1008
|
+
if (s2 >= 1) {
|
|
1009
|
+
img2.width = w * s2;
|
|
1010
|
+
img2.height = h * s2;
|
|
1011
|
+
} else img2.className = "fit";
|
|
1012
|
+
box.append(img2);
|
|
1013
|
+
multi.append(box);
|
|
1014
|
+
}
|
|
1015
|
+
cell.append(multi, el("span", "badge", shown.length + " outputs"));
|
|
1016
|
+
return cell;
|
|
1017
|
+
}
|
|
1018
|
+
const img = el("img");
|
|
1019
|
+
img.src = shown[0].url;
|
|
1020
|
+
img.alt = item.assetId;
|
|
1021
|
+
img.loading = "lazy";
|
|
1022
|
+
const s = displayScale(w, h, 156, 116);
|
|
1023
|
+
if (s >= 1) {
|
|
1024
|
+
img.width = w * s;
|
|
1025
|
+
img.height = h * s;
|
|
1026
|
+
} else img.className = "fit";
|
|
1027
|
+
cell.append(img);
|
|
1028
|
+
if (isFrameSet(item)) cell.append(el("span", "badge", "▸ " + shown.length + " frames"));
|
|
1029
|
+
else if (s >= 1 && s !== 1) cell.append(el("span", "badge", s + "×"));
|
|
1030
|
+
return cell;
|
|
1031
|
+
}
|
|
1032
|
+
function card(item) {
|
|
1033
|
+
const slot = ui.compare.indexOf(item.id);
|
|
1034
|
+
const c = el("button", "card" + (item.outputs.some((o) => o.url) ? "" : " ghost") + (ui.open === item.id ? " active" : "") + (slot >= 0 ? " compared" : ""));
|
|
1035
|
+
c.type = "button";
|
|
1036
|
+
c.dataset.key = item.id;
|
|
1037
|
+
c.setAttribute("aria-label", item.id + ", " + item.state + (slot >= 0 ? ", in comparison" : ""));
|
|
1038
|
+
const body = el("div", "cbody");
|
|
1039
|
+
body.append(el("div", "aid", item.assetId));
|
|
1040
|
+
const meta = el("div", "cmeta");
|
|
1041
|
+
const st = el("span", "st");
|
|
1042
|
+
st.append(el("i", "dot " + STATE_TONE[item.state]), document.createTextNode(item.state));
|
|
1043
|
+
meta.append(st, el("span", null, item.width + "×" + item.height));
|
|
1044
|
+
if (item.cost) meta.append(el("span", null, fmtCost(item.costUnit, item.cost)));
|
|
1045
|
+
body.append(meta);
|
|
1046
|
+
const family = familyLabel(item);
|
|
1047
|
+
if (family) body.append(el("div", "family", family));
|
|
1048
|
+
const cell = thumb(item);
|
|
1049
|
+
if (slot >= 0) cell.append(el("span", "slot", String(slot + 1)));
|
|
1050
|
+
if (item.editStatus === "edited" || item.editStatus === "regenerated-since") {
|
|
1051
|
+
const pen = el("span", "pen", "✎");
|
|
1052
|
+
pen.title = "hand-edited";
|
|
1053
|
+
cell.append(pen);
|
|
1054
|
+
}
|
|
1055
|
+
const job = activeJobFor(item);
|
|
1056
|
+
if (job) {
|
|
1057
|
+
const badge = el("span", "busy-badge");
|
|
1058
|
+
badge.append(el("i", "dot busy"), document.createTextNode(job.phase === "review" ? "review" : job.mode === "generate" ? "generating" : job.mode));
|
|
1059
|
+
badge.title = (JOB_VERB[job.mode] || job.mode) + " this asset: " + (PHASE_TEXT[job.phase] || job.phase);
|
|
1060
|
+
cell.append(badge);
|
|
1061
|
+
}
|
|
1062
|
+
c.append(cell, body);
|
|
1063
|
+
c.onclick = (e) => {
|
|
1064
|
+
if (e.shiftKey) toggleCompare(item.id);
|
|
1065
|
+
else openItem(item.id);
|
|
1066
|
+
};
|
|
1067
|
+
return c;
|
|
1068
|
+
}
|
|
1069
|
+
function renderMain(items) {
|
|
1070
|
+
const root = $("root");
|
|
1071
|
+
root.textContent = "";
|
|
1072
|
+
if (!S.snap.items.length) {
|
|
1073
|
+
const e = el("div", "empty");
|
|
1074
|
+
e.append(el("p", null, "Nothing has been generated for this project yet."));
|
|
1075
|
+
e.append(el("p", null, "pixelkiln plan shows what the manifest would make; pixelkiln gen makes it."));
|
|
1076
|
+
root.append(e);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (!items.length) {
|
|
1080
|
+
const e = el("div", "empty");
|
|
1081
|
+
e.append(el("p", null, "No generations match these filters."));
|
|
1082
|
+
const b = el("button", null, "Clear filters");
|
|
1083
|
+
b.type = "button";
|
|
1084
|
+
b.onclick = clearFilters;
|
|
1085
|
+
e.append(b);
|
|
1086
|
+
root.append(e);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const hidden = Math.max(0, items.length - ui.limit);
|
|
1090
|
+
const shown = hidden ? items.slice(0, ui.limit) : items;
|
|
1091
|
+
const unfiltered = !ui.projects.size && !ui.q && !ui.states.size && !ui.providers.size && !ui.generators.size;
|
|
1092
|
+
const projects = S.snap.workspace ? S.snap.workspace.projects.filter((pr) => ui.projects.size ? ui.projects.has(pr.id) : unfiltered || shown.some((i) => i.project === pr.id)) : [null];
|
|
1093
|
+
const sections = [];
|
|
1094
|
+
for (const pr of projects) {
|
|
1095
|
+
const inProject = pr ? shown.filter((i) => i.project === pr.id) : shown;
|
|
1096
|
+
const styleSections = ui.group === "style" ? S.snap.styles.filter((s) => !pr || s.project === pr.id).map((s) => ({ style: s, items: inProject.filter((i) => i.styleId === s.id) })).filter((s) => s.items.length) : inProject.length ? [{ style: null, items: inProject }] : [];
|
|
1097
|
+
if (pr) sections.push({ project: pr, style: null, items: null, count: inProject.length });
|
|
1098
|
+
sections.push(...styleSections);
|
|
1099
|
+
}
|
|
1100
|
+
for (const sec of sections) {
|
|
1101
|
+
if (sec.project) {
|
|
1102
|
+
root.append(projectHeader(sec.project, sec.count));
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
const wrap = el("section", "style");
|
|
1106
|
+
if (sec.style) {
|
|
1107
|
+
const s = sec.style;
|
|
1108
|
+
const head = el("div", "shead");
|
|
1109
|
+
head.append(el("h2", null, s.id));
|
|
1110
|
+
const meta = el("div", "meta");
|
|
1111
|
+
meta.append(el("span", null, s.provider), el("span", null, s.generator));
|
|
1112
|
+
if (s.outDir) meta.append(el("span", "mono", s.outDir));
|
|
1113
|
+
if (s.palette.length) {
|
|
1114
|
+
const sw = el("span", "swatches");
|
|
1115
|
+
for (const c of s.palette) {
|
|
1116
|
+
const i = el("i");
|
|
1117
|
+
i.style.background = c;
|
|
1118
|
+
i.title = c;
|
|
1119
|
+
sw.append(i);
|
|
1120
|
+
}
|
|
1121
|
+
meta.append(sw);
|
|
1122
|
+
}
|
|
1123
|
+
if (s.quality) meta.append(el("span", null, "quality profile"));
|
|
1124
|
+
if (s.characters) {
|
|
1125
|
+
const parts = [];
|
|
1126
|
+
parts.push(s.characters.bases + (s.characters.bases === 1 ? " character" : " characters"));
|
|
1127
|
+
if (s.characters.states) parts.push(s.characters.states + (s.characters.states === 1 ? " state" : " states"));
|
|
1128
|
+
if (s.characters.animations) parts.push(s.characters.animations + (s.characters.animations === 1 ? " loop" : " loops"));
|
|
1129
|
+
if (s.characters.portraits) parts.push(s.characters.portraits + (s.characters.portraits === 1 ? " portrait" : " portraits"));
|
|
1130
|
+
if (s.characters.outfits) parts.push(s.characters.outfits + (s.characters.outfits === 1 ? " outfit" : " outfits"));
|
|
1131
|
+
meta.append(el("span", null, parts.join(", ")));
|
|
1132
|
+
}
|
|
1133
|
+
meta.append(el("span", null, sec.items.length + " of " + s.items + (s.items === 1 ? " asset" : " assets")));
|
|
1134
|
+
if (Object.keys(s.spendByUnit).length) meta.append(el("span", null, fmtSpend(s.spendByUnit)));
|
|
1135
|
+
head.append(meta);
|
|
1136
|
+
const addKey = "new:" + (s.project || "") + ":" + s.id;
|
|
1137
|
+
const tools = el("div", "tools");
|
|
1138
|
+
if (s.outDir && s.candidates !== null) tools.append(candidatesControl(s));
|
|
1139
|
+
if (GENERATION && s.actionable.keys.length) {
|
|
1140
|
+
const g = el("button", "primary", "Generate " + s.actionable.keys.length + " · " + fmtCost(s.actionable.costUnit, Math.round(s.actionable.cost * 100) / 100));
|
|
1141
|
+
g.type = "button";
|
|
1142
|
+
g.onclick = () => generateDialog(S.snap.items.filter((i) => i.project === s.project && s.actionable.keys.includes(i.key)), { project: s.project });
|
|
1143
|
+
tools.append(g);
|
|
1144
|
+
}
|
|
1145
|
+
const refreshable = S.snap.items.filter((i) => i.project === s.project && i.styleId === s.id && i.refreshable && i.upstreamUrl);
|
|
1146
|
+
if (GENERATION && refreshable.length) {
|
|
1147
|
+
const r = el("button", null, "Pull upstream " + refreshable.length);
|
|
1148
|
+
r.type = "button";
|
|
1149
|
+
r.title = "Re-download objects edited in the provider’s own editor; no cost";
|
|
1150
|
+
r.onclick = () => generateDialog(refreshable, { project: s.project, refresh: true });
|
|
1151
|
+
tools.append(r);
|
|
1152
|
+
}
|
|
1153
|
+
const styleKey = "style:" + (s.project || "") + ":" + s.id;
|
|
1154
|
+
if (EDITABLE && s.outDir) {
|
|
1155
|
+
const es = el("button", null, ui.editing === styleKey ? "Cancel" : "Edit style");
|
|
1156
|
+
es.type = "button";
|
|
1157
|
+
es.onclick = () => {
|
|
1158
|
+
ui.editing = ui.editing === styleKey ? null : styleKey;
|
|
1159
|
+
ui.notice = null;
|
|
1160
|
+
render();
|
|
1161
|
+
};
|
|
1162
|
+
tools.append(es);
|
|
1163
|
+
const add = el("button", "add", ui.editing === addKey ? "Cancel" : "+ Add asset");
|
|
1164
|
+
add.type = "button";
|
|
1165
|
+
add.onclick = () => {
|
|
1166
|
+
ui.editing = ui.editing === addKey ? null : addKey;
|
|
1167
|
+
render();
|
|
1168
|
+
};
|
|
1169
|
+
tools.append(add);
|
|
1170
|
+
}
|
|
1171
|
+
if (tools.childNodes.length) head.append(tools);
|
|
1172
|
+
wrap.append(head);
|
|
1173
|
+
if (ui.notice && ui.notice.id === styleKey) wrap.append(el("div", "notice", ui.notice.text));
|
|
1174
|
+
if (ui.editing === styleKey) wrap.append(styleForm(s));
|
|
1175
|
+
if (ui.editing === addKey) wrap.append(addAssetForm(s));
|
|
1176
|
+
}
|
|
1177
|
+
const grid = el("div", "grid");
|
|
1178
|
+
for (const item of sec.items) grid.append(card(item));
|
|
1179
|
+
wrap.append(grid);
|
|
1180
|
+
root.append(wrap);
|
|
1181
|
+
}
|
|
1182
|
+
if (hidden) {
|
|
1183
|
+
const more = el("div", "empty");
|
|
1184
|
+
more.append(el("p", null, hidden + " more " + (hidden === 1 ? "generation is" : "generations are") + " not shown."));
|
|
1185
|
+
const b = el("button", null, "Show all " + items.length);
|
|
1186
|
+
b.type = "button";
|
|
1187
|
+
b.onclick = () => {
|
|
1188
|
+
ui.limit = Infinity;
|
|
1189
|
+
render();
|
|
1190
|
+
};
|
|
1191
|
+
more.append(b);
|
|
1192
|
+
root.append(more);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function projectHeader(pr, count) {
|
|
1196
|
+
const head = el("section", "project");
|
|
1197
|
+
const line = el("div", "phead");
|
|
1198
|
+
line.append(el("h2", null, pr.id));
|
|
1199
|
+
const meta = el("div", "meta");
|
|
1200
|
+
if (pr.name !== pr.id) meta.append(el("span", null, pr.name));
|
|
1201
|
+
meta.append(el("span", "mono", shortPath(pr.manifest)));
|
|
1202
|
+
if (pr.account) meta.append(el("span", null, pr.account));
|
|
1203
|
+
if (pr.error) {
|
|
1204
|
+
meta.append(el("span", "state-bad", "unreadable: " + pr.error));
|
|
1205
|
+
} else {
|
|
1206
|
+
meta.append(el("span", null, count + " of " + pr.items + " shown · " + pr.entries + " lock " + (pr.entries === 1 ? "entry" : "entries")));
|
|
1207
|
+
if (Object.keys(pr.spendByUnit).length) meta.append(el("span", null, fmtSpend(pr.spendByUnit)));
|
|
1208
|
+
}
|
|
1209
|
+
line.append(meta);
|
|
1210
|
+
head.append(line);
|
|
1211
|
+
return head;
|
|
1212
|
+
}
|
|
1213
|
+
function candidatesControl(style) {
|
|
1214
|
+
const wrap = el("span", "cand");
|
|
1215
|
+
const editKey = "cand:" + (style.project || "") + ":" + style.id;
|
|
1216
|
+
const declared = S.snap.items.filter((i) => i.project === style.project && i.styleId === style.id && i.declared).length;
|
|
1217
|
+
if (!(EDITABLE && style.candidatesEditable) || ui.editing !== editKey) {
|
|
1218
|
+
wrap.append(el("span", null, style.candidates + (style.candidates === 1 ? " candidate" : " candidates") + " per generation"));
|
|
1219
|
+
if (EDITABLE && style.candidatesEditable) {
|
|
1220
|
+
const b = el("button", null, "change");
|
|
1221
|
+
b.type = "button";
|
|
1222
|
+
b.onclick = () => {
|
|
1223
|
+
ui.editing = editKey;
|
|
1224
|
+
render();
|
|
1225
|
+
};
|
|
1226
|
+
wrap.append(b);
|
|
1227
|
+
} else if (style.provider === "pixellab") {
|
|
1228
|
+
wrap.title = "PixelLab: map and pixflux return one image per generation; a 1dir style returns 4–64 for its size.";
|
|
1229
|
+
}
|
|
1230
|
+
return wrap;
|
|
1231
|
+
}
|
|
1232
|
+
const input = el("input");
|
|
1233
|
+
input.type = "number";
|
|
1234
|
+
input.min = "1";
|
|
1235
|
+
input.max = "64";
|
|
1236
|
+
input.value = String(style.candidates);
|
|
1237
|
+
const save = el("button", "primary", "Save");
|
|
1238
|
+
save.type = "button";
|
|
1239
|
+
const cancel = el("button", null, "Cancel");
|
|
1240
|
+
cancel.type = "button";
|
|
1241
|
+
cancel.onclick = () => {
|
|
1242
|
+
ui.editing = null;
|
|
1243
|
+
render();
|
|
1244
|
+
};
|
|
1245
|
+
const note = el("span", null, "affects " + declared + (declared === 1 ? " asset, which becomes stale" : " assets, which become stale"));
|
|
1246
|
+
save.onclick = async () => {
|
|
1247
|
+
save.disabled = true;
|
|
1248
|
+
try {
|
|
1249
|
+
const pr = S.snap.workspace ? S.snap.workspace.projects.find((x) => x.id === style.project) : S.snap.project;
|
|
1250
|
+
const body = { action: "patch-style", styleId: style.id, expectedSha256: pr.manifestSha256, patch: { candidates: Number(input.value) } };
|
|
1251
|
+
if (style.project) body.project = style.project;
|
|
1252
|
+
S.snap = await postEdit(body);
|
|
1253
|
+
ui.editing = null;
|
|
1254
|
+
render();
|
|
1255
|
+
} catch (err) {
|
|
1256
|
+
save.disabled = false;
|
|
1257
|
+
note.textContent = err.message;
|
|
1258
|
+
note.className = "state-bad";
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
wrap.append(input, save, cancel, note);
|
|
1262
|
+
setTimeout(() => input.focus(), 0);
|
|
1263
|
+
return wrap;
|
|
1264
|
+
}
|
|
1265
|
+
function historySection(item) {
|
|
1266
|
+
const limit = projectOf(item)?.historyLimit;
|
|
1267
|
+
if (!item.history.length && !((limit ?? 0) > 0 && item.status === "downloaded")) return null;
|
|
1268
|
+
const s = el("section", "meta");
|
|
1269
|
+
s.append(el("h3", null, item.history.length ? "Previous generations (" + item.history.length + ")" : "Previous generations"));
|
|
1270
|
+
if (!item.history.length) {
|
|
1271
|
+
s.append(el("div", "state-dim", "None yet. A regeneration keeps the generation it replaces, up to " + limit + " per asset (PIXELKILN_HISTORY, or the manifest’s history), so it can be brought back from here."));
|
|
1272
|
+
return s;
|
|
1273
|
+
}
|
|
1274
|
+
const list = el("div", "versions");
|
|
1275
|
+
const job = GENERATION ? activeJobFor(item) : null;
|
|
1276
|
+
for (const g of item.history) {
|
|
1277
|
+
const v = el("div", "version");
|
|
1278
|
+
const thumb2 = el("div", "thumb");
|
|
1279
|
+
const first = g.outputs[0];
|
|
1280
|
+
if (first && first.url) {
|
|
1281
|
+
const img = el("img");
|
|
1282
|
+
img.src = first.url;
|
|
1283
|
+
img.alt = "generation #" + g.index;
|
|
1284
|
+
thumb2.append(img);
|
|
1285
|
+
} else thumb2.append(el("span", null, g.cached ? "" : "not cached"));
|
|
1286
|
+
const facts = el("div", "facts");
|
|
1287
|
+
const head = el("div");
|
|
1288
|
+
const billedNote = g.billed && (g.billed.unit !== g.costUnit || g.billed.amount !== g.cost) ? " (billed " + fmtCost(g.billed.unit, g.billed.amount) + ")" : "";
|
|
1289
|
+
head.append(el("b", null, "#" + g.index), document.createTextNode(" · " + fmtWhen(g.downloadedAt || g.submittedAt) + " · " + fmtCost(g.costUnit, g.cost) + billedNote + (g.outputs.length > 1 ? " · " + g.outputs.length + " files" : "")));
|
|
1290
|
+
facts.append(head);
|
|
1291
|
+
const prompt = el("div", "prompt" + (g.promptDiffers ? " differs" : ""), (g.promptDiffers ? "prompt: " : "same prompt: ") + g.prompt);
|
|
1292
|
+
prompt.title = g.prompt;
|
|
1293
|
+
facts.append(prompt);
|
|
1294
|
+
const meta = el("div", "meta");
|
|
1295
|
+
meta.append(document.createTextNode((first ? first.sha256.slice(0, 12) + "…" : "") + (g.cached ? " · bytes cached locally" : " · not in the local cache; a restore re-downloads it")));
|
|
1296
|
+
if (g.upstreamUrl) {
|
|
1297
|
+
const a = el("a", null, "open in " + item.provider + " ↗");
|
|
1298
|
+
a.href = g.upstreamUrl;
|
|
1299
|
+
a.target = "_blank";
|
|
1300
|
+
a.rel = "noopener";
|
|
1301
|
+
meta.append(document.createTextNode(" · "), a);
|
|
1302
|
+
}
|
|
1303
|
+
facts.append(meta);
|
|
1304
|
+
const acts = el("div", "acts");
|
|
1305
|
+
if (GENERATION) {
|
|
1306
|
+
const b = el("button", null, "Restore this one · no cost");
|
|
1307
|
+
b.type = "button";
|
|
1308
|
+
b.disabled = !!job;
|
|
1309
|
+
b.title = job ? "Wait for the running job to finish" : "The current generation moves into history; this one becomes current and its bytes are written back";
|
|
1310
|
+
b.onclick = () => generateDialog([item], { project: item.project, revert: String(g.index), generation: g });
|
|
1311
|
+
acts.append(b);
|
|
1312
|
+
}
|
|
1313
|
+
v.append(thumb2, facts, acts);
|
|
1314
|
+
list.append(v);
|
|
1315
|
+
}
|
|
1316
|
+
s.append(list);
|
|
1317
|
+
s.append(el("div", "state-dim", "Keeping up to " + limit + " per asset; a restore moves the current generation into this list, so it can be undone the same way." + (GENERATION ? "" : " Restoring needs the gallery started with --budget (any amount; nothing is spent).")));
|
|
1318
|
+
return s;
|
|
1319
|
+
}
|
|
1320
|
+
function upstreamSection(item) {
|
|
1321
|
+
if (!item.upstreamUrl && !(GENERATION && item.refreshable)) return null;
|
|
1322
|
+
const s = el("section", "meta");
|
|
1323
|
+
s.append(el("h3", null, "Upstream"));
|
|
1324
|
+
const dl = el("dl");
|
|
1325
|
+
if (item.upstreamUrl) {
|
|
1326
|
+
const a = el("a", null, "Open in " + item.provider + " ↗");
|
|
1327
|
+
a.href = item.upstreamUrl;
|
|
1328
|
+
a.target = "_blank";
|
|
1329
|
+
a.rel = "noopener";
|
|
1330
|
+
const tiles = item.generator === "tiles";
|
|
1331
|
+
row(dl, tiles ? "tile set" : "object", a);
|
|
1332
|
+
row(dl, "note", item.provider === "pixellab" ? tiles ? "The tile set this generation was chosen from. Edit it there with PixelLab’s editor, then pull the changes back here; the lockfile records the new bytes as this generation." : "The account object this generation came from. Edit it there with PixelLab’s editor, then pull the changes back here; the lockfile records the new bytes as this generation." : "The provider’s page for this object.");
|
|
1333
|
+
}
|
|
1334
|
+
s.append(dl);
|
|
1335
|
+
if (GENERATION && item.refreshable) {
|
|
1336
|
+
const acts = el("div", "hand-actions");
|
|
1337
|
+
const b = el("button", null, "Pull upstream changes · no cost");
|
|
1338
|
+
b.type = "button";
|
|
1339
|
+
b.onclick = () => generateDialog([item], { project: item.project, refresh: true });
|
|
1340
|
+
acts.append(b);
|
|
1341
|
+
s.append(acts);
|
|
1342
|
+
}
|
|
1343
|
+
return s;
|
|
1344
|
+
}
|
|
1345
|
+
function jobStrip(item, job) {
|
|
1346
|
+
const strip = el("div", "job-strip");
|
|
1347
|
+
const ph = el("span", "ph");
|
|
1348
|
+
ph.append(el("i", "dot busy"), document.createTextNode(job.phase));
|
|
1349
|
+
const last = el("span", "last", job.messages.length ? job.messages[job.messages.length - 1].trim() : "");
|
|
1350
|
+
last.title = job.messages.join("\n");
|
|
1351
|
+
const what = el("span", "what", (JOB_VERB[job.mode] || job.mode) + " this asset: " + (PHASE_TEXT[job.phase] || job.phase) + (job.mode === "generate" && job.phase !== "review" ? ". The current file stays until the new result is downloaded." : "."));
|
|
1352
|
+
strip.append(ph, last, what);
|
|
1353
|
+
const acts = el("div", "acts");
|
|
1354
|
+
if (job.phase === "review" && job.review.includes(item.key)) {
|
|
1355
|
+
const b = el("button", "primary", "Review candidates");
|
|
1356
|
+
b.type = "button";
|
|
1357
|
+
b.onclick = () => openReview(job.id);
|
|
1358
|
+
acts.append(b);
|
|
1359
|
+
}
|
|
1360
|
+
const logBtn = el("button", null, ui.logs.has(job.id) ? "Hide log" : "Log");
|
|
1361
|
+
logBtn.type = "button";
|
|
1362
|
+
logBtn.onclick = () => {
|
|
1363
|
+
ui.logs.has(job.id) ? ui.logs.delete(job.id) : ui.logs.add(job.id);
|
|
1364
|
+
renderDrawer();
|
|
1365
|
+
};
|
|
1366
|
+
acts.append(logBtn);
|
|
1367
|
+
strip.append(acts);
|
|
1368
|
+
if (ui.logs.has(job.id)) {
|
|
1369
|
+
const pre = el("pre", null, job.messages.join("\n"));
|
|
1370
|
+
pre.style.gridColumn = "1 / -1";
|
|
1371
|
+
strip.append(pre);
|
|
1372
|
+
}
|
|
1373
|
+
return strip;
|
|
1374
|
+
}
|
|
1375
|
+
function generateActions(item) {
|
|
1376
|
+
const row2 = el("div", "gen");
|
|
1377
|
+
if (!GENERATION || !item.declared || item.currentSpecHash === null) return row2;
|
|
1378
|
+
const cost = item.estimatedCost === null ? "" : " · " + fmtCost(item.costUnit, item.estimatedCost);
|
|
1379
|
+
const add = (label, cls, opts) => {
|
|
1380
|
+
const b = el("button", cls, label);
|
|
1381
|
+
b.type = "button";
|
|
1382
|
+
b.onclick = () => generateDialog([item], { project: item.project, ...opts });
|
|
1383
|
+
row2.append(b);
|
|
1384
|
+
};
|
|
1385
|
+
if (item.state === "missing" || item.state === "stale" || item.state === "failed") add("Generate" + cost, "primary", {});
|
|
1386
|
+
else if (item.state === "ok") add("Regenerate" + cost, null, { force: true });
|
|
1387
|
+
else if (item.state === "orphaned" && item.outputs.every((o) => o.exists)) {
|
|
1388
|
+
add("Restore recorded bytes · no cost", null, { restore: true, force: true });
|
|
1389
|
+
add("Regenerate" + cost, null, { force: true });
|
|
1390
|
+
} else if (item.state === "orphaned") add("Restore · no cost", "primary", { restore: true });
|
|
1391
|
+
else if (item.state === "untracked") add("Generate" + cost + " · replaces the file", null, { force: true });
|
|
1392
|
+
if (item.state === "recoverable" && item.status === "downloaded") {
|
|
1393
|
+
add(paletteRuleOf(item) ? "Snap to the palette · no cost" : "Put the provider’s bytes back · no cost", "primary", { resume: true });
|
|
1394
|
+
} else if (item.state === "in-flight" || item.state === "recoverable") {
|
|
1395
|
+
const holder = S.GEN.jobs.find((job) => job.phase === "review" && job.project === item.project && job.review.includes(item.key));
|
|
1396
|
+
if (holder) {
|
|
1397
|
+
const b = el("button", "primary", "Review candidates");
|
|
1398
|
+
b.type = "button";
|
|
1399
|
+
b.onclick = () => openReview(holder.id);
|
|
1400
|
+
row2.append(b);
|
|
1401
|
+
} else {
|
|
1402
|
+
add(item.status === "review" ? "Resume review" : "Resume · no cost", "primary", { resume: true });
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return row2;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
859
1408
|
// src/gallery/client/src/jobs.ts
|
|
860
1409
|
var ACTIVE_PHASES = /* @__PURE__ */ new Set(["queued", "submitting", "polling", "fetching", "review"]);
|
|
861
1410
|
var PHASE_TONE = { queued: "cool", submitting: "cool", polling: "cool", fetching: "cool", review: "warn", done: "ok", failed: "bad" };
|
|
@@ -1547,577 +2096,490 @@
|
|
|
1547
2096
|
close.type = "button";
|
|
1548
2097
|
close.onclick = () => {
|
|
1549
2098
|
host.textContent = "";
|
|
1550
|
-
pollJobs();
|
|
1551
|
-
refresh();
|
|
1552
|
-
};
|
|
1553
|
-
bar.append(close, el("span", null, "Choose from the candidates below. Apply selections writes the lockfile and downloads what you chose; unchosen rows stay in review."));
|
|
1554
|
-
const frame = el("iframe");
|
|
1555
|
-
frame.src = "/review/" + encodeURIComponent(jobId);
|
|
1556
|
-
frame.title = "Candidate review";
|
|
1557
|
-
panel.append(bar, frame);
|
|
1558
|
-
host.append(scrim, panel);
|
|
1559
|
-
}
|
|
1560
|
-
window.addEventListener("message", (e) => {
|
|
1561
|
-
if (e.origin !== location.origin || !e.data || typeof e.data.type !== "string") return;
|
|
1562
|
-
if (e.data.type === "pixelkiln:review-applied") {
|
|
1563
|
-
setTimeout(() => {
|
|
1564
|
-
$("dialog-host").textContent = "";
|
|
1565
|
-
pollJobs();
|
|
1566
|
-
refresh();
|
|
1567
|
-
}, 600);
|
|
1568
|
-
return;
|
|
1569
|
-
}
|
|
1570
|
-
if (S.SHEET && e.source === S.SHEET.frame.contentWindow) onEditorMessage(e.data);
|
|
1571
|
-
});
|
|
1572
|
-
|
|
1573
|
-
// src/gallery/client/src/grid.ts
|
|
1574
|
-
function visibleItems() {
|
|
1575
|
-
const q = ui.q.trim().toLowerCase();
|
|
1576
|
-
let items = S.snap.items.filter((item) => {
|
|
1577
|
-
if (ui.states.size && !ui.states.has(item.state)) return false;
|
|
1578
|
-
if (ui.providers.size && !ui.providers.has(item.provider)) return false;
|
|
1579
|
-
if (ui.generators.size && !ui.generators.has(item.generator)) return false;
|
|
1580
|
-
if (ui.projects.size && !ui.projects.has(item.project ?? "")) return false;
|
|
1581
|
-
if (!q) return true;
|
|
1582
|
-
const hay = [
|
|
1583
|
-
item.id,
|
|
1584
|
-
item.prompt,
|
|
1585
|
-
item.currentPrompt,
|
|
1586
|
-
item.jobId,
|
|
1587
|
-
item.objectId,
|
|
1588
|
-
item.recordedSpecHash,
|
|
1589
|
-
item.currentSpecHash,
|
|
1590
|
-
item.category,
|
|
1591
|
-
...item.tags || [],
|
|
1592
|
-
...item.outputs.flatMap((o) => [o.path, o.sha256])
|
|
1593
|
-
].filter(Boolean).join("\n").toLowerCase();
|
|
1594
|
-
return hay.includes(q);
|
|
1595
|
-
});
|
|
1596
|
-
const when = (item) => item.downloadedAt || item.submittedAt || "";
|
|
1597
|
-
const sorters = {
|
|
1598
|
-
key: (a, b) => a.id.localeCompare(b.id),
|
|
1599
|
-
newest: (a, b) => when(b).localeCompare(when(a)) || a.id.localeCompare(b.id),
|
|
1600
|
-
oldest: (a, b) => when(a).localeCompare(when(b)) || a.id.localeCompare(b.id),
|
|
1601
|
-
cost: (a, b) => b.cost - a.cost || a.id.localeCompare(b.id),
|
|
1602
|
-
size: (a, b) => b.width * b.height - a.width * a.height || a.id.localeCompare(b.id)
|
|
1603
|
-
};
|
|
1604
|
-
items.sort(sorters[ui.sort] || sorters.key);
|
|
1605
|
-
return items;
|
|
1606
|
-
}
|
|
1607
|
-
function renderHeader() {
|
|
1608
|
-
$("editing").hidden = !EDITABLE;
|
|
1609
|
-
$("foot-edit").hidden = EDITABLE;
|
|
1610
|
-
$("foot-editing").hidden = !EDITABLE;
|
|
1611
|
-
$("foot-gen").hidden = !GENERATION;
|
|
1612
|
-
const p = $("project");
|
|
1613
|
-
p.textContent = "";
|
|
1614
|
-
if (S.snap.workspace) {
|
|
1615
|
-
p.append(el("b", null, "workspace"), " ", document.createTextNode(shortPath(S.snap.workspace.path)));
|
|
1616
|
-
p.title = S.snap.workspace.path;
|
|
1617
|
-
} else {
|
|
1618
|
-
p.append(el("b", null, S.snap.project.name), " ", document.createTextNode(shortPath(S.snap.project.manifest)));
|
|
1619
|
-
p.title = S.snap.project.manifest + "\nlockfile: " + S.snap.project.lock;
|
|
1620
|
-
}
|
|
1621
|
-
const t = $("totals");
|
|
1622
|
-
t.textContent = "";
|
|
1623
|
-
if (S.snap.workspace) {
|
|
1624
|
-
const broken = S.snap.workspace.projects.filter((pr) => pr.error).length;
|
|
1625
|
-
const w = el("span", null, "");
|
|
1626
|
-
w.append(el("b", null, S.snap.workspace.projects.length), document.createTextNode(" project" + (S.snap.workspace.projects.length === 1 ? "" : "s") + (broken ? " (" + broken + " unreadable)" : "")));
|
|
1627
|
-
t.append(w);
|
|
1628
|
-
}
|
|
1629
|
-
const n = S.snap.totals.entries;
|
|
1630
|
-
t.append(el("span", null, ""));
|
|
1631
|
-
t.lastChild.append(el("b", null, n), document.createTextNode(" lock " + (n === 1 ? "entry" : "entries")));
|
|
1632
|
-
const declared = S.snap.items.filter((i) => i.declared).length;
|
|
1633
|
-
t.append(el("span", null, declared + " declared by " + (S.snap.workspace ? "a manifest" : "the manifest")));
|
|
1634
|
-
const recorded = fmtSpend(S.snap.totals.spendByUnit);
|
|
1635
|
-
t.append(el("span", null, recorded === "no spend recorded" ? recorded : recorded + " recorded"));
|
|
1636
|
-
if (S.snap.filter.styles.length || S.snap.filter.assets.length) {
|
|
1637
|
-
t.append(el("span", "state-warn", "filtered: " + [
|
|
1638
|
-
S.snap.filter.styles.length ? "--style " + S.snap.filter.styles.join(",") : "",
|
|
1639
|
-
S.snap.filter.assets.length ? "--only " + S.snap.filter.assets.join(",") : ""
|
|
1640
|
-
].filter(Boolean).join(" ")));
|
|
1641
|
-
}
|
|
1642
|
-
t.append(el("span", null, "snapshot " + fmtWhen(S.snap.generatedAt)));
|
|
1643
|
-
if (GENERATION) {
|
|
1644
|
-
const line = budgetLine();
|
|
1645
|
-
if (line) t.append(el("span", "budget", line));
|
|
1646
|
-
}
|
|
1647
|
-
const chips = $("chips");
|
|
1648
|
-
chips.textContent = "";
|
|
1649
|
-
const counts = (pick) => {
|
|
1650
|
-
const m = /* @__PURE__ */ new Map();
|
|
1651
|
-
for (const item of S.snap.items) {
|
|
1652
|
-
const k = pick(item);
|
|
1653
|
-
m.set(k, (m.get(k) || 0) + 1);
|
|
1654
|
-
}
|
|
1655
|
-
return m;
|
|
1656
|
-
};
|
|
1657
|
-
const chip = (label, count, set, key, tone) => {
|
|
1658
|
-
const b = el("button", "chip" + (set.has(key) ? " on" : ""));
|
|
1659
|
-
b.type = "button";
|
|
1660
|
-
b.setAttribute("aria-pressed", String(set.has(key)));
|
|
1661
|
-
if (tone) b.append(el("i", "dot " + tone));
|
|
1662
|
-
b.append(document.createTextNode(label), el("span", "n", count));
|
|
1663
|
-
b.onclick = () => {
|
|
1664
|
-
set.has(key) ? set.delete(key) : set.add(key);
|
|
1665
|
-
render();
|
|
1666
|
-
};
|
|
1667
|
-
return b;
|
|
2099
|
+
pollJobs();
|
|
2100
|
+
refresh();
|
|
1668
2101
|
};
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
chips.append(el("span", "sep"));
|
|
1686
|
-
for (const [k, v] of [...byGen].sort()) chips.append(chip(k, v, ui.generators, k));
|
|
2102
|
+
bar.append(close, el("span", null, "Choose from the candidates below. Apply selections writes the lockfile and downloads what you chose; unchosen rows stay in review."));
|
|
2103
|
+
const frame = el("iframe");
|
|
2104
|
+
frame.src = "/review/" + encodeURIComponent(jobId);
|
|
2105
|
+
frame.title = "Candidate review";
|
|
2106
|
+
panel.append(bar, frame);
|
|
2107
|
+
host.append(scrim, panel);
|
|
2108
|
+
}
|
|
2109
|
+
window.addEventListener("message", (e) => {
|
|
2110
|
+
if (e.origin !== location.origin || !e.data || typeof e.data.type !== "string") return;
|
|
2111
|
+
if (e.data.type === "pixelkiln:review-applied") {
|
|
2112
|
+
setTimeout(() => {
|
|
2113
|
+
$("dialog-host").textContent = "";
|
|
2114
|
+
pollJobs();
|
|
2115
|
+
refresh();
|
|
2116
|
+
}, 600);
|
|
2117
|
+
return;
|
|
1687
2118
|
}
|
|
1688
|
-
if (
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
2119
|
+
if (S.SHEET && e.source === S.SHEET.frame.contentWindow) onEditorMessage(e.data);
|
|
2120
|
+
});
|
|
2121
|
+
|
|
2122
|
+
// src/gallery/client/src/family.ts
|
|
2123
|
+
var COMPASS = ["south", "south-east", "east", "north-east", "north", "north-west", "west", "south-west"];
|
|
2124
|
+
var SHORT = {
|
|
2125
|
+
south: "S",
|
|
2126
|
+
"south-east": "SE",
|
|
2127
|
+
east: "E",
|
|
2128
|
+
"north-east": "NE",
|
|
2129
|
+
north: "N",
|
|
2130
|
+
"north-west": "NW",
|
|
2131
|
+
west: "W",
|
|
2132
|
+
"south-west": "SW"
|
|
2133
|
+
};
|
|
2134
|
+
var ROSE = ["north-west", "north", "north-east", "west", null, "east", "south-west", "south", "south-east"];
|
|
2135
|
+
var SPEEDS = [0.5, 1, 2];
|
|
2136
|
+
var SPIN_MS = 420;
|
|
2137
|
+
var DRAG_STEP_PX = 28;
|
|
2138
|
+
var F = null;
|
|
2139
|
+
var reducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2140
|
+
var byKey = (project, key) => S.snap.items.find((i) => i.key === key && i.project === project);
|
|
2141
|
+
function familyRoot(item) {
|
|
2142
|
+
let at = item;
|
|
2143
|
+
for (let guard = 0; at && at.character && at.character.parentKey && guard < 16; guard++) {
|
|
2144
|
+
at = byKey(at.project, at.character.parentKey);
|
|
1693
2145
|
}
|
|
2146
|
+
return at && at.character && !at.character.parentKey ? at : null;
|
|
1694
2147
|
}
|
|
1695
|
-
function
|
|
1696
|
-
|
|
1697
|
-
ui.providers.clear();
|
|
1698
|
-
ui.generators.clear();
|
|
1699
|
-
ui.projects.clear();
|
|
1700
|
-
ui.q = "";
|
|
1701
|
-
$("q").value = "";
|
|
1702
|
-
render();
|
|
2148
|
+
function familyMembers(root) {
|
|
2149
|
+
return S.snap.items.filter((i) => i.project === root.project && i.character && familyRoot(i) === root);
|
|
1703
2150
|
}
|
|
1704
|
-
function
|
|
1705
|
-
const
|
|
1706
|
-
|
|
1707
|
-
if (!shown.length) {
|
|
1708
|
-
cell.classList.add("none");
|
|
1709
|
-
cell.append(el("span", null, item.state === "missing" ? "not generated yet" : item.state === "in-flight" ? item.status === "review" ? "awaiting review" : "awaiting provider" : item.state === "recoverable" ? "ready to fetch" : item.state === "failed" ? "generation failed" : item.state === "blocked" ? "waiting on its parent" : item.outputs.length ? "file missing on disk" : "no output recorded"));
|
|
1710
|
-
return cell;
|
|
1711
|
-
}
|
|
1712
|
-
const w = item.width, h = item.height;
|
|
1713
|
-
if (shown.length > 1 && !isFrameSet(item)) {
|
|
1714
|
-
const multi = el("div", "multi");
|
|
1715
|
-
for (const o of shown.slice(0, 4)) {
|
|
1716
|
-
const box = el("div");
|
|
1717
|
-
const img2 = el("img");
|
|
1718
|
-
img2.src = o.url;
|
|
1719
|
-
img2.alt = o.role || item.assetId;
|
|
1720
|
-
img2.loading = "lazy";
|
|
1721
|
-
const s2 = displayScale(w, h, 66, 52);
|
|
1722
|
-
if (s2 >= 1) {
|
|
1723
|
-
img2.width = w * s2;
|
|
1724
|
-
img2.height = h * s2;
|
|
1725
|
-
} else img2.className = "fit";
|
|
1726
|
-
box.append(img2);
|
|
1727
|
-
multi.append(box);
|
|
1728
|
-
}
|
|
1729
|
-
cell.append(multi, el("span", "badge", shown.length + " outputs"));
|
|
1730
|
-
return cell;
|
|
1731
|
-
}
|
|
1732
|
-
const img = el("img");
|
|
1733
|
-
img.src = shown[0].url;
|
|
1734
|
-
img.alt = item.assetId;
|
|
1735
|
-
img.loading = "lazy";
|
|
1736
|
-
const s = displayScale(w, h, 156, 116);
|
|
1737
|
-
if (s >= 1) {
|
|
1738
|
-
img.width = w * s;
|
|
1739
|
-
img.height = h * s;
|
|
1740
|
-
} else img.className = "fit";
|
|
1741
|
-
cell.append(img);
|
|
1742
|
-
if (isFrameSet(item)) cell.append(el("span", "badge", "▸ " + shown.length + " frames"));
|
|
1743
|
-
else if (s >= 1 && s !== 1) cell.append(el("span", "badge", s + "×"));
|
|
1744
|
-
return cell;
|
|
2151
|
+
function loopName(item) {
|
|
2152
|
+
const d = item.character && item.character.direction;
|
|
2153
|
+
return d && item.assetId.endsWith("." + d) ? item.assetId.slice(0, -(d.length + 1)) : item.assetId;
|
|
1745
2154
|
}
|
|
1746
|
-
function
|
|
1747
|
-
const
|
|
1748
|
-
const
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
if (e.shiftKey) toggleCompare(item.id);
|
|
1779
|
-
else openItem(item.id);
|
|
2155
|
+
function turnOrder(item) {
|
|
2156
|
+
const roles = new Set(item.outputs.map((o) => o.role));
|
|
2157
|
+
const shown = COMPASS.filter((d) => roles.has(d));
|
|
2158
|
+
if (shown.length) return shown;
|
|
2159
|
+
const n = item.character ? item.character.directions : 1;
|
|
2160
|
+
return n >= 8 ? COMPASS : n === 4 ? ["south", "east", "north", "west"] : ["south"];
|
|
2161
|
+
}
|
|
2162
|
+
var rotationUrl = (item, direction) => {
|
|
2163
|
+
const out = item && item.outputs.find((o) => o.role === direction && o.url);
|
|
2164
|
+
return out ? out.url : item && direction === "south" && item.outputs[0] && item.outputs[0].url && !item.outputs[0].role ? item.outputs[0].url : null;
|
|
2165
|
+
};
|
|
2166
|
+
var frameUrls = (item) => item.outputs.filter((o) => o.url).map((o) => o.url);
|
|
2167
|
+
function openFamily(item) {
|
|
2168
|
+
const root = familyRoot(item);
|
|
2169
|
+
if (!root) return;
|
|
2170
|
+
stopFamily();
|
|
2171
|
+
const turnable = item.character && (item.character.kind === "base" || item.character.kind === "state") ? item : root;
|
|
2172
|
+
F = {
|
|
2173
|
+
rootId: root.id,
|
|
2174
|
+
drawn: null,
|
|
2175
|
+
source: turnable.id,
|
|
2176
|
+
direction: item.character && item.character.direction && turnOrder(turnable).includes(item.character.direction) ? item.character.direction : "south",
|
|
2177
|
+
spinning: false,
|
|
2178
|
+
playing: !reducedMotion(),
|
|
2179
|
+
speed: 1,
|
|
2180
|
+
t0: performance.now(),
|
|
2181
|
+
lastSpin: 0,
|
|
2182
|
+
raf: null,
|
|
2183
|
+
panel: null,
|
|
2184
|
+
anims: [],
|
|
2185
|
+
columns: /* @__PURE__ */ new Map(),
|
|
2186
|
+
turn: null
|
|
1780
2187
|
};
|
|
1781
|
-
|
|
2188
|
+
renderFamily();
|
|
1782
2189
|
}
|
|
1783
|
-
function
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
2190
|
+
function closeFamily() {
|
|
2191
|
+
stopFamily();
|
|
2192
|
+
$("dialog-host").textContent = "";
|
|
2193
|
+
}
|
|
2194
|
+
function stopFamily() {
|
|
2195
|
+
if (F && F.raf !== null) cancelAnimationFrame(F.raf);
|
|
2196
|
+
F = null;
|
|
2197
|
+
}
|
|
2198
|
+
function refreshFamily() {
|
|
2199
|
+
if (!F) return;
|
|
2200
|
+
if (!F.panel || !F.panel.isConnected) {
|
|
2201
|
+
stopFamily();
|
|
1791
2202
|
return;
|
|
1792
2203
|
}
|
|
1793
|
-
if (
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
root.append(e);
|
|
2204
|
+
if (F.drawn !== S.snap) renderFamily();
|
|
2205
|
+
}
|
|
2206
|
+
function renderFamily() {
|
|
2207
|
+
if (!F) return;
|
|
2208
|
+
const root = S.snap.items.find((i) => i.id === F.rootId);
|
|
2209
|
+
if (!root) {
|
|
2210
|
+
closeFamily();
|
|
1801
2211
|
return;
|
|
1802
2212
|
}
|
|
1803
|
-
|
|
1804
|
-
const
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
meta.append(sw);
|
|
1836
|
-
}
|
|
1837
|
-
if (s.quality) meta.append(el("span", null, "quality profile"));
|
|
1838
|
-
if (s.characters) {
|
|
1839
|
-
const parts = [];
|
|
1840
|
-
parts.push(s.characters.bases + (s.characters.bases === 1 ? " character" : " characters"));
|
|
1841
|
-
if (s.characters.states) parts.push(s.characters.states + (s.characters.states === 1 ? " state" : " states"));
|
|
1842
|
-
if (s.characters.animations) parts.push(s.characters.animations + (s.characters.animations === 1 ? " loop" : " loops"));
|
|
1843
|
-
if (s.characters.portraits) parts.push(s.characters.portraits + (s.characters.portraits === 1 ? " portrait" : " portraits"));
|
|
1844
|
-
if (s.characters.outfits) parts.push(s.characters.outfits + (s.characters.outfits === 1 ? " outfit" : " outfits"));
|
|
1845
|
-
meta.append(el("span", null, parts.join(", ")));
|
|
1846
|
-
}
|
|
1847
|
-
meta.append(el("span", null, sec.items.length + " of " + s.items + (s.items === 1 ? " asset" : " assets")));
|
|
1848
|
-
if (Object.keys(s.spendByUnit).length) meta.append(el("span", null, fmtSpend(s.spendByUnit)));
|
|
1849
|
-
head.append(meta);
|
|
1850
|
-
const addKey = "new:" + (s.project || "") + ":" + s.id;
|
|
1851
|
-
const tools = el("div", "tools");
|
|
1852
|
-
if (s.outDir && s.candidates !== null) tools.append(candidatesControl(s));
|
|
1853
|
-
if (GENERATION && s.actionable.keys.length) {
|
|
1854
|
-
const g = el("button", "primary", "Generate " + s.actionable.keys.length + " · " + fmtCost(s.actionable.costUnit, Math.round(s.actionable.cost * 100) / 100));
|
|
1855
|
-
g.type = "button";
|
|
1856
|
-
g.onclick = () => generateDialog(S.snap.items.filter((i) => i.project === s.project && s.actionable.keys.includes(i.key)), { project: s.project });
|
|
1857
|
-
tools.append(g);
|
|
1858
|
-
}
|
|
1859
|
-
const refreshable = S.snap.items.filter((i) => i.project === s.project && i.styleId === s.id && i.refreshable && i.upstreamUrl);
|
|
1860
|
-
if (GENERATION && refreshable.length) {
|
|
1861
|
-
const r = el("button", null, "Pull upstream " + refreshable.length);
|
|
1862
|
-
r.type = "button";
|
|
1863
|
-
r.title = "Re-download objects edited in the provider’s own editor; no cost";
|
|
1864
|
-
r.onclick = () => generateDialog(refreshable, { project: s.project, refresh: true });
|
|
1865
|
-
tools.append(r);
|
|
1866
|
-
}
|
|
1867
|
-
const styleKey = "style:" + (s.project || "") + ":" + s.id;
|
|
1868
|
-
if (EDITABLE && s.outDir) {
|
|
1869
|
-
const es = el("button", null, ui.editing === styleKey ? "Cancel" : "Edit style");
|
|
1870
|
-
es.type = "button";
|
|
1871
|
-
es.onclick = () => {
|
|
1872
|
-
ui.editing = ui.editing === styleKey ? null : styleKey;
|
|
1873
|
-
ui.notice = null;
|
|
1874
|
-
render();
|
|
1875
|
-
};
|
|
1876
|
-
tools.append(es);
|
|
1877
|
-
const add = el("button", "add", ui.editing === addKey ? "Cancel" : "+ Add asset");
|
|
1878
|
-
add.type = "button";
|
|
1879
|
-
add.onclick = () => {
|
|
1880
|
-
ui.editing = ui.editing === addKey ? null : addKey;
|
|
1881
|
-
render();
|
|
1882
|
-
};
|
|
1883
|
-
tools.append(add);
|
|
1884
|
-
}
|
|
1885
|
-
if (tools.childNodes.length) head.append(tools);
|
|
1886
|
-
wrap.append(head);
|
|
1887
|
-
if (ui.notice && ui.notice.id === styleKey) wrap.append(el("div", "notice", ui.notice.text));
|
|
1888
|
-
if (ui.editing === styleKey) wrap.append(styleForm(s));
|
|
1889
|
-
if (ui.editing === addKey) wrap.append(addAssetForm(s));
|
|
1890
|
-
}
|
|
1891
|
-
const grid = el("div", "grid");
|
|
1892
|
-
for (const item of sec.items) grid.append(card(item));
|
|
1893
|
-
wrap.append(grid);
|
|
1894
|
-
root.append(wrap);
|
|
1895
|
-
}
|
|
1896
|
-
if (hidden) {
|
|
1897
|
-
const more = el("div", "empty");
|
|
1898
|
-
more.append(el("p", null, hidden + " more " + (hidden === 1 ? "generation is" : "generations are") + " not shown."));
|
|
1899
|
-
const b = el("button", null, "Show all " + items.length);
|
|
1900
|
-
b.type = "button";
|
|
1901
|
-
b.onclick = () => {
|
|
1902
|
-
ui.limit = Infinity;
|
|
1903
|
-
render();
|
|
1904
|
-
};
|
|
1905
|
-
more.append(b);
|
|
1906
|
-
root.append(more);
|
|
2213
|
+
F.drawn = S.snap;
|
|
2214
|
+
const members = familyMembers(root);
|
|
2215
|
+
const turnables = members.filter((m) => m.character.kind === "base" || m.character.kind === "state");
|
|
2216
|
+
if (!turnables.some((m) => m.id === F.source)) F.source = root.id;
|
|
2217
|
+
const source = turnables.find((m) => m.id === F.source) || root;
|
|
2218
|
+
const order = turnOrder(source);
|
|
2219
|
+
if (!order.includes(F.direction)) F.direction = order[0];
|
|
2220
|
+
const host = $("dialog-host");
|
|
2221
|
+
const scroll = F.panel ? F.panel.scrollTop : 0;
|
|
2222
|
+
host.textContent = "";
|
|
2223
|
+
F.anims = [];
|
|
2224
|
+
F.columns = /* @__PURE__ */ new Map();
|
|
2225
|
+
const scrim = el("div", "sheet-scrim");
|
|
2226
|
+
scrim.onclick = closeFamily;
|
|
2227
|
+
const panel = el("div", "sheet family-sheet");
|
|
2228
|
+
panel.setAttribute("role", "dialog");
|
|
2229
|
+
panel.setAttribute("aria-label", "Family of " + root.key);
|
|
2230
|
+
F.panel = panel;
|
|
2231
|
+
const bar = el("div", "rbar");
|
|
2232
|
+
const back = el("button", null, "Back to gallery");
|
|
2233
|
+
back.type = "button";
|
|
2234
|
+
back.onclick = closeFamily;
|
|
2235
|
+
const title = el("b", "mono", root.key);
|
|
2236
|
+
const c = root.character;
|
|
2237
|
+
bar.append(back, title, el("span", null, (c.generator === "objectPro" ? "object" : "character") + " · " + c.mode + " · " + root.width + "×" + root.height));
|
|
2238
|
+
const tally = el("span", "family-tally");
|
|
2239
|
+
const counts = {};
|
|
2240
|
+
for (const m of members) counts[m.state] = (counts[m.state] || 0) + 1;
|
|
2241
|
+
for (const [state, n] of Object.entries(counts)) {
|
|
2242
|
+
const t = el("span", "st");
|
|
2243
|
+
t.append(el("i", "dot " + STATE_TONE[state]), document.createTextNode(n + " " + state));
|
|
2244
|
+
tally.append(t);
|
|
1907
2245
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2246
|
+
bar.append(tally);
|
|
2247
|
+
if (GENERATION) {
|
|
2248
|
+
const todo = members.filter((m) => m.declared && m.currentSpecHash !== null && ["missing", "stale", "failed"].includes(m.state));
|
|
2249
|
+
if (todo.length) {
|
|
2250
|
+
const totals = {};
|
|
2251
|
+
for (const m of todo) if (m.estimatedCost !== null) totals[m.costUnit] = (totals[m.costUnit] || 0) + m.estimatedCost;
|
|
2252
|
+
const price = Object.entries(totals).map(([unit, n]) => fmtCost(unit, Math.round(n * 100) / 100)).join(" + ");
|
|
2253
|
+
const g = el("button", "primary", "Generate " + todo.length + (price ? " · " + price : ""));
|
|
2254
|
+
g.type = "button";
|
|
2255
|
+
g.title = "One job for the whole family: parents first, then everything drawn from them";
|
|
2256
|
+
g.onclick = () => {
|
|
2257
|
+
stopFamily();
|
|
2258
|
+
generateDialog(todo, { project: root.project });
|
|
2259
|
+
};
|
|
2260
|
+
bar.append(g);
|
|
2261
|
+
}
|
|
1922
2262
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2263
|
+
panel.append(bar);
|
|
2264
|
+
const body = el("div", "family-body");
|
|
2265
|
+
body.append(turntable(root, turnables, source, order));
|
|
2266
|
+
body.append(loopGrid(root, members, order));
|
|
2267
|
+
const extras = members.filter((m) => m.character.kind === "portrait" || m.character.kind === "outfit");
|
|
2268
|
+
if (extras.length) body.append(extrasStrip(extras));
|
|
2269
|
+
panel.append(body);
|
|
2270
|
+
host.append(scrim, panel);
|
|
2271
|
+
panel.scrollTop = scroll;
|
|
2272
|
+
highlight(F.direction);
|
|
2273
|
+
if (F.raf === null) F.raf = requestAnimationFrame(tick);
|
|
1926
2274
|
}
|
|
1927
|
-
function
|
|
1928
|
-
const wrap = el("
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
if (EDITABLE && style.candidatesEditable) {
|
|
1934
|
-
const b = el("button", null, "change");
|
|
2275
|
+
function turntable(root, turnables, source, order) {
|
|
2276
|
+
const wrap = el("section", "turntable");
|
|
2277
|
+
if (turnables.length > 1) {
|
|
2278
|
+
const tabs = el("div", "family-tabs");
|
|
2279
|
+
for (const t of turnables) {
|
|
2280
|
+
const b = el("button", t.id === source.id ? "on" : null, t.id === root.id ? "base" : t.assetId.replace(root.assetId + ".", ""));
|
|
1935
2281
|
b.type = "button";
|
|
2282
|
+
b.title = t.key + " · " + t.state;
|
|
1936
2283
|
b.onclick = () => {
|
|
1937
|
-
|
|
1938
|
-
|
|
2284
|
+
F.source = t.id;
|
|
2285
|
+
renderFamily();
|
|
1939
2286
|
};
|
|
1940
|
-
|
|
1941
|
-
} else if (style.provider === "pixellab") {
|
|
1942
|
-
wrap.title = "PixelLab: map and pixflux return one image per generation; a 1dir style returns 4–64 for its size.";
|
|
2287
|
+
tabs.append(b);
|
|
1943
2288
|
}
|
|
1944
|
-
|
|
2289
|
+
wrap.append(tabs);
|
|
1945
2290
|
}
|
|
1946
|
-
const
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
const
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
2291
|
+
const stage = el("div", "tt-stage");
|
|
2292
|
+
stage.tabIndex = 0;
|
|
2293
|
+
stage.setAttribute("role", "img");
|
|
2294
|
+
const img = el("img");
|
|
2295
|
+
const empty = el("div", "tt-empty");
|
|
2296
|
+
const scale = displayScale(source.width, source.height, 256, 256);
|
|
2297
|
+
img.width = Math.round(source.width * scale);
|
|
2298
|
+
img.height = Math.round(source.height * scale);
|
|
2299
|
+
img.alt = source.assetId;
|
|
2300
|
+
stage.append(img, empty);
|
|
2301
|
+
for (const d of order) {
|
|
2302
|
+
const u = rotationUrl(source, d);
|
|
2303
|
+
if (u) {
|
|
2304
|
+
const p = new Image();
|
|
2305
|
+
p.src = u;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
const label = el("div", "tt-label");
|
|
2309
|
+
const rose = el("div", "tt-rose");
|
|
2310
|
+
const roseButtons = /* @__PURE__ */ new Map();
|
|
2311
|
+
for (const d of ROSE) {
|
|
2312
|
+
if (!d) {
|
|
2313
|
+
rose.append(el("i", "tt-hub"));
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
const b = el("button", null, SHORT[d]);
|
|
2317
|
+
b.type = "button";
|
|
2318
|
+
b.title = d;
|
|
2319
|
+
b.disabled = !order.includes(d);
|
|
2320
|
+
b.onclick = () => turn(d);
|
|
2321
|
+
roseButtons.set(d, b);
|
|
2322
|
+
rose.append(b);
|
|
2323
|
+
}
|
|
2324
|
+
const show = (direction) => {
|
|
2325
|
+
const url = rotationUrl(source, direction);
|
|
2326
|
+
img.hidden = !url;
|
|
2327
|
+
empty.hidden = !!url;
|
|
2328
|
+
if (url) img.src = url;
|
|
2329
|
+
else empty.textContent = source.state === "ok" ? "no " + direction + " rotation" : source.state + ": " + (source.reason || "not generated yet");
|
|
2330
|
+
stage.setAttribute("aria-label", source.assetId + " facing " + direction);
|
|
2331
|
+
label.textContent = direction + " · " + (order.indexOf(direction) + 1) + " of " + order.length;
|
|
2332
|
+
for (const [d, b] of roseButtons) b.classList.toggle("on", d === direction);
|
|
1958
2333
|
};
|
|
1959
|
-
const
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2334
|
+
const turn = (direction) => {
|
|
2335
|
+
if (!F) return;
|
|
2336
|
+
F.direction = direction;
|
|
2337
|
+
show(direction);
|
|
2338
|
+
highlight(direction);
|
|
2339
|
+
};
|
|
2340
|
+
const stepBy = (n) => {
|
|
2341
|
+
const i = order.indexOf(F.direction);
|
|
2342
|
+
turn(order[(i + n + order.length * 4) % order.length]);
|
|
2343
|
+
};
|
|
2344
|
+
F.turn = turn;
|
|
2345
|
+
show(F.direction);
|
|
2346
|
+
let dragX = null;
|
|
2347
|
+
stage.onpointerdown = (e) => {
|
|
2348
|
+
dragX = e.clientX;
|
|
2349
|
+
stage.setPointerCapture(e.pointerId);
|
|
2350
|
+
F.spinning = false;
|
|
2351
|
+
spin.classList.remove("on");
|
|
2352
|
+
};
|
|
2353
|
+
stage.onpointermove = (e) => {
|
|
2354
|
+
if (dragX === null) return;
|
|
2355
|
+
const steps = Math.trunc((e.clientX - dragX) / DRAG_STEP_PX);
|
|
2356
|
+
if (steps) {
|
|
2357
|
+
dragX += steps * DRAG_STEP_PX;
|
|
2358
|
+
stepBy(steps);
|
|
1973
2359
|
}
|
|
1974
2360
|
};
|
|
1975
|
-
|
|
1976
|
-
|
|
2361
|
+
stage.onpointerup = stage.onpointercancel = () => {
|
|
2362
|
+
dragX = null;
|
|
2363
|
+
};
|
|
2364
|
+
stage.onkeydown = (e) => {
|
|
2365
|
+
if (e.key === "ArrowRight") {
|
|
2366
|
+
e.preventDefault();
|
|
2367
|
+
e.stopPropagation();
|
|
2368
|
+
stepBy(1);
|
|
2369
|
+
} else if (e.key === "ArrowLeft") {
|
|
2370
|
+
e.preventDefault();
|
|
2371
|
+
e.stopPropagation();
|
|
2372
|
+
stepBy(-1);
|
|
2373
|
+
}
|
|
2374
|
+
};
|
|
2375
|
+
const controls = el("div", "tt-controls");
|
|
2376
|
+
const prev = el("button", null, "◀");
|
|
2377
|
+
prev.type = "button";
|
|
2378
|
+
prev.title = "Turn back";
|
|
2379
|
+
prev.onclick = () => stepBy(-1);
|
|
2380
|
+
const next = el("button", null, "▶");
|
|
2381
|
+
next.type = "button";
|
|
2382
|
+
next.title = "Turn on";
|
|
2383
|
+
next.onclick = () => stepBy(1);
|
|
2384
|
+
const spin = el("button", F.spinning ? "on" : null, "Spin");
|
|
2385
|
+
spin.type = "button";
|
|
2386
|
+
spin.title = "Turn through every rotation on a loop";
|
|
2387
|
+
spin.onclick = () => {
|
|
2388
|
+
F.spinning = !F.spinning;
|
|
2389
|
+
F.lastSpin = performance.now();
|
|
2390
|
+
spin.classList.toggle("on", F.spinning);
|
|
2391
|
+
};
|
|
2392
|
+
prev.disabled = next.disabled = spin.disabled = order.length < 2;
|
|
2393
|
+
controls.append(prev, next, spin);
|
|
2394
|
+
const side = el("div", "tt-side");
|
|
2395
|
+
side.append(rose, controls, label);
|
|
2396
|
+
const open = el("button", "linkish", "Open " + source.assetId);
|
|
2397
|
+
open.type = "button";
|
|
2398
|
+
open.onclick = () => {
|
|
2399
|
+
closeFamily();
|
|
2400
|
+
openItem(source.id);
|
|
2401
|
+
};
|
|
2402
|
+
side.append(open);
|
|
2403
|
+
const row2 = el("div", "tt-row");
|
|
2404
|
+
row2.append(stage, side);
|
|
2405
|
+
wrap.append(row2);
|
|
2406
|
+
wrap.append(el("small", "state-dim", order.length > 1 ? "Drag the sprite sideways, use ← and →, or pick a direction to turn it. The loops below follow." : "One direction: this " + (root.character.generator === "objectPro" ? "object" : "character") + " does not turn."));
|
|
1977
2407
|
return wrap;
|
|
1978
2408
|
}
|
|
1979
|
-
function
|
|
1980
|
-
const
|
|
1981
|
-
|
|
1982
|
-
const
|
|
1983
|
-
|
|
1984
|
-
if (
|
|
1985
|
-
|
|
1986
|
-
|
|
2409
|
+
function loopGrid(root, members, order) {
|
|
2410
|
+
const loops = members.filter((m) => m.character.kind === "animation");
|
|
2411
|
+
const wrap = el("section", "loop-grid");
|
|
2412
|
+
const head = el("div", "lg-head");
|
|
2413
|
+
head.append(el("h3", null, loops.length ? "Loops" : "No loops yet"));
|
|
2414
|
+
if (loops.length) {
|
|
2415
|
+
const play = el("button", null, F.playing ? "Pause" : "Play");
|
|
2416
|
+
play.type = "button";
|
|
2417
|
+
play.onclick = () => {
|
|
2418
|
+
F.playing = !F.playing;
|
|
2419
|
+
play.textContent = F.playing ? "Pause" : "Play";
|
|
2420
|
+
if (F.playing) F.t0 = performance.now();
|
|
2421
|
+
};
|
|
2422
|
+
head.append(play);
|
|
2423
|
+
const speed = el("select");
|
|
2424
|
+
speed.setAttribute("aria-label", "Playback speed");
|
|
2425
|
+
for (const s of SPEEDS) speed.append(new Option(s + "×", String(s)));
|
|
2426
|
+
speed.value = String(F.speed);
|
|
2427
|
+
speed.onchange = () => {
|
|
2428
|
+
F.speed = Number(speed.value);
|
|
2429
|
+
F.t0 = performance.now();
|
|
2430
|
+
};
|
|
2431
|
+
head.append(speed);
|
|
1987
2432
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
const thumb2 = el("div", "thumb");
|
|
1993
|
-
const first = g.outputs[0];
|
|
1994
|
-
if (first && first.url) {
|
|
1995
|
-
const img = el("img");
|
|
1996
|
-
img.src = first.url;
|
|
1997
|
-
img.alt = "generation #" + g.index;
|
|
1998
|
-
thumb2.append(img);
|
|
1999
|
-
} else thumb2.append(el("span", null, g.cached ? "" : "not cached"));
|
|
2000
|
-
const facts = el("div", "facts");
|
|
2001
|
-
const head = el("div");
|
|
2002
|
-
const billedNote = g.billed && (g.billed.unit !== g.costUnit || g.billed.amount !== g.cost) ? " (billed " + fmtCost(g.billed.unit, g.billed.amount) + ")" : "";
|
|
2003
|
-
head.append(el("b", null, "#" + g.index), document.createTextNode(" · " + fmtWhen(g.downloadedAt || g.submittedAt) + " · " + fmtCost(g.costUnit, g.cost) + billedNote + (g.outputs.length > 1 ? " · " + g.outputs.length + " files" : "")));
|
|
2004
|
-
facts.append(head);
|
|
2005
|
-
const prompt = el("div", "prompt" + (g.promptDiffers ? " differs" : ""), (g.promptDiffers ? "prompt: " : "same prompt: ") + g.prompt);
|
|
2006
|
-
prompt.title = g.prompt;
|
|
2007
|
-
facts.append(prompt);
|
|
2008
|
-
const meta = el("div", "meta");
|
|
2009
|
-
meta.append(document.createTextNode((first ? first.sha256.slice(0, 12) + "…" : "") + (g.cached ? " · bytes cached locally" : " · not in the local cache; a restore re-downloads it")));
|
|
2010
|
-
if (g.upstreamUrl) {
|
|
2011
|
-
const a = el("a", null, "open in " + item.provider + " ↗");
|
|
2012
|
-
a.href = g.upstreamUrl;
|
|
2013
|
-
a.target = "_blank";
|
|
2014
|
-
a.rel = "noopener";
|
|
2015
|
-
meta.append(document.createTextNode(" · "), a);
|
|
2016
|
-
}
|
|
2017
|
-
facts.append(meta);
|
|
2018
|
-
const acts = el("div", "acts");
|
|
2019
|
-
if (GENERATION) {
|
|
2020
|
-
const b = el("button", null, "Restore this one · no cost");
|
|
2021
|
-
b.type = "button";
|
|
2022
|
-
b.disabled = !!job;
|
|
2023
|
-
b.title = job ? "Wait for the running job to finish" : "The current generation moves into history; this one becomes current and its bytes are written back";
|
|
2024
|
-
b.onclick = () => generateDialog([item], { project: item.project, revert: String(g.index), generation: g });
|
|
2025
|
-
acts.append(b);
|
|
2026
|
-
}
|
|
2027
|
-
v.append(thumb2, facts, acts);
|
|
2028
|
-
list.append(v);
|
|
2433
|
+
wrap.append(head);
|
|
2434
|
+
if (!loops.length) {
|
|
2435
|
+
wrap.append(el("p", "state-dim", EDITABLE ? "Add one from the base's drawer (+ New animation), or draft a whole character in the studio." : "None declared."));
|
|
2436
|
+
return wrap;
|
|
2029
2437
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
s.append(el("h3", null, "Upstream"));
|
|
2038
|
-
const dl = el("dl");
|
|
2039
|
-
if (item.upstreamUrl) {
|
|
2040
|
-
const a = el("a", null, "Open in " + item.provider + " ↗");
|
|
2041
|
-
a.href = item.upstreamUrl;
|
|
2042
|
-
a.target = "_blank";
|
|
2043
|
-
a.rel = "noopener";
|
|
2044
|
-
const tiles = item.generator === "tiles";
|
|
2045
|
-
row(dl, tiles ? "tile set" : "object", a);
|
|
2046
|
-
row(dl, "note", item.provider === "pixellab" ? tiles ? "The tile set this generation was chosen from. Edit it there with PixelLab’s editor, then pull the changes back here; the lockfile records the new bytes as this generation." : "The account object this generation came from. Edit it there with PixelLab’s editor, then pull the changes back here; the lockfile records the new bytes as this generation." : "The provider’s page for this object.");
|
|
2438
|
+
const used = /* @__PURE__ */ new Set([...order, ...loops.map((l) => l.character.direction).filter(Boolean)]);
|
|
2439
|
+
const columns = COMPASS.filter((d) => used.has(d));
|
|
2440
|
+
const rows = /* @__PURE__ */ new Map();
|
|
2441
|
+
for (const l of loops) {
|
|
2442
|
+
const k = (l.character.parentKey || "") + "|" + loopName(l);
|
|
2443
|
+
if (!rows.has(k)) rows.set(k, []);
|
|
2444
|
+
rows.get(k).push(l);
|
|
2047
2445
|
}
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2446
|
+
const table = el("table", "lg");
|
|
2447
|
+
const thead = el("thead");
|
|
2448
|
+
const hr = el("tr");
|
|
2449
|
+
hr.append(el("th", "lg-name", ""));
|
|
2450
|
+
for (const d of columns) {
|
|
2451
|
+
const th = el("th", null);
|
|
2452
|
+
const b = el("button", "lg-dir", SHORT[d]);
|
|
2052
2453
|
b.type = "button";
|
|
2053
|
-
b.
|
|
2054
|
-
|
|
2055
|
-
|
|
2454
|
+
b.title = "Turn to " + d;
|
|
2455
|
+
b.disabled = !order.includes(d);
|
|
2456
|
+
b.onclick = () => F && F.turn && F.turn(d);
|
|
2457
|
+
th.append(b);
|
|
2458
|
+
hr.append(th);
|
|
2459
|
+
column(d, th);
|
|
2056
2460
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
const
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2461
|
+
thead.append(hr);
|
|
2462
|
+
table.append(thead);
|
|
2463
|
+
const tbody = el("tbody");
|
|
2464
|
+
for (const [k, cells] of rows) {
|
|
2465
|
+
const parentKey = k.split("|")[0];
|
|
2466
|
+
const parent = byKey(root.project, parentKey) || root;
|
|
2467
|
+
const tr = el("tr");
|
|
2468
|
+
const th = el("th", "lg-name");
|
|
2469
|
+
const name = loopName(cells[0]);
|
|
2470
|
+
th.append(el("b", "mono", name.startsWith(root.assetId + ".") ? name.slice(root.assetId.length + 1) : name));
|
|
2471
|
+
const sample = cells.find((l) => !l.mirrorOfKey) || cells[0];
|
|
2472
|
+
const meta = [sample.character.mode, frameUrls(sample).length ? frameUrls(sample).length + " frames" : null, parent !== root ? "from " + parent.assetId.replace(root.assetId + ".", "") : null].filter(Boolean).join(" · ");
|
|
2473
|
+
th.append(el("small", null, meta));
|
|
2474
|
+
tr.append(th);
|
|
2475
|
+
for (const d of columns) {
|
|
2476
|
+
const td = el("td");
|
|
2477
|
+
column(d, td);
|
|
2478
|
+
const item = cells.find((l) => l.character.direction === d);
|
|
2479
|
+
td.append(item ? loopCell(item, parent, d) : el("span", "lg-none", "·"));
|
|
2480
|
+
tr.append(td);
|
|
2481
|
+
}
|
|
2482
|
+
tbody.append(tr);
|
|
2073
2483
|
}
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2484
|
+
table.append(tbody);
|
|
2485
|
+
const scroller = el("div", "lg-scroll");
|
|
2486
|
+
scroller.append(table);
|
|
2487
|
+
wrap.append(scroller);
|
|
2488
|
+
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."));
|
|
2489
|
+
return wrap;
|
|
2490
|
+
}
|
|
2491
|
+
function column(direction, node) {
|
|
2492
|
+
if (!F.columns.has(direction)) F.columns.set(direction, []);
|
|
2493
|
+
F.columns.get(direction).push(node);
|
|
2494
|
+
}
|
|
2495
|
+
function loopCell(item, parent, direction) {
|
|
2496
|
+
const tone = STATE_TONE[item.state] || "dim";
|
|
2497
|
+
const b = el("button", "lg-cell tone-" + tone);
|
|
2498
|
+
b.type = "button";
|
|
2499
|
+
b.title = item.key + " · " + item.state + (item.reason ? ": " + item.reason : "") + (item.mirrorOfKey ? " · mirror of " + item.mirrorOfKey : "");
|
|
2500
|
+
b.onclick = () => {
|
|
2501
|
+
closeFamily();
|
|
2502
|
+
openItem(item.id);
|
|
2079
2503
|
};
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2504
|
+
const img = el("img");
|
|
2505
|
+
img.alt = item.assetId;
|
|
2506
|
+
const urls = frameUrls(item);
|
|
2507
|
+
if (urls.length) {
|
|
2508
|
+
img.src = urls[0];
|
|
2509
|
+
for (const u of urls) {
|
|
2510
|
+
const p = new Image();
|
|
2511
|
+
p.src = u;
|
|
2512
|
+
}
|
|
2513
|
+
if (urls.length > 1) F.anims.push({ img, urls, fps: item.fps || 12, index: 0 });
|
|
2514
|
+
} else {
|
|
2515
|
+
const ghost = rotationUrl(parent, direction);
|
|
2516
|
+
if (ghost) {
|
|
2517
|
+
img.src = ghost;
|
|
2518
|
+
b.classList.add("ghost");
|
|
2519
|
+
} else img.hidden = true;
|
|
2086
2520
|
}
|
|
2087
|
-
|
|
2521
|
+
b.append(img);
|
|
2522
|
+
if (item.mirrorOfKey) b.append(el("span", "lg-mirror", "⇋"));
|
|
2523
|
+
if (item.state !== "ok") b.append(el("span", "lg-state", item.state));
|
|
2524
|
+
return b;
|
|
2088
2525
|
}
|
|
2089
|
-
function
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
const
|
|
2093
|
-
const
|
|
2094
|
-
const b = el("button",
|
|
2526
|
+
function extrasStrip(extras) {
|
|
2527
|
+
const wrap = el("section", "family-extras");
|
|
2528
|
+
wrap.append(el("h3", null, extras.some((e) => e.character.kind === "outfit") ? "Portraits and outfits" : extras.length === 1 ? "Portrait" : "Portraits"));
|
|
2529
|
+
const strip = el("div", "fx-strip");
|
|
2530
|
+
for (const item of extras) {
|
|
2531
|
+
const b = el("button", "lg-cell tone-" + (STATE_TONE[item.state] || "dim"));
|
|
2095
2532
|
b.type = "button";
|
|
2096
|
-
b.
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2533
|
+
b.title = item.key + " · " + item.state;
|
|
2534
|
+
b.onclick = () => {
|
|
2535
|
+
closeFamily();
|
|
2536
|
+
openItem(item.id);
|
|
2537
|
+
};
|
|
2538
|
+
const img = el("img");
|
|
2539
|
+
img.alt = item.assetId;
|
|
2540
|
+
const urls = frameUrls(item);
|
|
2541
|
+
if (urls.length) {
|
|
2542
|
+
img.src = urls[0];
|
|
2543
|
+
if (urls.length > 1) F.anims.push({ img, urls, fps: item.fps || 12, index: 0 });
|
|
2544
|
+
} else img.hidden = true;
|
|
2545
|
+
b.append(img);
|
|
2546
|
+
if (item.state !== "ok") b.append(el("span", "lg-state", item.state));
|
|
2547
|
+
const fig = el("figure");
|
|
2548
|
+
fig.append(b, el("figcaption", "mono", item.assetId));
|
|
2549
|
+
strip.append(fig);
|
|
2550
|
+
}
|
|
2551
|
+
wrap.append(strip);
|
|
2552
|
+
return wrap;
|
|
2553
|
+
}
|
|
2554
|
+
function highlight(direction) {
|
|
2555
|
+
if (!F) return;
|
|
2556
|
+
for (const [d, nodes] of F.columns) for (const n of nodes) n.classList.toggle("col-on", d === direction);
|
|
2557
|
+
}
|
|
2558
|
+
function tick(now) {
|
|
2559
|
+
if (!F) return;
|
|
2560
|
+
if (!F.panel || !F.panel.isConnected) {
|
|
2561
|
+
stopFamily();
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
if (F.playing) {
|
|
2565
|
+
const seconds = Math.max(0, (now - F.t0) / 1e3 * F.speed);
|
|
2566
|
+
for (const a of F.anims) {
|
|
2567
|
+
const i = Math.floor(seconds * a.fps) % a.urls.length;
|
|
2568
|
+
if (i !== a.index) {
|
|
2569
|
+
a.index = i;
|
|
2570
|
+
a.img.src = a.urls[i];
|
|
2571
|
+
}
|
|
2117
2572
|
}
|
|
2118
2573
|
}
|
|
2119
|
-
|
|
2574
|
+
if (F.spinning && F.turn && now - F.lastSpin >= SPIN_MS / F.speed) {
|
|
2575
|
+
F.lastSpin = now;
|
|
2576
|
+
const source = S.snap.items.find((i) => i.id === F.source);
|
|
2577
|
+
const order = source ? turnOrder(source) : COMPASS;
|
|
2578
|
+
F.turn(order[(order.indexOf(F.direction) + 1) % order.length]);
|
|
2579
|
+
}
|
|
2580
|
+
F.raf = requestAnimationFrame(tick);
|
|
2120
2581
|
}
|
|
2582
|
+
var familyState = () => F && { rootId: F.rootId, source: F.source, direction: F.direction, playing: F.playing, anims: F.anims.length };
|
|
2121
2583
|
|
|
2122
2584
|
// src/gallery/client/src/pose-editor.ts
|
|
2123
2585
|
var MIN_FRAMES = 3;
|
|
@@ -3023,6 +3485,15 @@
|
|
|
3023
3485
|
if (c.kind === "animation") row(dl, "direction", c.direction || "—");
|
|
3024
3486
|
else row(dl, "directions", String(c.directions));
|
|
3025
3487
|
if (c.characterId) row(dl, "character id", c.characterId, { mono: true, copy: c.characterId });
|
|
3488
|
+
const root = familyRoot(item);
|
|
3489
|
+
const size = root ? familyMembers(root).length : 0;
|
|
3490
|
+
if (root && size > 1) {
|
|
3491
|
+
const fam = el("button", "add", "Family view · " + size);
|
|
3492
|
+
fam.type = "button";
|
|
3493
|
+
fam.title = "Turn " + root.assetId + " through its rotations and play every loop by direction";
|
|
3494
|
+
fam.onclick = () => openFamily(item);
|
|
3495
|
+
row(dl, "family", fam);
|
|
3496
|
+
}
|
|
3026
3497
|
const family = S.snap.items.filter((i) => i.character && i.character.parentKey === item.key && i.project === item.project);
|
|
3027
3498
|
if (family.length) {
|
|
3028
3499
|
const list = el("div");
|
|
@@ -3300,6 +3771,7 @@
|
|
|
3300
3771
|
renderMain(visibleItems());
|
|
3301
3772
|
renderDrawer();
|
|
3302
3773
|
renderTray();
|
|
3774
|
+
refreshFamily();
|
|
3303
3775
|
}
|
|
3304
3776
|
function renderQuietly() {
|
|
3305
3777
|
const region = formRegion();
|
|
@@ -3308,6 +3780,7 @@
|
|
|
3308
3780
|
if (region !== "main") renderMain(visibleItems());
|
|
3309
3781
|
if (region !== "drawer") renderDrawer();
|
|
3310
3782
|
renderTray();
|
|
3783
|
+
refreshFamily();
|
|
3311
3784
|
}
|
|
3312
3785
|
|
|
3313
3786
|
// src/gallery/client/src/refresh.ts
|
|
@@ -3340,7 +3813,7 @@
|
|
|
3340
3813
|
];
|
|
3341
3814
|
var BODIES = ["mannequin", "bear", "cat", "dog", "horse", "lion"];
|
|
3342
3815
|
var VIEWS = ["low top-down", "high top-down", "side"];
|
|
3343
|
-
var
|
|
3816
|
+
var COMPASS2 = ["south", "south-east", "east", "north-east", "north", "north-west", "west", "south-west"];
|
|
3344
3817
|
var FOUR = ["south", "east", "north", "west"];
|
|
3345
3818
|
var PORTRAIT_SIZES = [16, 32, 48, 64, 128, 160];
|
|
3346
3819
|
var LOOP_MODES = [
|
|
@@ -3469,8 +3942,8 @@
|
|
|
3469
3942
|
const chosenEngine = () => ENGINES.find((e) => e.id === engine.value);
|
|
3470
3943
|
const isNewStyle = () => style.value === "";
|
|
3471
3944
|
const styleDirections = () => {
|
|
3472
|
-
if (isNewStyle()) return Number(directions.value) === 4 ? FOUR :
|
|
3473
|
-
return
|
|
3945
|
+
if (isNewStyle()) return Number(directions.value) === 4 ? FOUR : COMPASS2;
|
|
3946
|
+
return COMPASS2;
|
|
3474
3947
|
};
|
|
3475
3948
|
function fillStyles() {
|
|
3476
3949
|
const keep = style.value;
|
|
@@ -3529,7 +4002,7 @@
|
|
|
3529
4002
|
what.value = defaults.what ?? "";
|
|
3530
4003
|
const dirs = el("div", "studio-dirs");
|
|
3531
4004
|
const boxes = /* @__PURE__ */ new Map();
|
|
3532
|
-
for (const d of
|
|
4005
|
+
for (const d of COMPASS2) {
|
|
3533
4006
|
const box = el("input");
|
|
3534
4007
|
box.type = "checkbox";
|
|
3535
4008
|
box.checked = (defaults.directions ?? ["south", "west", "north"]).includes(d);
|
|
@@ -3859,5 +4332,5 @@
|
|
|
3859
4332
|
if (GENERATION) pollJobs();
|
|
3860
4333
|
if (EDITOR) pollEditor();
|
|
3861
4334
|
if (ui.compare.length >= 2 && !ui.open) openCompare();
|
|
3862
|
-
window.__pixelkiln = { S, ui };
|
|
4335
|
+
window.__pixelkiln = { S, ui, family: familyState };
|
|
3863
4336
|
})();
|