717 lines
26 KiB
JavaScript
717 lines
26 KiB
JavaScript
/**
|
|
* DaSiWa Node Status Switch - ComfyUI frontend extension
|
|
*
|
|
* Minimal JS responsibilities:
|
|
* 1. Manage dynamic target_XX inputs (grow on connect, up to 20)
|
|
* 2. At queue time, read the effective "enabled" value (from the
|
|
* local widget OR from a connected upstream node) and set the
|
|
* mode of every target node accordingly.
|
|
*
|
|
* No live updates. No widget callbacks. No polling.
|
|
* The mode change happens right before the prompt is sent.
|
|
*
|
|
* ComfyUI mode values:
|
|
* 0 = ALWAYS (active)
|
|
* 2 = NEVER (mute)
|
|
* 4 = bypass
|
|
*/
|
|
|
|
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
|
|
const MODE_ACTIVE = 0;
|
|
const MODE_MUTE = 2;
|
|
const MODE_BYPASS = 4;
|
|
const NODE_TYPE = "DaSiWa_NodeStatusSwitch";
|
|
const DIRECTOR_TYPE = "MiniMaxH3Director";
|
|
const TARGET_PREFIX = "target_";
|
|
const MAX_TARGETS = 99;
|
|
|
|
// Toggle in browser console: window.DASIWA_SWITCH_DEBUG = true
|
|
function dlog(...args) {
|
|
if (typeof window !== "undefined" && window.DASIWA_SWITCH_DEBUG) {
|
|
console.log("[DaSiWa Switch]", ...args);
|
|
}
|
|
}
|
|
|
|
// ── helpers ─────────────────────────────────────────────────────────────
|
|
|
|
function getWidget(node, name) {
|
|
return (node.widgets ?? []).find((w) => w.name === name);
|
|
}
|
|
|
|
function isTargetSlot(input) {
|
|
return typeof input?.name === "string" &&
|
|
input.name.startsWith(TARGET_PREFIX);
|
|
}
|
|
|
|
function targetSlotName(n) {
|
|
return `${TARGET_PREFIX}${String(n).padStart(2, "0")}`;
|
|
}
|
|
|
|
function countTargetSlots(node) {
|
|
return (node.inputs ?? []).filter(isTargetSlot).length;
|
|
}
|
|
|
|
function isBoolWidget(w) {
|
|
if (!w) return false;
|
|
if (w.type === "toggle") return true;
|
|
if (typeof w.type === "string" && w.type.toUpperCase().includes("BOOLEAN")) return true;
|
|
if (typeof w.value === "boolean") return true;
|
|
return false;
|
|
}
|
|
|
|
function findBoolWidget(node) {
|
|
return node?.widgets?.find(isBoolWidget) ?? null;
|
|
}
|
|
|
|
function asBoolean(value) {
|
|
if (typeof value === "boolean") return value;
|
|
if (value === "true") return true;
|
|
if (value === "false") return false;
|
|
return null;
|
|
}
|
|
|
|
function nodesInGraphTree(graph, visited = new Set()) {
|
|
if (!graph || visited.has(graph)) return [];
|
|
visited.add(graph);
|
|
|
|
const nodes = graph._nodes ?? graph.nodes ?? [];
|
|
return nodes.flatMap((node) => [node, ...nodesInGraphTree(node.subgraph, visited)]);
|
|
}
|
|
|
|
/**
|
|
* In the current frontend, a promoted widget is store-backed by the enclosing
|
|
* subgraph host. The original widget inside the subgraph is no longer the
|
|
* source of truth, so resolve its host widget before reading the local value.
|
|
*/
|
|
function readPromotedWidgetValue(node, widgetName) {
|
|
const childGraph = node?.graph;
|
|
const rootGraph = childGraph?.rootGraph ?? app.graph;
|
|
if (!childGraph || !rootGraph || childGraph === rootGraph) return null;
|
|
|
|
const widgetInput = (node.inputs ?? []).find(
|
|
(input) => input?.widget?.name === widgetName || input?.name === widgetName
|
|
);
|
|
for (const host of nodesInGraphTree(rootGraph)) {
|
|
if (host?.subgraph !== childGraph) continue;
|
|
for (const hostInput of host.inputs ?? []) {
|
|
const slot = hostInput?._subgraphSlot;
|
|
for (const linkId of slot?.linkIds ?? []) {
|
|
const link = childGraph.getLink?.(linkId) ??
|
|
childGraph.links?.[linkId] ?? childGraph._links?.get?.(linkId);
|
|
const resolved = link?.resolve?.(childGraph);
|
|
if (resolved?.inputNode !== node) continue;
|
|
if (widgetInput && resolved.input !== widgetInput) continue;
|
|
|
|
// A promoted widget can itself be linked on the subgraph
|
|
// host. In that case the outer connection, not the host's
|
|
// store-backed widget value, is authoritative.
|
|
const parentGraph = host.graph ?? rootGraph;
|
|
if (hostInput.link != null && parentGraph) {
|
|
const outerLink = parentGraph.getLink?.(hostInput.link) ??
|
|
parentGraph.links?.[hostInput.link] ??
|
|
parentGraph._links?.get?.(hostInput.link);
|
|
const parentNodes = parentGraph._nodes ?? parentGraph.nodes ?? [];
|
|
const source = resolveSourceNode(
|
|
parentNodes.find((candidate) => candidate.id === outerLink?.origin_id),
|
|
parentGraph
|
|
);
|
|
const linkedValue = readBoolFromNode(source);
|
|
if (linkedValue != null) return linkedValue;
|
|
}
|
|
|
|
const hostWidget = host.getWidgetFromSlot?.(hostInput) ?? hostInput.widget;
|
|
const value = asBoolean(hostWidget?.value);
|
|
if (value != null) return value;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Read a boolean value from a node, regardless of how the node
|
|
* stores it. Tries every plausible storage path. Returns the
|
|
* boolean or null if nothing usable is found.
|
|
*/
|
|
function readBoolFromNode(node) {
|
|
if (!node) return null;
|
|
|
|
// ── Path 1: conventional widget ─────────────────────────────────────
|
|
const w = findBoolWidget(node);
|
|
const widgetValue = asBoolean(w?.value);
|
|
if (widgetValue != null) return widgetValue;
|
|
|
|
// ── Path 2: any widget whose value is boolean (broader than Path 1) ──
|
|
if (node.widgets) {
|
|
for (const aw of node.widgets) {
|
|
const value = asBoolean(aw?.value);
|
|
if (value != null) return value;
|
|
}
|
|
}
|
|
|
|
// ── Path 3: input slots may carry a widget object with the value ───
|
|
// PrimitiveBoolean (and similar widget-input hybrids) put the widget
|
|
// reference under inputs[i].widget; the value can live on the widget,
|
|
// on the input slot itself, or in widgets_values keyed by widget name.
|
|
if (node.inputs) {
|
|
for (const inp of node.inputs) {
|
|
const widgetValue = asBoolean(inp?.widget?.value);
|
|
if (widgetValue != null) return widgetValue;
|
|
const inputValue = asBoolean(inp?.value);
|
|
if (inputValue != null) return inputValue;
|
|
}
|
|
}
|
|
|
|
// ── Path 4: top-level widgets_values array ──────────────────────────
|
|
if (Array.isArray(node.widgets_values)) {
|
|
// First boolean entry
|
|
for (const v of node.widgets_values) {
|
|
if (typeof v === "boolean") return v;
|
|
}
|
|
// First entry coerced (some versions store "true"/"false" strings)
|
|
const first = node.widgets_values[0];
|
|
if (first === "true" || first === true) return true;
|
|
if (first === "false" || first === false) return false;
|
|
}
|
|
|
|
// ── Path 5: properties may hold the value ───────────────────────────
|
|
if (node.properties) {
|
|
for (const v of Object.values(node.properties)) {
|
|
if (typeof v === "boolean") return v;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Resolves the true source node by traversing through Reroute nodes.
|
|
*/
|
|
function resolveSourceNode(startNode, graph) {
|
|
let current = startNode;
|
|
const visited = new Set();
|
|
while (current?.type?.includes?.("Reroute") && !visited.has(current.id)) {
|
|
visited.add(current.id);
|
|
const linkId = current.inputs?.[0]?.link;
|
|
const link = linkId != null ? (graph.links?.[linkId] ?? graph._links?.get?.(linkId)) : null;
|
|
if (!link) break;
|
|
const allNodes = graph._nodes ?? graph.nodes ?? [];
|
|
current = allNodes.find((n) => n.id === link.origin_id);
|
|
}
|
|
return current;
|
|
}
|
|
|
|
// Modes that make the Director's fl2va_requested output true — mirrors
|
|
// `mode in BASE_MODES or mode == "Image Inpaint"` in
|
|
// nodes/nodes_minimax_h3_director.py.
|
|
const FL2VA_FAMILY_MODES = ["T2VA", "I2VA", "FL2VA", "L2VA", "Image Inpaint"];
|
|
|
|
/**
|
|
* Read the computed "request" boolean outputs of the MiniMax H3 Director.
|
|
*
|
|
* The Director's fl2va_requested / inpaint_requested outputs are computed
|
|
* in the backend from the mode combo (build_guide); the frontend node never
|
|
* stores them. A switch wired to one of these outputs therefore derives the
|
|
* value from the Director's mode widget instead.
|
|
*
|
|
* Returns a boolean, or null if the slot is not a known request output.
|
|
*/
|
|
function readDirectorRequest(director, slotIndex) {
|
|
if (!director) return null;
|
|
const name = director.outputs?.[slotIndex]?.name;
|
|
const mode = getWidget(director, "mode")?.value;
|
|
if (mode == null) return null;
|
|
switch (name) {
|
|
case "inpaint_requested":
|
|
return mode === "Image Inpaint";
|
|
case "fl2va_requested":
|
|
return FL2VA_FAMILY_MODES.includes(mode);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read the effective "enabled" value at queue time.
|
|
* If the "enabled" input is connected, follow the link to the source
|
|
* node and read its boolean value. Otherwise fall back to the local
|
|
* widget on the switch.
|
|
*
|
|
* Chaining: if the upstream node is another DaSiWa_NodeStatusSwitch,
|
|
* recursively read its effective enabled value (which is what the
|
|
* upstream switch outputs from its enabled_out pin). A `visited`
|
|
* set guards against accidental cycles in user graphs.
|
|
*/
|
|
function readEnabled(switchNode, visited) {
|
|
visited = visited ?? new Set();
|
|
if (visited.has(switchNode.id)) return null; // cycle guard
|
|
visited.add(switchNode.id);
|
|
|
|
const graph = switchNode.graph ?? app.graph;
|
|
|
|
const enabledInput = (switchNode.inputs ?? []).find(
|
|
(inp) => inp?.name === "enabled"
|
|
);
|
|
|
|
if (enabledInput && enabledInput.link != null && graph) {
|
|
const link =
|
|
graph.links?.[enabledInput.link] ??
|
|
graph._links?.get?.(enabledInput.link);
|
|
if (link) {
|
|
const allNodes = graph._nodes ?? graph.nodes ?? [];
|
|
const src = resolveSourceNode(allNodes.find((n) => n.id === link.origin_id), graph);
|
|
if (src) {
|
|
// If upstream is another switch, use its effective value,
|
|
// not its raw widget — a chained switch's output is what
|
|
// matters for downstream chaining.
|
|
if (src.type === NODE_TYPE) {
|
|
const v = readEnabled(src, visited);
|
|
if (v != null) return v;
|
|
}
|
|
// The MiniMax H3 Director's request outputs (inpaint_requested,
|
|
// fl2va_requested) are computed in the backend from its mode
|
|
// combo; derive them from the mode widget so the switch follows
|
|
// the mode pill live.
|
|
if (src.type === DIRECTOR_TYPE) {
|
|
const v = readDirectorRequest(src, link.origin_slot);
|
|
if (v != null) return v;
|
|
}
|
|
const v = readBoolFromNode(src);
|
|
if (v != null) return v;
|
|
}
|
|
}
|
|
}
|
|
|
|
const promotedValue = readPromotedWidgetValue(switchNode, "enabled");
|
|
if (promotedValue != null) return promotedValue;
|
|
|
|
// Fallback: local widget on the switch itself
|
|
const localW = getWidget(switchNode, "enabled");
|
|
return localW != null ? !!localW.value : true;
|
|
}
|
|
|
|
// ── target discovery ────────────────────────────────────────────────────
|
|
|
|
function getTargetNodeIds(switchNode) {
|
|
const graph = switchNode.graph ?? app.graph;
|
|
if (!graph) return [];
|
|
|
|
const ids = [];
|
|
for (const input of switchNode.inputs ?? []) {
|
|
if (!isTargetSlot(input)) continue;
|
|
if (input.link == null) continue;
|
|
|
|
const link =
|
|
graph.links?.[input.link] ?? graph._links?.get?.(input.link);
|
|
if (link && link.origin_id != null) {
|
|
const allNodes = graph._nodes ?? graph.nodes ?? [];
|
|
const src = resolveSourceNode(allNodes.find((n) => n.id === link.origin_id), graph);
|
|
if (src) ids.push(src.id);
|
|
}
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
function removeBackendSwitchNodes(prompt) {
|
|
if (!prompt || typeof prompt !== "object") return;
|
|
|
|
for (const [nodeId, nodeData] of Object.entries(prompt)) {
|
|
if (nodeData?.class_type !== NODE_TYPE) continue;
|
|
delete prompt[nodeId];
|
|
}
|
|
}
|
|
|
|
function removeBackendTargetInputsFromSwitchNodes(prompt) {
|
|
if (!prompt || typeof prompt !== "object") return;
|
|
|
|
for (const nodeData of Object.values(prompt)) {
|
|
if (nodeData?.class_type !== NODE_TYPE) continue;
|
|
const inputs = nodeData.inputs;
|
|
if (!inputs || typeof inputs !== "object" || Array.isArray(inputs)) continue;
|
|
|
|
for (const key of Object.keys(inputs)) {
|
|
if (key.startsWith(TARGET_PREFIX)) {
|
|
delete inputs[key];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function isPromptNodeKeyForTarget(key, targetId) {
|
|
const id = String(targetId);
|
|
return key === id || key.startsWith(`${id}:`) || key.endsWith(`:${id}`);
|
|
}
|
|
|
|
function removeInactiveTargetOutputs(output) {
|
|
if (!output || typeof output !== "object") return;
|
|
|
|
const allNodes = nodesInGraphTree(app.graph);
|
|
if (!allNodes.length) return;
|
|
|
|
const inactiveTargets = new Set();
|
|
for (const node of allNodes) {
|
|
if (node.type !== NODE_TYPE) continue;
|
|
|
|
for (const targetId of getTargetNodeIds(node)) {
|
|
const target = allNodes.find((n) => String(n.id) === String(targetId));
|
|
if (target && target.mode !== MODE_ACTIVE) {
|
|
inactiveTargets.add(target.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const key of Object.keys(output)) {
|
|
for (const targetId of inactiveTargets) {
|
|
if (isPromptNodeKeyForTarget(key, targetId)) {
|
|
delete output[key];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function sanitizeQueuePrompt(queuePromptData) {
|
|
if (!queuePromptData || typeof queuePromptData !== "object") return;
|
|
|
|
applyAllSwitches();
|
|
removeInactiveTargetOutputs(queuePromptData.output);
|
|
removeBackendTargetInputsFromSwitchNodes(queuePromptData.output);
|
|
removeBackendTargetInputsFromSwitchNodes(queuePromptData.prompt);
|
|
removeBackendSwitchNodes(queuePromptData.output);
|
|
removeBackendSwitchNodes(queuePromptData.prompt);
|
|
|
|
if (!queuePromptData.output && !queuePromptData.prompt) {
|
|
removeBackendTargetInputsFromSwitchNodes(queuePromptData);
|
|
removeBackendSwitchNodes(queuePromptData);
|
|
}
|
|
}
|
|
|
|
function patchQueuePrompt() {
|
|
if (api.__dasiwaNodeStatusSwitchQueuePatched) return;
|
|
|
|
const originalQueuePrompt = api.queuePrompt;
|
|
api.queuePrompt = async function (index, prompt, ...args) {
|
|
sanitizeQueuePrompt(prompt);
|
|
return originalQueuePrompt.apply(this, [index, prompt, ...args]);
|
|
};
|
|
|
|
api.__dasiwaNodeStatusSwitchQueuePatched = true;
|
|
}
|
|
|
|
function applyAllSwitches() {
|
|
const allNodes = nodesInGraphTree(app.graph);
|
|
for (const n of allNodes) {
|
|
if (n.type === NODE_TYPE) applySwitch(n);
|
|
}
|
|
app.graph?.setDirtyCanvas?.(true, true);
|
|
}
|
|
|
|
function patchGraphToPrompt() {
|
|
if (app.__dasiwaNodeStatusSwitchGraphToPromptPatched) return;
|
|
|
|
const originalGraphToPrompt = app.graphToPrompt;
|
|
app.graphToPrompt = async function (...args) {
|
|
applyAllSwitches();
|
|
const prompt = await originalGraphToPrompt.apply(this, args);
|
|
sanitizeQueuePrompt(prompt);
|
|
return prompt;
|
|
};
|
|
|
|
app.__dasiwaNodeStatusSwitchGraphToPromptPatched = true;
|
|
}
|
|
|
|
patchQueuePrompt();
|
|
patchGraphToPrompt();
|
|
|
|
// ── dynamic input management ────────────────────────────────────────────
|
|
|
|
function syncTargetSlots(node) {
|
|
const targetInputs = (node.inputs ?? []).filter(isTargetSlot);
|
|
const connected = targetInputs.filter((inp) => inp.link != null).length;
|
|
const desired = Math.min(connected + 1, MAX_TARGETS);
|
|
|
|
while (countTargetSlots(node) < desired) {
|
|
const nextNum = countTargetSlots(node) + 1;
|
|
node.addInput(targetSlotName(nextNum), "*");
|
|
}
|
|
|
|
while (countTargetSlots(node) > desired) {
|
|
const allInputs = node.inputs ?? [];
|
|
for (let i = allInputs.length - 1; i >= 0; i--) {
|
|
if (isTargetSlot(allInputs[i]) && allInputs[i].link == null) {
|
|
node.removeInput(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
node.setSize(node.computeSize());
|
|
}
|
|
|
|
// ── apply mode to targets ───────────────────────────────────────────────
|
|
|
|
/**
|
|
* "true -> active" => targets ACTIVE when enabled=true
|
|
* "false -> active" => targets ACTIVE when enabled=false
|
|
*/
|
|
function applySwitch(switchNode) {
|
|
const triggerW = getWidget(switchNode, "trigger_on");
|
|
const actionW = getWidget(switchNode, "action");
|
|
if (!triggerW || !actionW) return;
|
|
|
|
const enabled = readEnabled(switchNode);
|
|
const triggerOn = triggerW.value ?? "";
|
|
const action = actionW.value ?? "bypass";
|
|
const activeWhenTrue = triggerOn.startsWith("true");
|
|
const targetsActive = activeWhenTrue ? enabled : !enabled;
|
|
const actionMode = action === "mute" ? MODE_MUTE : MODE_BYPASS;
|
|
const targetMode = targetsActive ? MODE_ACTIVE : actionMode;
|
|
|
|
dlog("applySwitch", {
|
|
nodeId: switchNode.id,
|
|
enabled,
|
|
triggerOn,
|
|
action,
|
|
targetsActive,
|
|
targetMode,
|
|
});
|
|
|
|
const graph = switchNode.graph ?? app.graph;
|
|
if (!graph) return;
|
|
|
|
const targetIds = getTargetNodeIds(switchNode);
|
|
const allNodes = graph._nodes ?? graph.nodes ?? [];
|
|
|
|
for (const id of targetIds) {
|
|
const target = allNodes.find((n) => n.id === id);
|
|
if (target) target.mode = targetMode;
|
|
}
|
|
}
|
|
|
|
// ── propagate to downstream chained switches ────────────────────────────
|
|
//
|
|
// When this switch's effective state changes, find every other switch
|
|
// whose `enabled` input is wired (directly or transitively) from this
|
|
// switch's `enabled_out` and re-apply them too. This is more reliable
|
|
// than waiting for the polling mirror loop to notice.
|
|
|
|
function findDownstreamSwitches(switchNode, visited) {
|
|
visited = visited ?? new Set();
|
|
if (visited.has(switchNode.id)) return [];
|
|
visited.add(switchNode.id);
|
|
|
|
const graph = switchNode.graph ?? app.graph;
|
|
if (!graph) return [];
|
|
|
|
const allNodes = graph._nodes ?? graph.nodes ?? [];
|
|
const linksMap = graph.links ?? graph._links;
|
|
if (!linksMap) return [];
|
|
|
|
// Find the output slot named "enabled_out" on this node.
|
|
const outIdx = (switchNode.outputs ?? []).findIndex(
|
|
(o) => o?.name === "enabled_out"
|
|
);
|
|
if (outIdx < 0) return [];
|
|
|
|
const outSlot = switchNode.outputs[outIdx];
|
|
const linkIds = outSlot?.links ?? [];
|
|
|
|
const found = [];
|
|
const traceVisited = new Set();
|
|
|
|
function traceDownstream(linkId) {
|
|
if (traceVisited.has(linkId)) return;
|
|
traceVisited.add(linkId);
|
|
|
|
const link = linksMap?.[linkId] ?? linksMap?.get?.(linkId);
|
|
const target = link ? allNodes.find((n) => n.id === link.target_id) : null;
|
|
if (!target) return;
|
|
|
|
if (target.type?.includes?.("Reroute")) {
|
|
(target.outputs?.[0]?.links ?? []).forEach(traceDownstream);
|
|
} else if (target.type === NODE_TYPE && target.inputs?.[link.target_slot]?.name === "enabled") {
|
|
found.push(target, ...findDownstreamSwitches(target, visited));
|
|
}
|
|
}
|
|
|
|
linkIds.forEach(traceDownstream);
|
|
return found;
|
|
}
|
|
|
|
function applySwitchAndDownstream(switchNode) {
|
|
applySwitch(switchNode);
|
|
const downstream = findDownstreamSwitches(switchNode);
|
|
dlog("downstream of", switchNode.id, "=", downstream.map((d) => d.id));
|
|
for (const d of downstream) {
|
|
// Mirror upstream's effective enabled into the downstream
|
|
// switch's local widget so the UI reflects the chain.
|
|
const upstreamEnabled = readEnabled(switchNode);
|
|
const localW = getWidget(d, "enabled");
|
|
if (localW && localW.value !== upstreamEnabled) {
|
|
localW.value = upstreamEnabled;
|
|
}
|
|
applySwitch(d);
|
|
}
|
|
const graph = switchNode.graph ?? app.graph;
|
|
graph?.setDirtyCanvas?.(true, true);
|
|
}
|
|
|
|
// ── live mirror: external boolean -> local widget ───────────────────────
|
|
//
|
|
// When the "enabled" input is connected to an external boolean source,
|
|
// poll that source and mirror its value into the switch's own
|
|
// "enabled" widget. Writing to the local widget shows the toggle
|
|
// flipping on the switch's UI and triggers a live applySwitch.
|
|
|
|
function syncExternalToLocal(switchNode) {
|
|
const localW = getWidget(switchNode, "enabled");
|
|
if (!localW) return;
|
|
|
|
const graph = switchNode.graph ?? app.graph;
|
|
if (!graph) return;
|
|
|
|
// The external value can arrive through this input directly, or through
|
|
// the enclosing subgraph host after the widget has been promoted.
|
|
const externalValue = readEnabled(switchNode);
|
|
if (externalValue == null) return;
|
|
|
|
if (localW.value === externalValue) return; // already in sync
|
|
|
|
localW.value = externalValue;
|
|
applySwitch(switchNode);
|
|
graph.setDirtyCanvas?.(true, true);
|
|
}
|
|
|
|
let mirrorLoopRunning = false;
|
|
|
|
function mirrorFrame() {
|
|
if (!mirrorLoopRunning) return;
|
|
try {
|
|
const allNodes = nodesInGraphTree(app.graph);
|
|
for (const n of allNodes) {
|
|
if (n.type === NODE_TYPE) syncExternalToLocal(n);
|
|
}
|
|
} catch (_) {}
|
|
requestAnimationFrame(mirrorFrame);
|
|
}
|
|
|
|
function startMirrorLoop() {
|
|
if (mirrorLoopRunning) return;
|
|
mirrorLoopRunning = true;
|
|
requestAnimationFrame(mirrorFrame);
|
|
}
|
|
|
|
startMirrorLoop();
|
|
|
|
// ── extension registration ──────────────────────────────────────────────
|
|
|
|
app.registerExtension({
|
|
name: "DaSiWa.NodeStatusSwitch",
|
|
|
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData.name !== NODE_TYPE) return;
|
|
|
|
const origCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
origCreated?.apply(this, arguments);
|
|
const self = this;
|
|
|
|
// Ensure the chaining output pin exists. ComfyUI normally
|
|
// adds it from RETURN_TYPES, but be defensive in case the
|
|
// node definition is reloaded without it.
|
|
const hasOutput = (self.outputs ?? []).some(
|
|
(o) => o?.name === "enabled_out"
|
|
);
|
|
if (!hasOutput) {
|
|
self.addOutput("enabled_out", "BOOLEAN");
|
|
}
|
|
|
|
if (countTargetSlots(self) === 0) {
|
|
self.addInput(targetSlotName(1), "*");
|
|
}
|
|
|
|
// Local widget callbacks: live updates when the user
|
|
// toggles directly on the switch node. Cascade through
|
|
// any chained downstream switches.
|
|
for (const w of self.widgets ?? []) {
|
|
const origCb = w.callback;
|
|
w.callback = function (...args) {
|
|
origCb?.apply(this, args);
|
|
requestAnimationFrame(() => applySwitchAndDownstream(self));
|
|
};
|
|
}
|
|
|
|
self.setSize(self.computeSize());
|
|
};
|
|
|
|
const origConnChange = nodeType.prototype.onConnectionsChange;
|
|
nodeType.prototype.onConnectionsChange = function (
|
|
side,
|
|
slotIndex,
|
|
connected,
|
|
linkInfo
|
|
) {
|
|
origConnChange?.apply(this, arguments);
|
|
const self = this;
|
|
if (side === 1) {
|
|
requestAnimationFrame(() => {
|
|
syncTargetSlots(self);
|
|
applySwitchAndDownstream(self);
|
|
});
|
|
}
|
|
};
|
|
|
|
const origOnConfigure = nodeType.prototype.onConfigure;
|
|
nodeType.prototype.onConfigure = function (data) {
|
|
origOnConfigure?.apply(this, arguments);
|
|
const self = this;
|
|
// Migration: older saved graphs had no outputs. Add the
|
|
// chaining output if missing. Strip any *other* outputs
|
|
// (defensive — should not occur).
|
|
const outs = self.outputs ?? [];
|
|
for (let i = outs.length - 1; i >= 0; i--) {
|
|
if (outs[i]?.name !== "enabled_out") {
|
|
self.removeOutput(i);
|
|
}
|
|
}
|
|
const hasOutput = (self.outputs ?? []).some(
|
|
(o) => o?.name === "enabled_out"
|
|
);
|
|
if (!hasOutput) {
|
|
self.addOutput("enabled_out", "BOOLEAN");
|
|
}
|
|
requestAnimationFrame(() => syncTargetSlots(self));
|
|
};
|
|
},
|
|
|
|
async loadedGraphNode(node) {
|
|
if (node.type !== NODE_TYPE) return;
|
|
requestAnimationFrame(() => {
|
|
const outs = node.outputs ?? [];
|
|
for (let i = outs.length - 1; i >= 0; i--) {
|
|
if (outs[i]?.name !== "enabled_out") {
|
|
node.removeOutput(i);
|
|
}
|
|
}
|
|
const hasOutput = (node.outputs ?? []).some(
|
|
(o) => o?.name === "enabled_out"
|
|
);
|
|
if (!hasOutput) {
|
|
node.addOutput("enabled_out", "BOOLEAN");
|
|
}
|
|
syncTargetSlots(node);
|
|
});
|
|
},
|
|
|
|
// The only place mode is applied: right before the prompt is sent.
|
|
async beforeQueued(...args) {
|
|
for (const arg of args) {
|
|
sanitizeQueuePrompt(arg);
|
|
}
|
|
|
|
applyAllSwitches();
|
|
},
|
|
});
|