Files
DemonGatanjieu-Anomalous_Mo…/web/modules/canvas_roles.js
T
DemonGatanjieuandClaude Opus 5.5 eab141f971 Merge the upscale ways into three; repaint is one use for the AI
Upscale held by Tile takes in the tile upscale, the Anima Tile detail
upscale and the second-model upscale: its redraw has its own copy of the
canvas's model line (copy_of "$sampler.model": loader and LoRAs, titled
"Upscale model"), so a second model is one set_model and the LoRAs stay;
on Anima it is the user's tested route (latent 1.22x, Tile LLLite,
denoise 0.85). The iterative upscale goes. add_nodes no longer lists every
block name, and the repaint hints name the use, not a block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 02:32:09 +08:00

172 lines
9.6 KiB
JavaScript

/**
* Where a ready-made block (api/reference_blocks.py) goes on the canvas: the roles its links name,
* found on the graph by their sockets, never by title or position. "$sampler" is the sampler
* (model, positive, negative and latent_image in), "$decode" the node decoding its latent,
* "$save" the output node that decode's picture reaches (past a stitch put in between). In a link,
* ["$decode", 0] is that node's own output and ["$sampler.positive", "*"] whatever feeds that
* input, a longer path ("$sampler.positive.clip") followed upstream one input at a time; wire_to
* may be a role too. A node of the block "like" a role takes that node's settings of the same
* name (a second sampler with the first one's steps and cfg); one that "replaces" a role also
* takes its links in and out, and the old node goes (LanPaint's sampler in the KSampler's place).
* "$loader" is the node the sampler's model comes from, past its LoRAs (a second model loader "like" it).
* A node "copy_of" a path ("$sampler.model") is a copy of the model line feeding it: its loader and
* LoRAs as new nodes with their settings, past bypassed nodes and control patches (a Tile on the first
* pass is not the second's); the block's key is the copy's last node, its loader titled as the node is
* (a redraw's own model, which the AI swaps for a second one with set_model, the LoRAs kept).
* Several samplers: near_node_id names the one, else nothing changes and they are listed.
*/
import { linkOf } from './mcp_canvas_brief.js';
const SAMPLER_INPUTS = ['model', 'positive', 'negative', 'latent_image'];
const OFF = new Set([2, 4]); // muted, bypassed
const CONTROL = /lllite|controlnet|t2iadapter|ipadapter/i;
const upper = type => String(type ?? '').toUpperCase();
const inputNamed = (node, name) => (node?.inputs || []).find(item => String(item.name).toLowerCase() === name.toLowerCase());
const label = node => `#${node.id} ${node.title || node.type}`;
function takers(g, node, slot) {
return ((node.outputs || [])[slot]?.links || []).map(id => linkOf(g, id)).filter(Boolean)
.map(link => g.getNodeById(link.target_id)).filter(item => item && !OFF.has(item.mode));
}
const isOutput = node => node.constructor?.nodeData?.output_node || !(node.outputs || []).length;
/** The output node the picture at `node`'s `slot` reaches, through nodes passing a picture on. */
function outputOf(g, node, slot, seen = new Set()) {
for (const next of takers(g, node, slot)) {
if (seen.has(next)) continue;
seen.add(next);
if (isOutput(next)) return next;
const picture = (next.outputs || []).findIndex(item => upper(item.type) === 'IMAGE');
const found = picture < 0 ? null : outputOf(g, next, picture, seen);
if (found) return found;
}
return null;
}
/** A node's settings by name ({steps: 30, cfg: 5}), buttons left out. */
function settingsOf(node) {
return Object.fromEntries((node.widgets || []).filter(item => item?.name && item.type !== 'button').map(item => [item.name, item.value]));
}
/** The node `node`'s model comes from, through the nodes passing a MODEL on (LoRAs): the loader. */
function loaderOf(g, node) {
let at = node;
for (let hop = 0; hop < 30; hop++) {
const id = inputNamed(at, 'model')?.link;
const link = id == null ? null : linkOf(g, id);
const from = link && g.getNodeById(link.origin_id);
if (!from) break;
at = from;
}
return at === node ? null : at;
}
/** The roles on graph `g`: {sampler, decode, save, loader}; decode, save and loader may be missing. */
export function findRoles(g, nearId = null) {
const samplers = (g._nodes || []).filter(node => !OFF.has(node.mode) && SAMPLER_INPUTS.every(name => inputNamed(node, name)));
let sampler = nearId == null ? null : samplers.find(node => String(node.id) === String(nearId));
if (!sampler) {
if (samplers.length !== 1) {
throw new Error(samplers.length
? `Several samplers on the canvas (${samplers.map(label).join(', ')}): near_node_id names the one this block goes with. Nothing added.`
: 'No sampler (KSampler) on the canvas: open a workflow that makes the picture first (open_workflow). Nothing added.');
}
[sampler] = samplers;
}
const latent = (sampler.outputs || []).findIndex(item => upper(item.type) === 'LATENT');
const decode = takers(g, sampler, latent).find(node => inputNamed(node, 'samples')
&& (node.outputs || []).some(item => upper(item.type) === 'IMAGE')) || null;
const picture = decode ? decode.outputs.findIndex(item => upper(item.type) === 'IMAGE') : -1;
const save = decode ? outputOf(g, decode, picture) || takers(g, decode, picture)[0] || null : null;
return { sampler, decode, save, loader: loaderOf(g, sampler) };
}
function upstream(g, node, path, ref) {
let at = node, slot = null;
for (const name of path) {
const id = inputNamed(at, name)?.link;
const link = id == null ? null : linkOf(g, id);
if (!link) throw new Error(`${ref}: nothing is wired into ${label(at)}'s ${name}. Nothing added.`);
at = g.getNodeById(link.origin_id);
slot = link.origin_slot;
}
return [`#${at.id}`, slot];
}
/** Group `nodes` (add_nodes' API form) with its roles resolved on graph `g`; a group naming none comes back as it is. */
export function placeRoles(g, nodes, nearId = null) {
if (!JSON.stringify(nodes).includes('"$')) return nodes;
const roles = findRoles(g, nearId);
const role = (name, ref) => {
const node = roles[name];
const what = { decode: 'VAE Decode', save: 'Save Image', loader: 'model loader' }[name] || name;
if (!node) throw new Error(`${ref}: no ${name} on the canvas (the sampler's ${what}). Nothing added.`);
return node;
};
const link = (value) => {
if (!Array.isArray(value) || typeof value[0] !== 'string' || !value[0].startsWith('$')) return value;
const [name, ...path] = value[0].slice(1).split('.');
return path.length ? upstream(g, role(name, value[0]), path, value[0]) : [`#${role(name, value[0]).id}`, value[1]];
};
const target = value => (typeof value === 'string' && value.startsWith('$') ? role(value.slice(1), value).id : value);
// "like" a path ("$sampler.positive"): the node feeding it (a second prompt with the first one's text).
const nodeAt = (ref) => {
const [name, ...path] = ref.slice(1).split('.');
return path.length ? g.getNodeById(Number(upstream(g, role(name, ref), path, ref)[0].slice(1))) : role(name, ref);
};
const gone = new Set(Object.values(nodes).filter(spec => spec.replaces).map(spec => role(spec.replaces.slice(1), spec.replaces)));
return Object.fromEntries(Object.entries(nodes).flatMap(([key, { like, replaces, copy_of: copyOf, ...spec }]) => {
if (copyOf) return modelCopy(g, key, nodeAt(copyOf), spec._meta?.title, copyOf);
const inputs = Object.fromEntries(Object.entries(spec.inputs || {}).map(([name, value]) => [name, link(value)]));
const placed = { ...spec, inputs,
...(spec.wire_to == null ? {} : { wire_to: Array.isArray(spec.wire_to) ? spec.wire_to.map(target) : target(spec.wire_to) }) };
const model = like || replaces ? nodeAt(like || replaces) : null;
if (model) placed.carry = settingsOf(model);
if (replaces) Object.assign(placed, takenOver(g, model, inputs, gone));
return [[key, placed]];
}));
}
/** The model line ending at `last` as group entries [[key, spec]], loader first, the last one under `key`. */
function modelCopy(g, key, last, title, ref) {
const line = [];
for (let at = last, hop = 0; at && hop < 30; hop++) {
if (!OFF.has(at.mode) && !CONTROL.test(at.type)) line.unshift(at);
const id = inputNamed(at, 'model')?.link;
const link = id == null ? null : linkOf(g, id);
at = link ? g.getNodeById(link.origin_id) : null;
}
if (!line.length || inputNamed(line[0], 'model')) throw new Error(`${ref}: no model loader feeds it on the canvas. Nothing added.`);
const keys = line.map((node, at) => (at === line.length - 1 ? key : `${key}_${at + 1}`));
return line.map((node, at) => {
const inputs = {};
for (const input of node.inputs || []) {
const link = input.link == null ? null : linkOf(g, input.link);
if (String(input.name).toLowerCase() === 'model') inputs[input.name] = [keys[at - 1], 0];
else if (link) inputs[input.name] = [`#${link.origin_id}`, link.origin_slot];
}
return [keys[at], { class_type: node.type, inputs, carry: settingsOf(node), ...(at === 0 && title ? { _meta: { title } } : {}) }];
});
}
/** What a node replacing `old` takes from it: the links into it the block does not give (`kept`,
* wired where the new node has that input), where its outputs went (`feeds`), and its id to remove. */
function takenOver(g, old, inputs, gone) {
const kept = {}, feeds = [];
for (const input of old.inputs || []) {
const link = input.link == null ? null : linkOf(g, input.link);
const from = link && g.getNodeById(link.origin_id);
if (from && !gone.has(from) && !Object.keys(inputs).some(name => name.toLowerCase() === String(input.name).toLowerCase())) {
kept[input.name] = [`#${from.id}`, link.origin_slot];
}
}
(old.outputs || []).forEach((output, slot) => (output.links || []).map(id => linkOf(g, id)).filter(Boolean).forEach(link => {
const to = g.getNodeById(link.target_id);
if (to && !gone.has(to)) feeds.push({ output: output.name, to: to.id, input: to.inputs[link.target_slot].name });
}));
return { kept, feeds, replaces: old.id };
}