Files
DumiFlex 98929f3eb5 feat(docs): node pictures from real runs with numbered pins and a legend (#259)
Every node page opens with a picture shot from a real run (scripts/docs-shots), with pins on its sockets and controls placed from the page and a legend drawn from the node's field list (DocAnnotated).
2026-11-03 22:37:07 +01:00

353 lines
13 KiB
JavaScript

/**
* The docs node pictures (shoot.mjs). Each scene:
* image file name under public/images/docs/ (no .png)
* node the WP node the pins are for (its fields in node-fields.ts)
* from example workflow to start from: "one" | "grids" | "pick" (else empty)
* build `async (S, arg) => id`, run IN THE PAGE from its source text, so
* it can't use anything from this file: S is window.__shot
* (page-helper.js), arg is the scene's `arg`. Returns the pinned
* node's id.
* run queue the graph before shooting
* height picture height in px (644 / 752 / 860)
* groups node-fields groups to pin (default ports + controls)
*/
const TOGGLES = {
version: 1,
mode: "any",
group_var: "",
rows: [
{ id: "a1000001", kind: "label", name: "Render", color: 5 },
{ id: "a1000002", kind: "toggle", name: "Hires fix", var: "hires", on: true, pin: true, out: 1, style: "switch" },
{ id: "a1000003", kind: "toggle", name: "Quick preview", var: "quick_preview", on: false, pin: true, out: 2, style: "check" },
{ id: "a1000005", kind: "toggle", name: "Detailer", var: "detailer", on: false, pin: false, out: 0, style: "switch", control: "mute" },
],
};
export const SCENES = [
{
image: "wp-toggles",
node: "WP_Toggles",
height: 860,
run: true,
arg: TOGGLES,
build: async (S, cfg) => {
const t = S.w(S.add("WP_Toggles"), 380);
S.set(t, "wp_toggles", JSON.stringify(cfg));
await S.wait(600);
const det = S.w(S.add("PrimitiveString", { title: "Detailer prompt", widgets: { value: "detailed face" } }), 260);
const on = S.w(S.add("PrimitiveString", { title: "Hires on", widgets: { value: "upscaled 2x, then refined" } }), 250);
const off = S.w(S.add("PrimitiveString", { title: "Hires off", widgets: { value: "base size only" } }), 250);
const sw = S.w(S.add("ComfySwitchNode"), 220);
const p1 = S.w(S.add("PreviewAny", { title: "Preview as Text" }), 250);
const p2 = S.w(S.add("PreviewAny", { title: "Preview as Text" }), 250);
const p3 = S.w(S.add("PreviewAny", { title: "Preview as Text" }), 250);
S.link(on, "STRING", sw, "on_true");
S.link(off, "STRING", sw, "on_false");
S.link(t, "toggle_1", sw, "switch");
S.link(sw, "output", p1, "source");
S.link(t, "toggle_1", p2, "source");
S.link(t, "toggle_2", p3, "source");
S.link(det, "STRING", t, "ctl_a1000005");
await S.columns([[{ gap: 150 }, det], [{ gap: 40 }, t], [on, off, p2], [sw, p1, p3]]);
return t.id;
},
},
{
image: "wp-context",
node: "WP_Context",
from: "one",
groups: ["ports", "markers"],
build: async (S) => {
S.only([3, 5]);
S.w(3, 440);
S.w(5, 440);
await S.columns([[3], [5]]);
return 3;
},
},
{
image: "wp-context-injector",
node: "WP_ContextInjector",
from: "one",
run: true,
groups: ["ports", "rowOptions"],
build: async (S) => {
S.only([5, 13, 14, 15, 16, 10, 3]);
S.link(16, "context", 10, "context");
S.fold(3, 5);
S.w(13, 320); S.w(14, 320); S.w(15, 320); S.w(16, 460); S.w(10, 440);
await S.columns([[3, 5, 13, 14, 15], [{ gap: 60 }, 16], [{ gap: 60 }, 10]], { gapY: 20 });
return 16;
},
},
{
image: "wp-context-loop-graph",
node: "WP_ContextLoop",
from: "grids",
run: true,
height: 860,
build: async (S) => {
S.only([3, 5, 6, 8]);
const p = S.w(S.add("PreviewAny", { title: "Preview as Text" }), 320);
S.link(8, "prompt", p, "source");
S.fold(5, 6);
S.w(3, 420); S.w(8, 400);
await S.columns([[3], [5, 6, 8], [{ gap: 130 }, p]], { gapY: 20 });
return 3;
},
},
{
image: "wp-seed-list",
node: "WP_SeedList",
from: "grids",
run: true,
build: async (S) => {
S.only([3, 28]);
S.mode(28, 0);
const p = S.w(S.add("PreviewAny", { title: "Preview as Text" }), 300);
S.link(28, "seed", p, "source");
S.w(3, 420); S.w(28, 400);
await S.columns([[3], [28], [p]]);
return 28;
},
},
{
image: "wp-debug-viewer",
node: "WP_Debug",
from: "one",
run: true,
height: 860,
groups: ["ports", "tabs"],
build: async (S) => {
S.only([3, 5, 10]);
S.link(5, "context", 10, "context");
S.fold(3);
S.w(5, 440); S.w(10, 560);
await S.columns([[3, 5], [{ gap: 56 }, 10]], { gapY: 20 });
return 10;
},
},
{
image: "wp-prompt-assembler",
node: "WP_PromptAssembler",
from: "one",
run: true,
height: 860,
build: async (S) => {
S.only([3, 5, 7]);
S.link(5, "context", 7, "context");
const p1 = S.w(S.add("PreviewAny", { title: "Preview as Text · prompt" }), 360);
const p2 = S.w(S.add("PreviewAny", { title: "Preview as Text · negative" }), 360);
S.link(7, "prompt", p1, "source");
S.link(7, "negative", p2, "source");
S.fold(3, 5);
S.w(7, 440);
await S.columns([[3, 5], [7], [p1, p2]], { gapY: 20 });
return 7;
},
},
{
image: "wp-prompt-cleaner",
node: "WP_PromptCleaner",
from: "one",
run: true,
height: 860,
build: async (S) => {
S.only([8]);
const a = S.w(S.add("PrimitiveStringMultiline", { title: "Prompt", widgets: { value: "masterpiece, best quality, , 1girl, red hair, Red Hair, ((smile)), (, ), solo,, < lora : style : 0.8 >" } }), 340);
const b = S.w(S.add("PrimitiveStringMultiline", { title: "Negative", widgets: { value: "lowres, bad hands, , lowres, red hair" } }), 340);
const p1 = S.w(S.add("PreviewAny", { title: "Preview as Text · prompt" }), 340);
const p2 = S.w(S.add("PreviewAny", { title: "Preview as Text · negative" }), 340);
S.link(a, "STRING", 8, "prompt");
S.link(b, "STRING", 8, "negative");
S.link(8, "prompt", p1, "source");
S.link(8, "negative", p2, "source");
S.w(8, 440);
await S.columns([[a, b], [8], [p1, p2]]);
return 8;
},
},
];
/** Var → Int / Float / Choice: the example's Render Context, its model family
* given by a text into WP Model Info (no checkpoint needed), then the node. */
for (const [image, node, id, into, port, height] of [
["wp-var-to-int", "WP_VarToInt", 33, 25, "value", 644],
["wp-var-to-float", "WP_VarToFloat", 35, 26, "value", 752],
["wp-var-to-choice", "WP_VarToChoice", 36, 26, "choice", 752],
]) {
SCENES.push({
image,
node,
from: "one",
run: true,
height,
arg: { id, into, port },
build: async (S, a) => {
S.only([31, 32, a.id, a.into]);
const fam = S.w(S.add("PrimitiveString", { title: "Model family", widgets: { value: "sdxl" } }), 240);
S.link(fam, "STRING", 31, "family");
const p = S.w(S.add("PreviewAny", { title: "Preview as Text" }), 300);
S.link(a.id, a.port, p, "source");
S.fold(fam, 31, 32);
S.w(a.id, 380); S.w(a.into, 300);
await S.columns([[fam, 31, 32], [a.id], [a.into, p]], { gapY: 18 });
return a.id;
},
});
}
SCENES.push(
{
image: "wp-var-to-bool",
node: "WP_VarToBool",
from: "grids",
run: true,
height: 752,
build: async (S) => {
S.only([12, 14, 15, 16, 17]);
const p = S.w(S.add("PreviewAny", { title: "Preview as Text" }), 260);
S.link(14, "value", p, "source");
S.w(12, 380); S.w(14, 380); S.w(15, 260); S.w(16, 260); S.w(17, 240);
await S.columns([[12], [14, 15], [p, 16, 17]], { gapY: 24 });
return 14;
},
},
{
image: "wp-model-info",
node: "WP_ModelInfo",
from: "one",
run: true,
build: async (S) => {
// No checkpoint in the shooting ComfyUI: the family comes in as text.
S.only([31, 32, 33]);
const fam = S.w(S.add("PrimitiveString", { title: "Model family", widgets: { value: "sdxl" } }), 240);
S.link(fam, "STRING", 31, "family");
const file = S.w(S.add("PrimitiveString", { title: "Checkpoint file", widgets: { value: "ponyDiffusionV6XL.safetensors" } }), 300);
S.link(file, "STRING", 31, "name");
const p = S.w(S.add("PreviewAny", { title: "Preview as Text" }), 240);
S.link(33, "value", p, "source");
S.w(31, 440);
S.fold(32, 33, p);
await S.columns([[{ gap: 40 }, fam, file], [31], [{ gap: 20 }, 32, 33, p]], { gapY: 18 });
return 31;
},
},
{
image: "wp-node-group",
node: "WP_NodeGroup",
height: 752,
arg: {
version: 1, mode: "any", group_var: "",
rows: [
{ id: "b2000001", kind: "toggle", name: "Detailer", var: "detailer", on: false, pin: false, out: 0, style: "buttons", control: "mute" },
{ id: "b2000002", kind: "toggle", name: "Upscale", var: "upscale", on: true, pin: false, out: 0, style: "switch" },
],
},
build: async (S, cfg) => {
const dec = S.w(S.add("VAEDecode", { title: "Detailer decode" }), 260);
const up = S.w(S.add("ImageScaleBy", { title: "Detail upscale" }), 320);
const g = S.w(S.add("WP_NodeGroup"), 420);
await S.wait(400);
S.link(dec, "IMAGE", g, "node_1");
await S.wait(400);
S.link(up, "IMAGE", g, "node_2");
const t = S.w(S.add("WP_Toggles"), 400);
S.set(t, "wp_toggles", JSON.stringify(cfg));
await S.wait(600);
S.link(g, "nodes", t, "ctl_b2000001");
await S.wait(600);
await S.columns([[dec, up], [g], [t]]);
return g.id;
},
},
{
image: "wp-mode-relay",
node: "WP_ModeRelay",
from: "one",
build: async (S) => {
S.only([22, 31, 32, 33, 34, 37, 38]);
S.mode(31, 2);
S.fold(22, 31, 32, 33, 34);
S.w(38, 380); S.w(37, 400);
await S.columns([[22, 31, { gap: 40 }, 32, 33, 34], [38], [37]], { gapY: 18 });
return 38;
},
},
{
image: "wp-label-grid",
node: "WP_LabelGrid",
from: "grids",
run: true,
height: 860,
groups: ["ports", "label", "grid"],
build: async (S) => {
S.only([3, 5, 6, 8, 10, 25]);
const img = S.w(S.add("DocsDemoLandscape", { title: "Your images (stand-in for the sampler)", widgets: { width: 384, height: 384 } }), 300);
S.link(8, "prompt", img, "text");
S.link(img, "IMAGE", 25, "images");
const prev = S.w(S.add("PreviewImage"), 420);
S.link(25, "grid", prev, "images");
S.fold(3, 5, 6, 8, 10, prev);
S.w(25, 460);
await S.columns([[3, 5, 6, 8, 10, { gap: 20 }, img, prev], [25]], { gapY: 16 });
return 25;
},
},
{
image: "wp-image-filter",
node: "WP_ImageFilter",
from: "pick",
run: true,
height: 860,
groups: ["ports", "settings"],
answer: { action: "picks", picks: [[0, 0], [1, 1], [3, 0]] },
build: async (S) => {
S.only([3, 4, 5, 6, 7, 17]);
const img = S.w(S.add("DocsDemoLandscape", { title: "Your images (stand-in for the sampler)", widgets: { width: 384, height: 384, count: 2 } }), 300);
S.link(7, "prompt", img, "text");
S.link(img, "IMAGE", 17, "images");
const prev = S.w(S.add("PreviewImage"), 380);
S.link(17, "images", prev, "images");
S.fold(3, 4, 5, 6, 7);
S.w(17, 460);
await S.columns([[3, 4, 5, 6, 7, { gap: 20 }, img], [17], [prev]], { gapY: 16 });
return 17;
},
},
{
image: "wp-note",
height: 752,
arg: [
{ title: "Load your model", cfg: { tone: "step", look: "card", step: 1, icon: "info-circle", color: "", text: "Pick the **checkpoint** in the loader. `$model_family` follows it, so the prompt fits the model.\n\n[WP Model Info](docs:wp-model-info){button pi-microchip}\n[Writing prompts for your model](docs:howto-model-prompts){button pi-file}" } },
{ title: "Lock before you compare", cfg: { tone: "icon", look: "card", step: 1, icon: "exclamation-triangle", color: "warning", text: "Lock the seed :pi-lock:{#f59e0b} before you compare two settings, or each run rolls new picks." } },
{ title: "Tip", cfg: { tone: "icon", look: "sticky", step: 1, icon: "lightbulb", color: "tip", text: "Right-click a wildcard row and pick **Test** to see 20 rolls at once." } },
{ title: "Label each frame", cfg: { tone: "step", look: "label", step: 2, icon: "info-circle", color: "", text: "A second **Assembler** writes `$outfit · $time` per frame; the Label & Grid reads them as rows and columns.\n\n[The grid](docs:wp-label-grid#s-the-grid){button pi-th-large}" } },
],
build: async (S, notes) => {
const made = notes.map(({ title, cfg }) => {
const n = S.w(S.add("WP_Note", { title }), 420);
S.set(n, "wp_note", JSON.stringify({ version: 1, bookmark: null, ...cfg }));
return n;
});
await S.columns([[made[0], made[3]], [made[1], made[2]]], { gapY: 40 });
return made[0].id;
},
},
{
image: "wp-bookmark",
node: "WP_Bookmark",
from: "one",
height: 752,
edit: true,
scale: 0.85,
build: async (S) => {
S.only([26, 28]);
S.w(26, 320);
await S.columns([[26], [28]]);
return 28;
},
},
);