import { app } from "../../scripts/app.js"; // Helper function to find source through parent subgraph node function findSourceThroughParent(subgraphNode, inputSlotIndex, subgraph) { console.log("[FriendlyPipe] findSourceThroughParent called"); console.log("[FriendlyPipe] subgraphNode:", subgraphNode); console.log("[FriendlyPipe] subgraphNode.inputs:", subgraphNode?.inputs); console.log("[FriendlyPipe] inputSlotIndex:", inputSlotIndex); if (!subgraphNode || !subgraphNode.inputs) return null; // The origin_slot from the link inside the subgraph should map to the parent's input const parentInput = subgraphNode.inputs[inputSlotIndex]; console.log("[FriendlyPipe] parentInput:", parentInput); if (parentInput && parentInput.link) { const parentGraph = subgraphNode.graph || app.graph; const parentLink = parentGraph.links[parentInput.link]; console.log("[FriendlyPipe] parentLink:", parentLink); if (parentLink) { // Recursively handle if parent is also in a subgraph if (parentLink.origin_id < 0) { // Parent connection also comes from a subgraph boundary const grandparentNode = parentGraph._subgraph_node; if (grandparentNode) { return findSourceThroughParent(grandparentNode, parentLink.origin_slot, parentGraph); } } const source = parentGraph.getNodeById(parentLink.origin_id); console.log("[FriendlyPipe] Found source:", source); return source; } } return null; } // Helper function to check if a node is a pass-through type function isPassThroughNode(node) { if (!node) return false; // Known reroute types if (node.type === "Reroute" || node.type === "ReroutePrimitive" || node.type === "PrimitiveNode") { return true; } // Subgraph input/output nodes (various possible type names) if (node.type === "graph/input" || node.type === "graph/output" || node.type === "GraphInput" || node.type === "GraphOutput") { return true; } // Generic pass-through detection (1 input, 1 output, no custom slot handling) if (node.inputs && node.inputs.length === 1 && node.outputs && node.outputs.length === 1 && node.slotCount === undefined) { return true; } return false; } // Helper to find the parent subgraph node when inside a subgraph function getParentSubgraphInfo(node) { const graph = node.graph; if (!graph) return null; // Try various ways to get the parent subgraph node let parentNode = graph._subgraph_node || graph.parentNode || graph._parentNode; let parentGraph = parentNode?.graph || app.graph; // If we're in a subgraph, the graph might have a reference to its container if (!parentNode && graph._is_subgraph) { // Search main graph for subgraph nodes containing this graph for (const n of app.graph._nodes || []) { if (n.subgraph === graph) { parentNode = n; parentGraph = app.graph; break; } } } if (parentNode) { return { parentNode, parentGraph }; } return null; } // Helper function to traverse backwards through reroute/forwarding nodes to find the original source function findOriginalSource(node, slotIndex, depth = 0) { // Prevent infinite recursion if (depth > 50) return null; const visited = new Set(); let currentNode = node; let currentSlot = slotIndex; while (currentNode) { const graph = currentNode.graph || app.graph; // Prevent infinite loops const nodeKey = `${currentNode.id}-${currentSlot}-${graph?.id || 'main'}`; if (visited.has(nodeKey)) break; visited.add(nodeKey); // Check if this node has the properties we're looking for (FriendlyPipeIn or FriendlyPipeOut) if (currentNode.slotCount !== undefined && currentNode.slotNames !== undefined) { return currentNode; } // Handle graph/input nodes (subgraph boundary) - need to exit to parent graph if (currentNode.type === "graph/input" || currentNode.type === "GraphInput") { const parentInfo = getParentSubgraphInfo(currentNode); if (parentInfo) { const { parentNode, parentGraph } = parentInfo; // Find which input slot on parent corresponds to this graph/input const inputIndex = currentNode.properties?.slot_index ?? currentNode.properties?.index ?? currentNode.slot_index ?? 0; const parentInput = parentNode.inputs?.[inputIndex]; if (parentInput && parentInput.link) { const link = parentGraph.links[parentInput.link]; if (link) { const sourceNode = parentGraph.getNodeById(link.origin_id); if (sourceNode) { // Recursively search from parent graph const result = findOriginalSource(sourceNode, link.origin_slot, depth + 1); if (result) return result; } } } } break; } // Check if this is a pass-through node if (isPassThroughNode(currentNode)) { const inputSlot = currentNode.inputs?.[0]; if (inputSlot && inputSlot.link) { const link = graph.links[inputSlot.link]; if (link) { currentNode = graph.getNodeById(link.origin_id); currentSlot = link.origin_slot; continue; } } } // Check if this node has an input link we can follow (for non-passthrough nodes) const input = currentNode.inputs?.[currentSlot] || currentNode.inputs?.[0]; if (input && input.link) { const link = graph.links[input.link]; if (link) { currentNode = graph.getNodeById(link.origin_id); currentSlot = link.origin_slot; continue; } } // Not a pass-through or no more links to follow break; } return null; } // Helper function to traverse forwards through reroute/forwarding nodes to notify all connected outputs function notifyDownstreamNodes(node, slotIndex, visited = new Set(), depth = 0) { // Prevent infinite recursion if (depth > 50) return; const graph = node.graph || app.graph; if (!node.outputs || !node.outputs[slotIndex] || !node.outputs[slotIndex].links) return; for (const linkId of node.outputs[slotIndex].links) { const link = graph.links[linkId]; if (!link) continue; const targetNode = graph.getNodeById(link.target_id); if (!targetNode) continue; // Prevent infinite loops const nodeKey = `${targetNode.id}-${graph.id || 'main'}`; if (visited.has(nodeKey)) continue; visited.add(nodeKey); // If target has syncWithSource, call it if (targetNode.syncWithSource) { targetNode.syncWithSource(); } // Handle Subgraph nodes - enter the subgraph and notify from graph/input if (targetNode.subgraph) { const subgraph = targetNode.subgraph; const targetInputSlot = link.target_slot; const subgraphNodes = subgraph._nodes || []; for (const innerNode of subgraphNodes) { // Find the graph/input that corresponds to this input slot if (innerNode.type === "graph/input" || innerNode.type === "GraphInput") { const inputIndex = innerNode.properties?.slot_index ?? innerNode.properties?.index ?? innerNode.slot_index ?? 0; if (inputIndex === targetInputSlot) { notifyDownstreamNodes(innerNode, 0, visited, depth + 1); } } // Also directly notify FriendlyPipeOut nodes inside if (innerNode.syncWithSource) { innerNode.syncWithSource(); } } } // If target is a pass-through, continue traversing if (isPassThroughNode(targetNode) && targetNode.outputs) { notifyDownstreamNodes(targetNode, 0, visited, depth + 1); } } } app.registerExtension({ name: "Comfy.FriendlyPipe", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "FriendlyPipeIn") { setupFriendlyPipeIn(nodeType, nodeData, app); } if (nodeData.name === "FriendlyPipeOut") { setupFriendlyPipeOut(nodeType, nodeData, app); } if (nodeData.name === "FriendlyPipeEdit") { setupFriendlyPipeEdit(nodeType, nodeData, app); } } }); function setupFriendlyPipeIn(nodeType, nodeData, app) { const origOnNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { if (origOnNodeCreated) { origOnNodeCreated.apply(this, arguments); } const node = this; // Initialize slot count, names, and types this.slotCount = 1; this.slotNames = { 1: "slot_1" }; this.slotTypes = {}; // Remove all inputs except the first one while (this.inputs && this.inputs.length > 1) { this.removeInput(this.inputs.length - 1); } // Rename first input if (this.inputs && this.inputs.length > 0) { this.inputs[0].label = this.slotNames[1]; } // Add name widget for slot 1 first (above buttons) this.addSlotNameWidget(1); // Add control buttons const addWidget = this.addWidget("button", "➕ Add Slot", null, () => { if (node.slotCount < 80) { node.slotCount++; const defaultName = "slot_" + node.slotCount; node.slotNames[node.slotCount] = defaultName; // Add new input slot node.addInput("slot_" + node.slotCount, "*"); if (node.inputs && node.inputs.length > 0) { node.inputs[node.inputs.length - 1].label = defaultName; } // Add name widget for the new slot (will be inserted before buttons) node.addSlotNameWidget(node.slotCount); node.updateSize(); node.notifyConnectedOutputs(); node.setDirtyCanvas(true, true); } }); addWidget.serialize = false; const removeWidget = this.addWidget("button", "➖ Remove Slot", null, () => { if (node.slotCount > 1) { // Remove the name widget node.removeSlotNameWidget(node.slotCount); // Remove the input slot node.removeInput(node.inputs.length - 1); delete node.slotNames[node.slotCount]; node.slotCount--; node.updateSize(); node.notifyConnectedOutputs(); node.setDirtyCanvas(true, true); } }); removeWidget.serialize = false; // Store button references so we can insert widgets before them this.addButton = addWidget; this.removeButton = removeWidget; this.updateSize(); }; nodeType.prototype.addSlotNameWidget = function(slotNum) { const node = this; const defaultName = node.slotNames[slotNum] || ("slot_" + slotNum); const nameWidget = this.addWidget("text", "Label " + slotNum, defaultName, (value) => { node.slotNames[slotNum] = value; // Update the input label const inputIndex = slotNum - 1; if (node.inputs && node.inputs[inputIndex]) { node.inputs[inputIndex].label = value; } node.notifyConnectedOutputs(); node.setDirtyCanvas(true, true); }); nameWidget.slotNum = slotNum; // Move the widget before the buttons if they exist if (this.widgets && this.addButton) { const widgetIndex = this.widgets.indexOf(nameWidget); const addButtonIndex = this.widgets.indexOf(this.addButton); if (widgetIndex > addButtonIndex && addButtonIndex >= 0) { // Remove from current position and insert before buttons this.widgets.splice(widgetIndex, 1); this.widgets.splice(addButtonIndex, 0, nameWidget); } } }; nodeType.prototype.removeSlotNameWidget = function(slotNum) { if (this.widgets) { const widgetIndex = this.widgets.findIndex(w => w.slotNum === slotNum); if (widgetIndex >= 0) { this.widgets.splice(widgetIndex, 1); } } }; nodeType.prototype.notifyConnectedOutputs = function() { // Find all nodes connected to our output and notify them (traversing through reroutes) if (!this.outputs || !this.outputs[0] || !this.outputs[0].links) return; notifyDownstreamNodes(this, 0, new Set([`${this.id}`])); }; nodeType.prototype.updateSize = function() { this.setSize(this.computeSize()); }; // Handle connection changes to track types const origOnConnectionsChange = nodeType.prototype.onConnectionsChange; nodeType.prototype.onConnectionsChange = function(type, index, connected, linkInfo) { if (origOnConnectionsChange) { origOnConnectionsChange.apply(this, arguments); } // When an input connection changes, update the type if (type === 1) { // Input connection this.updateSlotTypes(); this.notifyConnectedOutputs(); } }; nodeType.prototype.updateSlotTypes = function() { this.slotTypes = {}; if (!this.inputs) return; for (let i = 0; i < this.inputs.length; i++) { const input = this.inputs[i]; if (input && input.link) { const link = app.graph.links[input.link]; if (link) { const sourceNode = app.graph.getNodeById(link.origin_id); if (sourceNode && sourceNode.outputs && sourceNode.outputs[link.origin_slot]) { const outputType = sourceNode.outputs[link.origin_slot].type; this.slotTypes[i + 1] = outputType; } } } } }; // Handle serialization const origOnSerialize = nodeType.prototype.onSerialize; nodeType.prototype.onSerialize = function(o) { if (origOnSerialize) { origOnSerialize.apply(this, arguments); } o.slotCount = this.slotCount; o.slotNames = this.slotNames; o.slotTypes = this.slotTypes; }; // Handle deserialization const origOnConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function(o) { if (origOnConfigure) { origOnConfigure.apply(this, arguments); } const node = this; if (o.slotNames) { this.slotNames = o.slotNames; } if (o.slotTypes) { this.slotTypes = o.slotTypes; } if (o.slotCount !== undefined && o.slotCount > 1) { // We already have 1 slot from onNodeCreated // Add the remaining slots for (let i = 2; i <= o.slotCount; i++) { this.slotCount = i; const name = this.slotNames[i] || ("slot_" + i); this.slotNames[i] = name; // Add input if needed if (!this.inputs || this.inputs.length < i) { this.addInput("slot_" + i, "*"); } if (this.inputs && this.inputs[i - 1]) { this.inputs[i - 1].label = name; } // Add name widget this.addSlotNameWidget(i); } } // Update all input labels if (this.inputs) { for (let i = 0; i < this.inputs.length; i++) { const slotNum = i + 1; if (this.slotNames[slotNum]) { this.inputs[i].label = this.slotNames[slotNum]; } } } // Update name widget values if (this.widgets) { for (const widget of this.widgets) { if (widget.slotNum !== undefined && this.slotNames[widget.slotNum]) { widget.value = this.slotNames[widget.slotNum]; } } } this.updateSize(); }; // Override getExtraMenuOptions to update hidden values before execution const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = function(canvas, options) { if (origGetExtraMenuOptions) { origGetExtraMenuOptions.apply(this, arguments); } }; // Override onExecute or similar to pass slot info const origOnExecutionStart = nodeType.prototype.onExecutionStart; nodeType.prototype.onExecutionStart = function() { if (origOnExecutionStart) { origOnExecutionStart.apply(this, arguments); } // Update widgets for execution if (this.widgets) { const slotCountWidget = this.widgets.find(w => w.name === "slot_count"); if (slotCountWidget) { slotCountWidget.value = this.slotCount; } const slotNamesWidget = this.widgets.find(w => w.name === "slot_names"); if (slotNamesWidget) { slotNamesWidget.value = JSON.stringify(this.slotNames); } } }; } function setupFriendlyPipeOut(nodeType, nodeData, app) { const origOnNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { if (origOnNodeCreated) { origOnNodeCreated.apply(this, arguments); } const node = this; // Initialize this.slotCount = 1; this.slotNames = {}; this.slotTypes = {}; // Remove all outputs except the first one while (this.outputs && this.outputs.length > 1) { this.removeOutput(this.outputs.length - 1); } // Rename first output if (this.outputs && this.outputs.length > 0) { this.outputs[0].label = "slot_1"; } this.updateSize(); }; nodeType.prototype.updateSize = function() { this.setSize(this.computeSize()); }; nodeType.prototype.updateFromSource = function(slotCount, slotNames, slotTypes) { const node = this; // Update outputs to match source const targetCount = slotCount || 1; const names = slotNames || {}; const types = slotTypes || {}; // Add or remove outputs as needed while (this.outputs && this.outputs.length < targetCount) { const num = this.outputs.length + 1; const slotType = types[num] || "*"; this.addOutput("slot_" + num, slotType); } while (this.outputs && this.outputs.length > targetCount) { this.removeOutput(this.outputs.length - 1); } // Update labels and types if (this.outputs) { for (let i = 0; i < this.outputs.length; i++) { const slotNum = i + 1; this.outputs[i].label = names[slotNum] || ("slot_" + slotNum); this.outputs[i].type = types[slotNum] || "*"; } } this.slotCount = targetCount; this.slotNames = names; this.slotTypes = types; this.updateSize(); this.setDirtyCanvas(true, true); }; // Handle connection changes const origOnConnectionsChange = nodeType.prototype.onConnectionsChange; nodeType.prototype.onConnectionsChange = function(type, index, connected, linkInfo) { if (origOnConnectionsChange) { origOnConnectionsChange.apply(this, arguments); } // When input connection changes if (type === 1) { this.syncWithSource(); } }; nodeType.prototype.syncWithSource = function() { const graph = this.graph || app.graph; console.log("[FriendlyPipe] syncWithSource called on node", this.id); console.log("[FriendlyPipe] this.graph:", this.graph); console.log("[FriendlyPipe] this.inputs:", this.inputs); if (!this.inputs || !this.inputs[0] || !this.inputs[0].link) { console.log("[FriendlyPipe] No connection, resetting to default"); // No connection, reset to default this.updateFromSource(1, {}, {}); return; } const linkId = this.inputs[0].link; console.log("[FriendlyPipe] linkId:", linkId); const link = graph.links[linkId]; console.log("[FriendlyPipe] link:", link); if (!link) { console.log("[FriendlyPipe] Link not found in graph.links"); return; } let immediateSource = null; let originSlot = link.origin_slot; // Handle negative origin_id (subgraph input boundary) if (link.origin_id < 0) { console.log("[FriendlyPipe] Negative origin_id detected, this is a subgraph input"); console.log("[FriendlyPipe] graph._subgraph_node:", graph._subgraph_node); console.log("[FriendlyPipe] graph.inputs:", graph.inputs); console.log("[FriendlyPipe] graph._inputs:", graph._inputs); console.log("[FriendlyPipe] graph.config:", graph.config); console.log("[FriendlyPipe] All graph keys:", Object.keys(graph)); // In LiteGraph, the subgraph stores input info in graph.inputs array // The negative ID maps to the input: -1 = first input, -2 = second, etc. // But sometimes it's offset, so let's try multiple approaches const subgraphNode = graph._subgraph_node; // Also try to find parent through other means if (!subgraphNode) { console.log("[FriendlyPipe] No _subgraph_node, searching app.graph for parent..."); // Search all graphs for a subgraph containing this graph for (const node of app.graph._nodes || []) { if (node.subgraph === graph) { console.log("[FriendlyPipe] Found parent node:", node); immediateSource = findSourceThroughParent(node, link.origin_slot, graph); if (immediateSource) { originSlot = 0; // Will be set by findSourceThroughParent break; } } } } if (subgraphNode) { immediateSource = findSourceThroughParent(subgraphNode, link.origin_slot, graph); } // Try using the graph.inputs array which defines subgraph inputs if (!immediateSource && graph.inputs) { console.log("[FriendlyPipe] Trying graph.inputs array"); // Find the input definition that matches our slot const inputDef = graph.inputs[link.origin_slot]; console.log("[FriendlyPipe] inputDef:", inputDef); } } else { immediateSource = graph.getNodeById(link.origin_id); } console.log("[FriendlyPipe] immediateSource:", immediateSource); console.log("[FriendlyPipe] immediateSource.type:", immediateSource?.type); if (!immediateSource) { console.log("[FriendlyPipe] No immediate source found"); return; } // Traverse through reroute/subgraph nodes to find the original FriendlyPipeIn or FriendlyPipeEdit const sourceNode = findOriginalSource(immediateSource, originSlot); console.log("[FriendlyPipe] sourceNode from traversal:", sourceNode); // Use the found source or fall back to immediate source const effectiveSource = sourceNode || immediateSource; if (effectiveSource && effectiveSource.slotCount !== undefined) { console.log("[FriendlyPipe] Found source with slotCount:", effectiveSource.slotCount); // Check if this is a FriendlyPipeEdit (has getTotalSlotCount method) if (effectiveSource.getTotalSlotCount) { console.log("[FriendlyPipe] Source is FriendlyPipeEdit, getting combined slots"); // Make sure source has latest types if (effectiveSource.updateSlotTypes) { effectiveSource.updateSlotTypes(); } this.updateFromSource( effectiveSource.getTotalSlotCount(), effectiveSource.getCombinedSlotNames(), effectiveSource.getCombinedSlotTypes() ); } else { // FriendlyPipeIn or other source if (effectiveSource.updateSlotTypes) { effectiveSource.updateSlotTypes(); } this.updateFromSource( effectiveSource.slotCount, effectiveSource.slotNames || {}, effectiveSource.slotTypes || {} ); } } else { console.log("[FriendlyPipe] No valid source found"); } }; // Handle serialization const origOnSerialize = nodeType.prototype.onSerialize; nodeType.prototype.onSerialize = function(o) { if (origOnSerialize) { origOnSerialize.apply(this, arguments); } o.slotCount = this.slotCount; o.slotNames = this.slotNames; o.slotTypes = this.slotTypes; }; // Handle deserialization const origOnConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function(o) { if (origOnConfigure) { origOnConfigure.apply(this, arguments); } if (o.slotCount !== undefined) { this.updateFromSource(o.slotCount, o.slotNames || {}, o.slotTypes || {}); } }; } function setupFriendlyPipeEdit(nodeType, nodeData, app) { const origOnNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { if (origOnNodeCreated) { origOnNodeCreated.apply(this, arguments); } const node = this; // Initialize slot count for additional slots (not including incoming pipe slots) this.slotCount = 1; this.slotNames = { 1: "slot_1" }; this.slotTypes = {}; // Track incoming pipe info this.incomingSlotCount = 0; this.incomingSlotNames = {}; // Remove all optional inputs except the first one // Keep the pipe input (index 0) and first slot input (index 1) while (this.inputs && this.inputs.length > 2) { this.removeInput(this.inputs.length - 1); } // Rename first additional slot if (this.inputs && this.inputs.length > 1) { this.inputs[1].label = this.slotNames[1]; } // Add name widget for slot 1 first (above buttons) this.addSlotNameWidget(1); // Add control buttons const addWidget = this.addWidget("button", "➕ Add Slot", null, () => { if (node.slotCount < 80) { node.slotCount++; const defaultName = "slot_" + node.slotCount; node.slotNames[node.slotCount] = defaultName; // Add new input slot node.addInput("slot_" + node.slotCount, "*"); if (node.inputs && node.inputs.length > 0) { node.inputs[node.inputs.length - 1].label = defaultName; } // Add name widget for the new slot (will be inserted before buttons) node.addSlotNameWidget(node.slotCount); node.updateSize(); node.notifyConnectedOutputs(); node.setDirtyCanvas(true, true); } }); addWidget.serialize = false; const removeWidget = this.addWidget("button", "➖ Remove Slot", null, () => { if (node.slotCount > 1) { // Remove the name widget node.removeSlotNameWidget(node.slotCount); // Remove the input slot node.removeInput(node.inputs.length - 1); delete node.slotNames[node.slotCount]; node.slotCount--; node.updateSize(); node.notifyConnectedOutputs(); node.setDirtyCanvas(true, true); } }); removeWidget.serialize = false; // Store button references so we can insert widgets before them this.addButton = addWidget; this.removeButton = removeWidget; this.updateSize(); }; nodeType.prototype.addSlotNameWidget = function(slotNum) { const node = this; const defaultName = node.slotNames[slotNum] || ("slot_" + slotNum); const nameWidget = this.addWidget("text", "Label " + slotNum, defaultName, (value) => { node.slotNames[slotNum] = value; // Update the input label (offset by 1 for pipe input) const inputIndex = slotNum; // slotNum maps directly since pipe is at 0 if (node.inputs && node.inputs[inputIndex]) { node.inputs[inputIndex].label = value; } node.notifyConnectedOutputs(); node.setDirtyCanvas(true, true); }); nameWidget.slotNum = slotNum; // Move the widget before the buttons if they exist if (this.widgets && this.addButton) { const widgetIndex = this.widgets.indexOf(nameWidget); const addButtonIndex = this.widgets.indexOf(this.addButton); if (widgetIndex > addButtonIndex && addButtonIndex >= 0) { // Remove from current position and insert before buttons this.widgets.splice(widgetIndex, 1); this.widgets.splice(addButtonIndex, 0, nameWidget); } } }; nodeType.prototype.removeSlotNameWidget = function(slotNum) { if (this.widgets) { const widgetIndex = this.widgets.findIndex(w => w.slotNum === slotNum); if (widgetIndex >= 0) { this.widgets.splice(widgetIndex, 1); } } }; nodeType.prototype.notifyConnectedOutputs = function() { // Find all nodes connected to our output and notify them (traversing through reroutes) if (!this.outputs || !this.outputs[0] || !this.outputs[0].links) return; notifyDownstreamNodes(this, 0, new Set([`${this.id}`])); }; nodeType.prototype.updateSize = function() { this.setSize(this.computeSize()); }; // Handle connection changes to sync with incoming pipe const origOnConnectionsChange = nodeType.prototype.onConnectionsChange; nodeType.prototype.onConnectionsChange = function(type, index, connected, linkInfo) { if (origOnConnectionsChange) { origOnConnectionsChange.apply(this, arguments); } // When pipe input (index 0) connection changes, sync with source if (type === 1 && index === 0) { this.syncWithSource(); } // When any input connection changes, update types and notify downstream if (type === 1) { this.updateSlotTypes(); this.notifyConnectedOutputs(); } }; nodeType.prototype.syncWithSource = function() { const graph = this.graph || app.graph; if (!this.inputs || !this.inputs[0] || !this.inputs[0].link) { // No pipe connection, reset incoming info this.incomingSlotCount = 0; this.incomingSlotNames = {}; this.notifyConnectedOutputs(); return; } const linkId = this.inputs[0].link; const link = graph.links[linkId]; if (!link) return; let sourceNode = null; let originSlot = link.origin_slot; // Handle negative origin_id (subgraph input boundary) if (link.origin_id < 0) { const subgraphNode = graph._subgraph_node; if (subgraphNode) { sourceNode = findSourceThroughParent(subgraphNode, link.origin_slot, graph); } else { // Search for parent subgraph node for (const node of app.graph._nodes || []) { if (node.subgraph === graph) { sourceNode = findSourceThroughParent(node, link.origin_slot, graph); break; } } } } else { sourceNode = graph.getNodeById(link.origin_id); } if (!sourceNode) return; // Traverse through reroute nodes to find the original pipe source const originalSource = findOriginalSource(sourceNode, originSlot); const effectiveSource = originalSource || sourceNode; // Get combined slot info from source (could be FriendlyPipeIn or another FriendlyPipeEdit) if (effectiveSource.slotCount !== undefined) { // For FriendlyPipeEdit, we need to get the combined count if (effectiveSource.getTotalSlotCount) { this.incomingSlotCount = effectiveSource.getTotalSlotCount(); this.incomingSlotNames = effectiveSource.getCombinedSlotNames(); } else { this.incomingSlotCount = effectiveSource.slotCount; this.incomingSlotNames = effectiveSource.slotNames || {}; } } this.notifyConnectedOutputs(); }; // Get total slot count (incoming + our additional slots) nodeType.prototype.getTotalSlotCount = function() { return this.incomingSlotCount + this.slotCount; }; // Get combined slot names (incoming names + our additional names with offset indices) nodeType.prototype.getCombinedSlotNames = function() { const combined = {}; // Copy incoming slot names for (const [key, value] of Object.entries(this.incomingSlotNames)) { combined[key] = value; } // Add our slots with offset indices for (let i = 1; i <= this.slotCount; i++) { const newIndex = this.incomingSlotCount + i; combined[newIndex] = this.slotNames[i] || ("slot_" + i); } return combined; }; // Get combined slot types nodeType.prototype.getCombinedSlotTypes = function() { const combined = {}; // Types from incoming pipe would be tracked by the FriendlyPipeIn // Our additional slot types for (let i = 1; i <= this.slotCount; i++) { const newIndex = this.incomingSlotCount + i; if (this.slotTypes[i]) { combined[newIndex] = this.slotTypes[i]; } } return combined; }; nodeType.prototype.updateSlotTypes = function() { this.slotTypes = {}; if (!this.inputs) return; // Start from index 1 to skip the pipe input for (let i = 1; i < this.inputs.length; i++) { const input = this.inputs[i]; if (input && input.link) { const link = app.graph.links[input.link]; if (link) { const sourceNode = app.graph.getNodeById(link.origin_id); if (sourceNode && sourceNode.outputs && sourceNode.outputs[link.origin_slot]) { const outputType = sourceNode.outputs[link.origin_slot].type; this.slotTypes[i] = outputType; // i corresponds to slot number here } } } } }; // Handle serialization const origOnSerialize = nodeType.prototype.onSerialize; nodeType.prototype.onSerialize = function(o) { if (origOnSerialize) { origOnSerialize.apply(this, arguments); } o.slotCount = this.slotCount; o.slotNames = this.slotNames; o.slotTypes = this.slotTypes; }; // Handle deserialization const origOnConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function(o) { if (origOnConfigure) { origOnConfigure.apply(this, arguments); } const node = this; if (o.slotNames) { this.slotNames = o.slotNames; } if (o.slotTypes) { this.slotTypes = o.slotTypes; } if (o.slotCount !== undefined && o.slotCount > 1) { // We already have 1 slot from onNodeCreated // Add the remaining slots for (let i = 2; i <= o.slotCount; i++) { this.slotCount = i; const name = this.slotNames[i] || ("slot_" + i); this.slotNames[i] = name; // Add input if needed (offset by 1 for pipe input) if (!this.inputs || this.inputs.length < i + 1) { this.addInput("slot_" + i, "*"); } if (this.inputs && this.inputs[i]) { this.inputs[i].label = name; } // Add name widget this.addSlotNameWidget(i); } } // Update all input labels if (this.inputs) { for (let i = 1; i < this.inputs.length; i++) { const slotNum = i; if (this.slotNames[slotNum]) { this.inputs[i].label = this.slotNames[slotNum]; } } } // Update name widget values if (this.widgets) { for (const widget of this.widgets) { if (widget.slotNum !== undefined && this.slotNames[widget.slotNum]) { widget.value = this.slotNames[widget.slotNum]; } } } this.updateSize(); // Sync with source after configure setTimeout(() => { this.syncWithSource(); }, 100); }; }