Files
SpaceWarpStudio-ComfyUI-Set…/web/js/setinputgetoutput.js
T
2026-03-31 01:06:41 +02:00

1331 lines
62 KiB
JavaScript

// Standalone Set/Get Node Implementation for ComfyUI
import { app } from "../../../scripts/app.js";
// Get LiteGraph instance
const LiteGraph = window.LiteGraph;
if (!LiteGraph) {
console.error("LiteGraph not found. Cannot register Standalone Set/Get nodes.");
} else {
// --- Configuration ---
const disablePrefix = false; // Set to true to remove "Set_" and "Get_" prefixes from titles
const DEBUG = false; // Set true for verbose console logging
let isExecutionActive = false; // Pause expensive UI updates during execution
// --- Helper Functions ---
function debugLog(...args) { if (DEBUG) console.log(...args); }
function debugWarn(...args) { if (DEBUG) console.warn(...args); }
// Placeholder/Simple implementation for color setting
// Replace with your actual color logic if needed
function setColorAndBgColor(type) {
// Example: Log the type instead of setting colors
// console.log(`Node ${this.id}: Setting color based on type: ${type}`);
// Default colors or simple logic:
this.color = "#333";
this.bgcolor = "#555";
if (type && type !=='*') {
const colorMap = {
"MODEL": { color: "#233", bgcolor: "#355" },
"CONDITIONING": { color: "#322", bgcolor: "#533" },
"LATENT": { color: "#223", bgcolor: "#335" },
"VAE": { color: "#232", bgcolor: "#353" },
"IMAGE": { color: "#332", bgcolor: "#553" },
// Add other types as needed
};
const colors = colorMap[type.toUpperCase()];
if (colors) {
this.color = colors.color;
this.bgcolor = colors.bgcolor;
}
}
}
// --- Node Conversion/Creation Helpers ---
function getNodeVarName(node) {
const v = node?.widgets?.[0]?.value;
return (typeof v === "string") ? v : (v == null ? "" : String(v));
}
function readVec2(v) {
// Accept Array, Float32Array, etc.
if (v && typeof v === "object" && typeof v.length === "number" && v.length >= 2) {
const x = Number(v[0]);
const y = Number(v[1]);
if (Number.isFinite(x) && Number.isFinite(y)) return [x, y];
}
return null;
}
function getNodePos(node) {
return readVec2(node?.pos) || readVec2(node?.position) || null;
}
function getNodeWidth(node) {
const size = readVec2(node?.size);
if (size) return size[0];
if (typeof node?.computeSize === "function") {
const s = node.computeSize();
const cs = readVec2(s);
if (cs) return cs[0];
}
return 180; // reasonable fallback
}
function computeSetNames(graph) {
if (!graph || !graph._nodes) return [];
return [...new Set(
graph._nodes
.filter(n => n.type === "SetInputNode" && n.widgets && n.widgets[0])
.map(n => n.widgets[0].value)
.filter(name => name !== null && name !== undefined && name !== "")
)].sort();
}
function updateAllGetterCombos(graph, force = false) {
if (!graph || !graph._nodes) return;
const names = computeSetNames(graph);
const signature = names.join("\u001f");
if (!force && graph._setGetNameSignature === signature) return;
graph._setGetNameSignature = signature;
graph._setGetNameList = names;
const getters = graph._nodes.filter(n => n.type === "GetOutputNode");
getters.forEach(getter => {
if (getter.setComboValues) {
getter.setComboValues();
}
});
}
function getRootGraph() {
return app?.graph || app?.rootGraph || app?.rootGraphInternal || null;
}
function flushSetGetUpdates(graph) {
if (!graph || !graph._nodes) return;
// First run pending Set updates
graph._nodes.forEach(node => {
if (node.type === "SetInputNode" && node._pendingUpdate) {
node._pendingUpdate = false;
node.update?.();
}
});
// Refresh combo list after names settle
updateAllGetterCombos(graph, true);
// Then run pending Get renames
graph._nodes.forEach(node => {
if (node.type === "GetOutputNode" && node._pendingRename) {
node._pendingRename = false;
node.onRename?.();
}
});
// Redraw once after flush
if (graph.list_of_graphcanvas) {
graph.list_of_graphcanvas.forEach(c => c.setDirty(true, true));
}
}
function disconnectAllLinks(node) {
const graph = node?.graph;
if (!graph) return;
// Remove incoming links
if (Array.isArray(node.inputs)) {
for (const input of node.inputs) {
if (typeof input?.link === "number") {
if (typeof graph.removeLink === "function") {
graph.removeLink(input.link);
} else {
console.warn("Set/Get: graph.removeLink missing; cannot remove link", input.link);
}
}
}
}
// Remove outgoing links
if (Array.isArray(node.outputs)) {
for (const output of node.outputs) {
if (Array.isArray(output?.links) && output.links.length) {
// copy because removeLink mutates graph.links / links arrays
const links = output.links.slice();
for (const linkId of links) {
if (typeof graph.removeLink === "function") {
graph.removeLink(linkId);
} else {
console.warn("Set/Get: graph.removeLink missing; cannot remove link", linkId);
}
}
}
}
}
}
function resolveRegisteredTypeKey(type) {
const reg = LiteGraph?.registered_node_types;
if (!reg || !type) return type;
if (reg[type]) return type;
// Some LiteGraph builds use "category/name" keys; try suffix match.
const suffix1 = `/${type}`;
for (const key of Object.keys(reg)) {
if (key === type) return key;
if (key.endsWith(suffix1)) return key;
}
return type;
}
function safeCreateNode(type) {
if (!type) return null;
const typeKey = resolveRegisteredTypeKey(type);
// Most LiteGraph builds
if (typeof LiteGraph?.createNode === "function") {
try {
const n = LiteGraph.createNode(typeKey);
if (n && !n.type) n.type = typeKey;
return n;
} catch (e) {
console.warn(`Set/Get: LiteGraph.createNode("${typeKey}") threw`, e);
}
}
// Fallback: registry-based construction (varies by LiteGraph version)
const reg = LiteGraph?.registered_node_types?.[typeKey];
if (!reg) {
console.warn(`Set/Get: Node type "${typeKey}" not found in LiteGraph.registered_node_types`);
return null;
}
try {
// Some versions store the class directly
if (typeof reg === "function") {
const n = new reg();
if (!n.type) n.type = typeKey;
return n;
}
// Other versions store an object containing the class
if (typeof reg?.class === "function") {
const n = new reg.class();
if (!n.type) n.type = typeKey;
return n;
}
} catch (e) {
console.warn(`Set/Get: failed constructing node "${typeKey}" from registry`, e);
}
console.warn(`Set/Get: unsupported registry entry for node type "${typeKey}"`, reg);
return null;
}
function safeGraphAddNode(graph, node) {
if (!graph || !node) return false;
try {
if (typeof graph.add === "function") { graph.add(node); return true; }
if (typeof graph.addNode === "function") { graph.addNode(node); return true; }
} catch (e) {
console.warn("Set/Get: graph add failed", e);
return false;
}
console.warn("Set/Get: graph has no add/addNode method");
return false;
}
function graphHasNode(graph, node) {
if (!graph || !node) return false;
if (typeof node.id === "number" && graph._nodes_by_id && graph._nodes_by_id[node.id]) return true;
if (Array.isArray(graph._nodes) && graph._nodes.includes(node)) return true;
return false;
}
function safeGraphRemoveNode(graph, node) {
if (!graph || !node) return false;
try {
if (typeof graph.remove === "function") { graph.remove(node); return true; }
if (typeof graph.removeNode === "function") { graph.removeNode(node); return true; }
if (typeof node.id === "number" && typeof graph.remove === "function") { graph.remove(node.id); return true; }
} catch (e) {
console.warn("Set/Get: graph remove failed", e);
return false;
}
console.warn("Set/Get: graph has no remove/removeNode method");
return false;
}
function initConvertedNodeNameAndUI(newNode, name) {
if (!newNode?.widgets?.[0]) return;
newNode.widgets[0].value = name ?? "";
// Ensure title/type updates happen
if (newNode.type === "GetOutputNode" && typeof newNode.onRename === "function") {
newNode.onRename();
} else if (newNode.type === "SetInputNode") {
if (newNode.widgets[0].value !== "") {
newNode.title = (!disablePrefix ? "Set_" : "") + newNode.widgets[0].value;
} else {
newNode.title = "Set (Input)";
}
if (newNode.properties) {
newNode.properties.previousName = newNode.widgets[0].value;
}
if (typeof newNode.update === "function") {
newNode.update();
}
}
}
function selectAndRedraw(node) {
if (app?.canvas) {
app.canvas.selectNode(node, false);
app.canvas.setDirty(true, true);
}
if (node?.graph?.list_of_graphcanvas) {
node.graph.list_of_graphcanvas.forEach(c => c.setDirty(true, true));
}
}
function getSelectedNodesFromCanvas(canvas) {
// ComfyUI/LiteGraph variants:
// - Nodes 2.0: canvas.selectedItems is a Set (nodes, groups, etc)
// - Older: canvas.selected_nodes is an object map
let items = [];
try {
if (canvas?.selectedItems && typeof canvas.selectedItems[Symbol.iterator] === "function") {
items = Array.from(canvas.selectedItems);
} else if (canvas?.selected_nodes && typeof canvas.selected_nodes === "object") {
items = Object.values(canvas.selected_nodes);
} else if (app?.canvas?.selected_nodes && typeof app.canvas.selected_nodes === "object") {
items = Object.values(app.canvas.selected_nodes);
}
} catch (_) {
items = [];
}
// Filter to node-like objects
return items.filter(n =>
n &&
typeof n === "object" &&
typeof n.type === "string" &&
n.graph
);
}
function convertSelectedSetGetNodes(canvas) {
const selected = getSelectedNodesFromCanvas(canvas)
.filter(n => n.type === "SetInputNode" || n.type === "GetOutputNode");
if (selected.length === 0) return;
// Snapshot list to avoid mutation while converting/removing nodes
const toConvert = selected.slice();
const created = [];
for (const node of toConvert) {
const targetType = node.type === "SetInputNode" ? "GetOutputNode" : "SetInputNode";
const newNode = convertNodeInPlace(node, targetType, { select: false });
if (newNode) created.push(newNode);
}
// Select one of the new nodes (optional, but avoids ending up with no selection after removal)
const first = created[0];
if (first) {
const c = canvas || app?.canvas;
if (c?.selectNode) {
try { c.selectNode(first, false); } catch (_) {}
}
}
// Ensure redraw
if (app?.canvas?.setDirty) app.canvas.setDirty(true, true);
if (first?.graph?.list_of_graphcanvas) {
first.graph.list_of_graphcanvas.forEach(gc => gc.setDirty(true, true));
}
}
function createSiblingNodeToRight(sourceNode, targetType) {
const graph = sourceNode?.graph;
if (!graph) return null;
const name = getNodeVarName(sourceNode);
const newNode = safeCreateNode(targetType);
if (!newNode) {
console.warn("Set/Get: Create sibling failed - could not create node", targetType);
return null;
}
const sourcePos = getNodePos(sourceNode) || (app?.canvas?.graph_mouse ? readVec2(app.canvas.graph_mouse) : null) || [0, 0];
const offsetX = getNodeWidth(sourceNode) + 50;
newNode.pos = [sourcePos[0] + offsetX, sourcePos[1]];
// Keep mode consistent with source (useful if user is working with bypassed groups)
if (typeof sourceNode.mode === "number") {
newNode.mode = sourceNode.mode;
}
if (!safeGraphAddNode(graph, newNode)) {
console.warn("Set/Get: Create sibling failed - could not add node to graph", targetType);
return null;
}
if (!graphHasNode(graph, newNode)) {
console.warn("Set/Get: Create sibling warning - graph add returned, but node not found in graph", newNode);
}
initConvertedNodeNameAndUI(newNode, name);
selectAndRedraw(newNode);
return newNode;
}
function convertNodeInPlace(sourceNode, targetType, options = {}) {
const graph = sourceNode?.graph;
if (!graph) return null;
const name = getNodeVarName(sourceNode);
const pos = getNodePos(sourceNode) || (app?.canvas?.graph_mouse ? readVec2(app.canvas.graph_mouse) : null) || [0, 0];
const mode = (typeof sourceNode.mode === "number") ? sourceNode.mode : undefined;
// User requested: no confirmation, just disconnect anything active.
disconnectAllLinks(sourceNode);
const newNode = safeCreateNode(targetType);
if (!newNode) {
console.warn("Set/Get: Convert failed - could not create node", targetType);
return null;
}
newNode.pos = [pos[0], pos[1]];
if (typeof mode === "number") newNode.mode = mode;
if (!safeGraphAddNode(graph, newNode)) {
console.warn("Set/Get: Convert failed - could not add new node to graph", targetType);
return null;
}
if (!graphHasNode(graph, newNode)) {
console.warn("Set/Get: Convert aborted - new node not found in graph, not deleting original", newNode);
return null;
}
initConvertedNodeNameAndUI(newNode, name);
const removed = safeGraphRemoveNode(graph, sourceNode);
if (!removed) {
console.warn("Set/Get: Convert warning - failed to remove original node", sourceNode);
}
if (options.select !== false) {
selectAndRedraw(newNode);
} else {
// Avoid viewport/selection churn for batch operations; still redraw.
if (app?.canvas?.setDirty) app.canvas.setDirty(true, true);
if (newNode?.graph?.list_of_graphcanvas) {
newNode.graph.list_of_graphcanvas.forEach(c => c.setDirty(true, true));
}
}
return newNode;
}
// --- Main Extension ---
app.registerExtension({
name: "Standalone.SetGetNodes", // Keep the extension name or change if desired
/**
* ComfyUI (new) context menu API (Nodes 2.0+):
* Prefer this over node.getExtraMenuOptions(), which is deprecated.
* Docs: https://docs.comfy.org/custom-nodes/js/context-menu-migration
*/
getNodeMenuItems(node) {
try {
if (!node) return [];
const t = node.type;
// If multiple nodes are selected, offer a bulk convert right from the node menu too.
const selected = getSelectedNodesFromCanvas(app?.canvas);
const selectedSetGet = selected.filter(n => n.type === "SetInputNode" || n.type === "GetOutputNode");
const bulkItems = (selectedSetGet.length > 1)
? [
null,
{
content: `Convert selected Set/Get nodes (${selectedSetGet.length})`,
callback: () => { convertSelectedSetGetNodes(app?.canvas); },
},
]
: [];
if (t === "SetInputNode") {
return [
...bulkItems,
null,
{
content: "Convert to Get (Output)",
callback: () => {
convertNodeInPlace(node, "GetOutputNode");
},
},
{
content: "Create Get (Output) to the right",
callback: () => {
createSiblingNodeToRight(node, "GetOutputNode");
},
},
];
}
if (t === "GetOutputNode") {
return [
...bulkItems,
null,
{
content: "Convert to Set (Input)",
callback: () => {
convertNodeInPlace(node, "SetInputNode");
},
},
{
content: "Create Set (Input) to the right",
callback: () => {
createSiblingNodeToRight(node, "SetInputNode");
},
},
];
}
return [];
} catch (e) {
console.error("[SetInputGetOutput] getNodeMenuItems error", e);
return [];
}
},
/**
* Canvas context menu (recommended API):
* Adds a bulk action when selection contains Set/Get nodes.
*/
getCanvasMenuItems(canvas) {
const selected = getSelectedNodesFromCanvas(canvas);
const hasAny = selected.some(n => n.type === "SetInputNode" || n.type === "GetOutputNode");
if (!hasAny) return [];
return [
null,
{
content: "Convert selected Set/Get nodes",
callback: () => { convertSelectedSetGetNodes(canvas); },
},
];
},
// Pause expensive UI updates during execution
onExecutionStart() {
isExecutionActive = true;
},
onExecutionEnd() {
isExecutionActive = false;
const graph = getRootGraph();
if (graph) {
flushSetGetUpdates(graph);
}
},
onExecutionError() {
isExecutionActive = false;
const graph = getRootGraph();
if (graph) {
flushSetGetUpdates(graph);
}
},
registerCustomNodes() {
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~ SET NODE -> SetInputNode ~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class SetInputNode extends LGraphNode {
defaultVisibility = true;
serialize_widgets = true;
drawConnection = false;
currentGetters = null;
slotColor = "#FFF"; // Default link color
canvas = app.canvas;
menuEntry = "Show connections";
constructor(title) {
super(title || "Set (Input)");
if (!this.properties) {
this.properties = { "previousName": "" };
}
this.properties.showOutputText = SetInputNode.defaultVisibility;
this._updateScheduled = false;
this._lastName = "";
this._lastInputType = "";
this._pendingUpdate = false;
const node = this;
this.addWidget(
"text",
"Input Name",
'', // Initial value
(value, widget, evt) => { // Widget callback
// No validation needed here anymore for duplicates
if (node.widgets[0].value !== '') {
node.title = (!disablePrefix ? "Set_" : "") + node.widgets[0].value;
} else {
node.title = "Set (Input)" // Reset title if name is cleared
}
node.properties.previousName = node.widgets[0].value; // Store for update checks
node.requestUpdate(); // Trigger updates (debounced)
},
{}
);
this.addInput("*", "*"); // Default input
// --- Connection Handling ---
this.onConnectionsChange = function(
slotType, slot, isChangeConnect, link_info, output
) {
// On Disconnect Input
if (slotType === LiteGraph.INPUT && !isChangeConnect) {
// If the link is removed, reset the input type if it's the primary input
if(slot === 0) {
this.inputs[0].type = '*';
this.inputs[0].name = '*';
// Optionally reset title if name wasn't manually set
if (this.widgets[0].value === '') {
this.title = "Set (Input)";
}
setColorAndBgColor.call(this, '*'); // Reset color
}
}
// On Connect Input
if (link_info && node.graph && slotType === LiteGraph.INPUT && isChangeConnect && slot === 0) {
const fromNode = node.graph.getNodeById(link_info.origin_id);
if (fromNode && fromNode.outputs && fromNode.outputs[link_info.origin_slot]) {
const type = fromNode.outputs[link_info.origin_slot].type || '*';
// Update input type and name
this.inputs[0].type = type;
this.inputs[0].name = type;
// Auto-name based on type if widget name is empty/default
if (this.widgets[0].value === '' || this.widgets[0].value === '*') {
this.widgets[0].value = type;
this.title = (!disablePrefix ? "Set_" : "") + type;
this.properties.previousName = type;
}
setColorAndBgColor.call(this, type);
} else {
console.warn("SetInputNode: Connected input node or slot not found.", link_info);
}
}
// Update getters regardless of connection change type
this.requestUpdate();
}; // End onConnectionsChange
// --- Name Validation (Removed Duplicate Check) ---
this.validateName = function(graph) {
// Name validation logic for duplicates is removed
this.requestUpdate(); // Ensure update is called
};
// --- Cloning ---
this.clone = function () {
const cloned = SetInputNode.prototype.clone.apply(this);
if (cloned.inputs && cloned.inputs[0]) {
cloned.inputs[0].name = '*';
cloned.inputs[0].type = '*';
}
cloned.properties.previousName = ''; // Reset previous name on clone
// Keep the widget value on clone if desired, or clear it:
// if(cloned.widgets && cloned.widgets[0]) cloned.widgets[0].value = '';
cloned.size = cloned.computeSize();
return cloned;
};
// --- Node Lifecycle Callbacks ---
this.onAdded = function(graph) {
this.validateName(graph); // Calls update
updateAllGetterCombos(graph, true);
};
this.onRemoved = function() {
if (!this.graph) return;
// Update combo boxes on removal
setTimeout(() => { if (this.graph) updateAllGetterCombos(this.graph, true); }, 0);
};
// Override onModeChange to trigger update
const originalOnModeChange = this.onModeChange;
this.onModeChange = function(mode) {
if (originalOnModeChange) {
originalOnModeChange.apply(this, arguments);
}
// When mode changes (bypass), trigger update after a short delay
// LiteGraph.BYPASS == 4
debugLog(`SetInputNode ${this.id} (${this.widgets[0].value}) mode changed to: ${mode}`);
setTimeout(() => { if (this.requestUpdate) this.requestUpdate(); }, 50);
};
// --- Core Update Logic ---
this.requestUpdate = function() {
if (isExecutionActive) {
this._pendingUpdate = true;
return;
}
if (this._updateScheduled) return;
this._updateScheduled = true;
requestAnimationFrame(() => {
this._updateScheduled = false;
if (this.update) this.update();
});
};
this.update = function() {
const node = this; // Use 'node' reference for safety in callbacks/timeouts
if (!node.graph) { return; }
if (isExecutionActive) {
node._pendingUpdate = true;
return;
}
const currentName = node.widgets[0].value;
const inputType = (node.inputs && node.inputs[0]) ? node.inputs[0].type : '*';
const inputTypeChanged = inputType !== node._lastInputType;
if (inputTypeChanged) {
node._lastInputType = inputType;
}
// Find Getters linked to current name and tell them to update
const currentGetters = this.findGetters(node.graph, false);
currentGetters.forEach(getter => {
if (getter.onRename) {
// Use timeout to allow graph state to update before getter reacts
setTimeout(() => { if (getter.graph) getter.onRename(); }, 0);
} else if (inputTypeChanged && getter.setType && getter.outputs[0].type !== inputType) {
// Fallback if onRename is unavailable
setTimeout(() => { if (getter.graph) getter.setType(inputType); }, 0);
}
});
// Handle Getters linked to a previous name (if name changed)
if (node.properties.previousName && currentName !== node.properties.previousName) {
debugLog(`SetInputNode ${node.id} name changed from ${node.properties.previousName} to ${currentName}`);
const previousGetters = this.findGetters(node.graph, true);
previousGetters.forEach(getter => {
// Check if the getter is still potentially linked by the old name in its widget
if (getter.onRename && getter.widgets[0].value === node.properties.previousName) {
debugLog(`Updating previous getter ${getter.id} (was linked to ${node.properties.previousName})`);
setTimeout(() => { if (getter.graph) getter.onRename(); }, 0);
}
});
node.properties.previousName = ''; // Clear previous name after handling
}
// Only rebuild combo list if the set names actually changed
if (currentName !== node._lastName) {
node._lastName = currentName;
updateAllGetterCombos(node.graph);
}
// Redraw only if we're actively drawing virtual connections
if (node.drawConnection && node.graph && node.graph.list_of_graphcanvas) {
node.graph.list_of_graphcanvas.forEach(canvas => canvas.setDirty(true, true));
}
};
// --- Helper: Find Linked Getters ---
// findGetters - Ensure bypass check uses mode 4 for the GetNode
this.findGetters = function(graph, checkForPreviousName) {
const name = checkForPreviousName ? this.properties.previousName : this.widgets[0].value;
if (!graph || name === '') return [];
return graph._nodes.filter(otherNode =>
otherNode.type === 'GetOutputNode' && // Check for GetOutputNode
otherNode.widgets && otherNode.widgets[0] &&
otherNode.widgets[0].value === name //&&
// We don't check the GetNode's bypass status here, Set should update all regardless
// otherNode.mode !== 4 // Check if the GetNode itself is bypassed
);
};
// Node is virtual (doesn't execute on backend)
this.isVirtualNode = true;
} // End constructor
// --- Context Menu ---
getExtraMenuOptions(_, options) {
this.menuEntry = this.drawConnection ? "Hide connections" : "Show connections";
options.unshift(
{
content: "Convert to Get (Output)",
callback: () => {
convertNodeInPlace(this, "GetOutputNode");
},
},
{
content: "Create Get (Output) to the right",
callback: () => {
createSiblingNodeToRight(this, "GetOutputNode");
},
},
{
content: this.menuEntry,
callback: () => {
// Need graph context
if (!this.graph) return;
this.currentGetters = this.findGetters(this.graph);
if (!this.currentGetters || this.currentGetters.length === 0) return;
// Determine color from the first active getter found or default
let activeGetter = this.currentGetters.find(g => g.mode !== 4 && g.mode !== 2);
let linkType = activeGetter ? (activeGetter.outputs[0].type || '*') : (this.inputs[0].type || '*');
// Use default connection color from canvas based on type
this.slotColor = this.canvas?.default_connection_color_byType?.[linkType] || "#FFF";
this.drawConnection = !this.drawConnection;
this.menuEntry = this.drawConnection ? "Hide connections" : "Show connections"; // Update label
this.canvas?.setDirty(true, true);
},
},
{
content: "Hide all connections",
callback: () => {
if (!this.graph) return;
const allSetGet = this.graph._nodes.filter(otherNode =>
otherNode.type === "GetOutputNode" || otherNode.type === "SetInputNode"
);
allSetGet.forEach(node => { node.drawConnection = false; });
this.canvas?.setDirty(true, true);
},
},
{
content: "Go to first active Getter",
callback: () => {
if (!this.graph) return;
const firstActiveGetter = this.findGetters(this.graph)
.find(getter => getter.mode !== 4 && getter.mode !== 2); // Find first non-bypassed, non-muted
if (firstActiveGetter && this.canvas) {
this.canvas.centerOnNode(firstActiveGetter);
this.canvas.selectNode(firstActiveGetter, false);
} else {
debugLog("No active GetOutput nodes found for this SetInput node.");
}
},
}
);
}
// --- Drawing Connections ---
onDrawForeground(ctx, graphcanvas) {
if (this.flags.collapsed || !this.drawConnection) {
return;
}
this.currentGetters = this.findGetters(this.graph);
if (!this.currentGetters || this.currentGetters.length === 0) return;
const lGraphCanvas = graphcanvas || app.canvas; // Use passed canvas or global fallback
if (!lGraphCanvas) return; // Need canvas to draw
ctx.save();
ctx.lineWidth = 1; // Thinner line for virtual connections
ctx.strokeStyle = this.slotColor || "#FFF";
const startPos = this.localToGlobal([this.size[0], LiteGraph.NODE_SLOT_HEIGHT / 2]);
for (const getter of this.currentGetters) {
// Only draw lines to non-bypassed, non-muted Getters
if (getter.mode === 4 || getter.mode === 2) continue; // LiteGraph.BYPASS == 4, LiteGraph.NEVER == 2
if (!getter.isVisibleOnCanvas(lGraphCanvas)) continue; // Don't draw if offscreen
const endPos = getter.localToGlobal([0, LiteGraph.NODE_SLOT_HEIGHT / 2]);
ctx.beginPath();
ctx.moveTo(startPos[0], startPos[1]);
// Simple bezier curve
ctx.bezierCurveTo(
startPos[0] + 100, startPos[1],
endPos[0] - 100, endPos[1],
endPos[0], endPos[1]
);
ctx.stroke();
}
ctx.restore();
}
} // End SetInputNode Class
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~ GET NODE -> GetOutputNode ~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class GetOutputNode extends LGraphNode {
defaultVisibility = true;
serialize_widgets = true;
drawConnection = false;
slotColor = "#FFF";
currentSetter = null;
canvas = app.canvas;
constructor(title) {
super(title || "Get (Output)");
if (!this.properties) { this.properties = {}; }
this.properties.showOutputText = GetOutputNode.defaultVisibility;
this._validateScheduled = false;
this._pendingRename = false;
const node = this;
this.addWidget(
"combo",
"Output Name",
"", // Initial value
(value, widget, event) => { // Widget callback
this.onRename(); // Trigger update when combo value changes
},
{ // Combo options
values: () => { // Function to populate the combo box
if (!node.graph) return [];
const setterNodes = node.graph._nodes.filter((otherNode) =>
otherNode.type == 'SetInputNode' // Look for SetInputNode
);
// Get unique names and sort them
const uniqueNames = [...new Set(
setterNodes
.map((otherNode) => otherNode.widgets && otherNode.widgets[0] ? otherNode.widgets[0].value : null)
.filter(name => name !== null && name !== '') // Filter out null/empty names
)].sort();
return uniqueNames;
}
}
);
this.addOutput("*", '*'); // Default output
// --- Connection Validation ---
this.onConnectionsChange = function(
slotType, slot, isChangeConnect, link_info, output
) {
// Only validate on output connections (not input)
// Delay validation to ensure connection is fully established
if (slotType === LiteGraph.OUTPUT && slot === 0 && this.graph) {
setTimeout(() => {
if (this.graph && this.validateLinks) {
this.validateLinks();
}
}, 50);
}
};
// --- Name/Type Update Logic ---
this.setName = function(name) {
if (this.widgets && this.widgets[0]) {
this.widgets[0].value = name;
this.onRename();
// Note: Serialization might happen automatically depending on widget config
// If manual serialization needed: LiteGraph.doSerialize(this);
}
};
this.onRename = function() {
if (!this.graph) { return; } // Ensure graph context
if (isExecutionActive) {
this._pendingRename = true;
return;
}
const setter = this.findSetter(this.graph); // Finds the first *active* setter
let linkType = '*';
let titleName = (this.widgets && this.widgets[0] ? this.widgets[0].value : null) || "unset";
if (setter && setter.inputs && setter.inputs[0]) {
linkType = setter.inputs[0].type || '*'; // Get type from active setter's input
titleName = setter.widgets[0].value; // Use setter's name for title
} else {
// If no active setter found, keep widget value but use '*' type
linkType = '*';
}
// --- Critical: Update Output Type ---
const desiredTitle = (!disablePrefix ? "Get_" : "") + titleName;
const output = this.outputs && this.outputs[0] ? this.outputs[0] : null;
const typeChanged = output ? output.type !== linkType : true;
const titleChanged = this.title !== desiredTitle;
if (!typeChanged && !titleChanged) {
return;
}
if (this.outputs && this.outputs[0]) {
if (typeChanged) {
debugLog(`GetOutputNode ${this.id} (${titleName}): Updating output type from ${this.outputs[0].type} to ${linkType}`);
this.outputs[0].type = linkType;
this.outputs[0].name = linkType; // Keep name and type consistent
// Important: Notify LiteGraph that connections may need revalidation
this.graph.links_dirty = true;
// Delay validation to ensure type change is processed and connections are established
if (!this._validateScheduled) {
this._validateScheduled = true;
setTimeout(() => {
this._validateScheduled = false;
if (this.graph && this.validateLinks) {
this.validateLinks();
}
}, 100);
}
}
} else if (!this.outputs || this.outputs.length === 0) {
this.addOutput("output", linkType); // Safety add output
this.graph.links_dirty = true;
}
// --- End Critical Section ---
this.title = desiredTitle;
if (typeChanged) {
setColorAndBgColor.call(this, linkType); // Update color
}
// Redraw the node
if ((typeChanged || titleChanged) && this.graph && this.graph.list_of_graphcanvas) {
this.graph.list_of_graphcanvas.forEach(canvas => canvas.setDirty(true, true));
}
};
// Method called by SetNode to update combo box options
this.setComboValues = function() {
if (this.widgets && this.widgets[0] && this.widgets[0].type === 'combo' && this.graph) {
const comboWidget = this.widgets[0];
const uniqueNames = this.graph._setGetNameList || computeSetNames(this.graph);
// Check if options actually changed before updating
if (JSON.stringify(comboWidget.options.values) !== JSON.stringify(uniqueNames)) {
comboWidget.options.values = uniqueNames;
// Ensure current value still exists, if not, maybe clear or default?
if (!uniqueNames.includes(comboWidget.value) && comboWidget.value !== '') {
// console.log(`GetOutputNode ${this.id}: Current value "${comboWidget.value}" no longer exists. Clearing.`);
// comboWidget.value = ''; // Optionally clear if linked SetNode removed
// this.onRename(); // Trigger update if value changed
}
}
}
};
// --- Cloning ---
this.clone = function () {
const cloned = GetOutputNode.prototype.clone.apply(this);
cloned.size = cloned.computeSize();
// Ensure combo value is preserved on clone
if (this.widgets && this.widgets[0] && cloned.widgets && cloned.widgets[0]) {
cloned.widgets[0].value = this.widgets[0].value;
}
// Trigger rename on cloned node after adding to graph (onAdded might be better)
setTimeout(() => { if(cloned.graph) cloned.onRename(); }, 0);
return cloned;
};
this.onAdded = function(graph) {
// Ensure the node updates its type/title when added to the graph
// Delay to allow graph to finish loading connections
setTimeout(() => {
if (this.graph && this.onRename) {
this.onRename();
}
}, 100);
}
// Override onModeChange to trigger update (e.g., if GetNode itself is bypassed)
const originalOnModeChange = this.onModeChange;
this.onModeChange = function(mode) {
if (originalOnModeChange) {
originalOnModeChange.apply(this, arguments);
}
// LiteGraph.BYPASS == 4
debugLog(`GetOutputNode ${this.id} (${this.widgets[0].value}) mode changed to: ${mode}`);
// Trigger rename to re-evaluate connections/drawing
setTimeout(() => { if (this.onRename) this.onRename(); }, 50);
};
// --- Link Validation ---
this.validateLinks = function() {
if (!this.graph || !this.outputs || !this.outputs[0] || !this.outputs[0].links) {
return;
}
const outputType = this.outputs[0].type;
const nodeName = (this.widgets && this.widgets[0]) ? this.widgets[0].value : 'unnamed';
// Don't validate if output type is generic/wildcard
if (!outputType || outputType === '*' || outputType === '') {
debugLog(`GetOutputNode ${this.id} (${nodeName}): Skipping validation - output type is wildcard (${outputType})`);
return;
}
debugLog(`GetOutputNode ${this.id} (${nodeName}): Validating ${this.outputs[0].links.length} links with output type "${outputType}"`);
const linksToRemove = [];
for (const linkId of this.outputs[0].links) {
const link = this.graph.links[linkId];
if (link) {
const targetNode = this.graph.getNodeById(link.target_id);
if (targetNode && targetNode.inputs && targetNode.inputs[link.target_slot]) {
const targetType = targetNode.inputs[link.target_slot].type;
const targetNodeType = targetNode.type || 'unknown';
// Normalize types for comparison (case-insensitive, handle common variations)
const normalizedOutputType = (outputType || '').toUpperCase().trim();
const normalizedTargetType = (targetType || '').toUpperCase().trim();
// Only remove links if there's a CLEAR incompatibility:
// - Target type must be explicitly defined (not undefined/null/empty/'*')
// - Target type must NOT match output type (case-insensitive)
// - Treat undefined/null/empty/'*' as wildcards (compatible with anything)
if (targetType &&
normalizedTargetType !== '*' &&
normalizedTargetType !== '' &&
normalizedOutputType !== normalizedTargetType) {
console.warn(`GetOutputNode ${this.id} (${nodeName}): Removing incompatible link ${linkId} to ${targetNodeType} (Output: "${outputType}", Target: "${targetType}")`);
linksToRemove.push(linkId);
} else {
debugLog(`GetOutputNode ${this.id} (${nodeName}): Link ${linkId} to ${targetNodeType} is compatible (Output: "${outputType}", Target: "${targetType || '*'}")`);
}
} else {
debugWarn(`GetOutputNode ${this.id} (${nodeName}): Link ${linkId} - target node or input slot not found`);
}
}
}
if (linksToRemove.length > 0) {
console.warn(`GetOutputNode ${this.id} (${nodeName}): Removing ${linksToRemove.length} incompatible link(s)`);
linksToRemove.forEach(linkId => this.graph.removeLink(linkId));
} else {
debugLog(`GetOutputNode ${this.id} (${nodeName}): All links validated successfully`);
}
};
// --- Set Output Type ---
this.setType = function(type) {
if (this.outputs && this.outputs[0]) {
if (this.outputs[0].type !== type) {
this.outputs[0].name = type;
this.outputs[0].type = type;
this.validateLinks(); // Validate after type change
this.graph.links_dirty = true; // Notify graph
}
}
};
// --- Helper: Find Active Setter ---
this.findSetter = function(graph) {
if (!graph) return null;
const name = (this.widgets && this.widgets[0]) ? this.widgets[0].value : null;
if (!name || name === '') return null;
const matchingSetters = graph._nodes.filter(otherNode =>
otherNode.type === 'SetInputNode' && // Look for SetInputNode
otherNode.widgets && otherNode.widgets[0] &&
otherNode.widgets[0].value === name
);
// LiteGraph.BYPASS is mode 4, LiteGraph.NEVER (muted) is mode 2
const activeSetter = matchingSetters.find(setter =>
setter.mode !== 4 && setter.mode !== 2
);
return activeSetter || null; // Return the first non-bypassed, non-muted or null
};
// --- Navigation ---
this.goToSetter = function() {
if (!this.graph || !this.canvas) return;
const setter = this.findSetter(this.graph);
if (setter) {
this.canvas.centerOnNode(setter);
this.canvas.selectNode(setter, false);
} else {
debugLog("GetOutputNode: No active SetInput node found to go to.");
}
};
// Node is virtual
this.isVirtualNode = true;
} // End constructor
// --- Get Input Info (for Backend Execution) ---
// This tells LiteGraph where the data actually comes from.
getInputInfo(slot) {
// Ensure graph context
if (!this.graph) {
return null;
}
// Find the active (non-bypassed) setter node linked by name
const setter = this.findSetter(this.graph);
// Check if setter exists, has an input, and that input is connected
if (!setter || !setter.inputs || !setter.inputs[0] || setter.inputs[0].link === null) {
// console.log(`GetOutputNode ${this.id} (${this.widgets[0].value}): getInputInfo - No active setter or setter input link found.`);
return null; // No active linked input found
}
// Get the ID of the link connected to the setter's input
const linkId = setter.inputs[0].link;
const link = this.graph.links[linkId];
if (!link) {
console.error(`GetOutputNode ${this.id}: getInputInfo - Link with ID ${linkId} not found in graph.`);
return null; // Link not found (shouldn't happen in a consistent graph)
}
// Find the actual source node connected to the setter
const originNode = this.graph.getNodeById(link.origin_id);
if (!originNode) {
console.error(`GetOutputNode ${this.id}: getInputInfo - Origin node with ID ${link.origin_id} not found.`);
return null;
}
// Determine the output slot index and type from the origin node
const originSlotIndex = link.origin_slot;
const originOutputType = (originNode.outputs && originNode.outputs[originSlotIndex])
? originNode.outputs[originSlotIndex].type
: '*';
// Return the information about the actual data source
// console.log(`GetOutputNode ${this.id} (${this.widgets[0].value}): Providing input info from Setter ${setter.id} -> Origin Node ${originNode.id}, Slot ${originSlotIndex} (Type: ${originOutputType})`);
return {
node_id: originNode.id,
slot: originSlotIndex,
type: originOutputType
};
}
// --- Get Input Link (for internal LiteGraph/ComfyUI connection logic) ---
// This function might be queried by the execution logic to find the
// link object providing data *to* the node connected *from* this GetNode.
getInputLink(slot) {
// This node provides output on slot 0. If queried for other slots, return null.
if (slot !== 0) return null;
// Ensure graph context
if (!this.graph) {
return null;
}
// Find the active (non-bypassed) setter node linked by name
const setter = this.findSetter(this.graph);
// Check if setter exists, has an input, and that input is connected
if (!setter || !setter.inputs || !setter.inputs[0] || typeof setter.inputs[0].link !== 'number') {
// console.log(`GetOutputNode ${this.id} (${this.widgets[0].value}): getInputLink - No active setter or setter input link found.`);
return null; // No active linked input found
}
// Get the ID of the link connected to the setter's input
const linkId = setter.inputs[0].link;
const link = this.graph.links[linkId];
if (!link) {
console.error(`GetOutputNode ${this.id}: getInputLink - Link with ID ${linkId} not found in graph.`);
return null;
}
// Return the actual link object originating from the node connected to the SetNode.
// The execution logic might trace this link's origin_id/origin_slot.
// console.log(`GetOutputNode ${this.id} (${this.widgets[0].value}): getInputLink returning link ${linkId} (Origin: ${link.origin_id}, Slot: ${link.origin_slot})`);
return link;
}
// --- Context Menu ---
getExtraMenuOptions(_, options) {
let menuEntry = this.drawConnection ? "Hide connections" : "Show connections";
options.unshift(
{
content: "Convert to Set (Input)",
callback: () => {
convertNodeInPlace(this, "SetInputNode");
},
},
{
content: "Create Set (Input) to the right",
callback: () => {
createSiblingNodeToRight(this, "SetInputNode");
},
},
{
content: "Go to active setter",
callback: () => { this.goToSetter(); },
},
{
content: menuEntry,
callback: () => {
if (!this.graph) return;
this.currentSetter = this.findSetter(this.graph);
if (!this.currentSetter) return; // Only toggle if active setter found
let linkType = (this.currentSetter.inputs[0].type || '*');
this.slotColor = this.canvas?.default_connection_color_byType?.[linkType] || "#FFF";
this.drawConnection = !this.drawConnection;
menuEntry = this.drawConnection ? "Hide connections" : "Show connections"; // Update label
this.canvas?.setDirty(true, true);
},
}
);
}
// --- Drawing Connections ---
onDrawForeground(ctx, graphcanvas) {
if (this.flags.collapsed || !this.drawConnection || this.mode === 4 /* Bypassed */ || this.mode === 2 /* Muted */) {
return;
}
this.currentSetter = this.findSetter(this.graph);
if (!this.currentSetter) {
// Optional: Turn off drawing if no active setter found?
// this.drawConnection = false;
return;
}
if (!this.currentSetter.isVisibleOnCanvas(graphcanvas)) return; // Don't draw if offscreen
const lGraphCanvas = graphcanvas || app.canvas;
if (!lGraphCanvas) return;
ctx.save();
ctx.lineWidth = 1;
ctx.strokeStyle = this.slotColor || "#FFF";
// Start from the Get node's output
const startPos = this.localToGlobal([this.size[0], LiteGraph.NODE_SLOT_HEIGHT / 2]);
// End at the Set node's input
const endPos = this.currentSetter.localToGlobal([0, LiteGraph.NODE_SLOT_HEIGHT / 2]);
ctx.beginPath();
ctx.moveTo(startPos[0], startPos[1]);
ctx.bezierCurveTo(
startPos[0] + 100, startPos[1],
endPos[0] - 100, endPos[1],
endPos[0], endPos[1]
);
ctx.stroke();
ctx.restore();
}
} // End GetOutputNode Class
// --- Register Nodes with LiteGraph ---
LiteGraph.registerNodeType("SetInputNode", SetInputNode);
LiteGraph.registerNodeType("GetOutputNode", GetOutputNode);
// Set category for node menu organization
SetInputNode.category = "Utilities/InputOutput";
GetOutputNode.category = "Utilities/InputOutput";
debugLog("Input/Output Nodes Registered");
} // End registerCustomNodes
}); // End app.registerExtension
} // End else (LiteGraph found)