// 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)