1479 lines
59 KiB
JavaScript
1479 lines
59 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
||
|
||
// Default configuration
|
||
const defaultConfig = {
|
||
debug: false,
|
||
};
|
||
|
||
// Try to load config, fall back to defaults if not found
|
||
let config = defaultConfig;
|
||
try {
|
||
const configModule = await import("./config.js");
|
||
config = { ...defaultConfig, ...configModule.config };
|
||
} catch (e) {
|
||
// config.js doesn't exist, use defaults
|
||
}
|
||
|
||
// Debug logging helper
|
||
function debugLog(...args) {
|
||
if (config.debug) {
|
||
console.log("[FriendlyPipe]", ...args);
|
||
}
|
||
}
|
||
|
||
// Helper function to find source through parent subgraph node
|
||
// Returns { node, slot } or null
|
||
function findSourceThroughParent(subgraphNode, inputSlotIndex, subgraph) {
|
||
debugLog("findSourceThroughParent called");
|
||
debugLog("subgraphNode:", subgraphNode);
|
||
debugLog("subgraphNode.inputs:", subgraphNode?.inputs);
|
||
debugLog("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];
|
||
debugLog("parentInput:", parentInput);
|
||
|
||
if (parentInput && parentInput.link) {
|
||
const parentGraph = subgraphNode.graph || app.graph;
|
||
const parentLink = parentGraph.links[parentInput.link];
|
||
debugLog("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);
|
||
debugLog("Found source:", source, "slot:", parentLink.origin_slot);
|
||
return { node: source, slot: parentLink.origin_slot };
|
||
}
|
||
}
|
||
|
||
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;
|
||
|
||
debugLog("getParentSubgraphInfo for node:", node.type, node.id);
|
||
debugLog("graph._subgraph_node:", graph._subgraph_node);
|
||
debugLog("graph.parentNode:", graph.parentNode);
|
||
|
||
// 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) {
|
||
// Search all graphs recursively for a subgraph node containing this graph
|
||
const searchForParent = (searchGraph, depth = 0) => {
|
||
if (depth > 10) return null;
|
||
|
||
for (const n of searchGraph._nodes || []) {
|
||
if (n.subgraph === graph) {
|
||
debugLog("Found parent node in graph at depth", depth, ":", n);
|
||
return { parentNode: n, parentGraph: searchGraph };
|
||
}
|
||
// If this node has a subgraph, search inside it too
|
||
if (n.subgraph) {
|
||
const result = searchForParent(n.subgraph, depth + 1);
|
||
if (result) return result;
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const result = searchForParent(app.graph);
|
||
if (result) {
|
||
parentNode = result.parentNode;
|
||
parentGraph = result.parentGraph;
|
||
}
|
||
}
|
||
|
||
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 FriendlyPipeEdit)
|
||
if (currentNode.slotCount !== undefined && currentNode.slotNames !== undefined) {
|
||
debugLog("findOriginalSource: Found source node:", currentNode.type, currentNode.id);
|
||
return currentNode;
|
||
}
|
||
|
||
// Handle Subgraph nodes - need to enter the subgraph and find the source connected to the output
|
||
if (currentNode.subgraph) {
|
||
debugLog("findOriginalSource: Entering subgraph node, outputSlot:", currentSlot);
|
||
const subgraph = currentNode.subgraph;
|
||
const outputSlot = currentSlot;
|
||
|
||
// Check if subgraph.outputs defines the output mappings (ComfyUI style)
|
||
if (subgraph.outputs && subgraph.outputs[outputSlot]) {
|
||
const outputDef = subgraph.outputs[outputSlot];
|
||
debugLog("findOriginalSource: outputDef:", outputDef);
|
||
|
||
// SubgraphOutput has linkIds array containing internal link IDs
|
||
const linkIds = outputDef.linkIds || outputDef.links || [];
|
||
debugLog("findOriginalSource: linkIds:", linkIds);
|
||
|
||
if (linkIds.length > 0) {
|
||
const innerLinkId = linkIds[0]; // Take the first link
|
||
const innerLink = subgraph.links[innerLinkId];
|
||
debugLog("findOriginalSource: inner link:", innerLink);
|
||
if (innerLink) {
|
||
const innerSource = subgraph.getNodeById(innerLink.origin_id);
|
||
debugLog("findOriginalSource: inner source:", innerSource?.type, innerSource?.id);
|
||
if (innerSource) {
|
||
const result = findOriginalSource(innerSource, innerLink.origin_slot, depth + 1);
|
||
debugLog("findOriginalSource: recursive result:", result?.type, result?.id);
|
||
if (result) return result;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Also check for a single 'link' property
|
||
if (outputDef.link !== undefined && outputDef.link !== null) {
|
||
const innerLink = subgraph.links[outputDef.link];
|
||
debugLog("findOriginalSource: inner link from .link:", innerLink);
|
||
if (innerLink) {
|
||
const innerSource = subgraph.getNodeById(innerLink.origin_id);
|
||
if (innerSource) {
|
||
const result = findOriginalSource(innerSource, innerLink.origin_slot, depth + 1);
|
||
if (result) return result;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fallback: search for graph/output nodes (LiteGraph style)
|
||
const subgraphNodes = subgraph._nodes || [];
|
||
for (const innerNode of subgraphNodes) {
|
||
if (innerNode.type === "graph/output" || innerNode.type === "GraphOutput") {
|
||
const outputIndex = innerNode.properties?.slot_index ??
|
||
innerNode.properties?.index ??
|
||
innerNode.slot_index ?? 0;
|
||
if (outputIndex === outputSlot) {
|
||
const graphOutputInput = innerNode.inputs?.[0];
|
||
if (graphOutputInput && graphOutputInput.link) {
|
||
const innerLink = subgraph.links[graphOutputInput.link];
|
||
if (innerLink) {
|
||
const innerSource = subgraph.getNodeById(innerLink.origin_id);
|
||
if (innerSource) {
|
||
const result = findOriginalSource(innerSource, innerLink.origin_slot, depth + 1);
|
||
if (result) return result;
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
debugLog("findOriginalSource: No source found in subgraph");
|
||
break;
|
||
}
|
||
|
||
// 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;
|
||
|
||
debugLog("notifyDownstreamNodes called:", {
|
||
nodeType: node.type,
|
||
nodeId: node.id,
|
||
slotIndex,
|
||
depth,
|
||
hasOutputs: !!node.outputs,
|
||
outputSlot: node.outputs?.[slotIndex],
|
||
links: node.outputs?.[slotIndex]?.links
|
||
});
|
||
|
||
if (!node.outputs || !node.outputs[slotIndex] || !node.outputs[slotIndex].links) return;
|
||
|
||
for (const linkId of node.outputs[slotIndex].links) {
|
||
// Try to find the link in the current graph first, then fall back to app.graph
|
||
let link = graph.links instanceof Map ? graph.links.get(linkId) : graph.links?.[linkId];
|
||
if (!link && graph !== app.graph) {
|
||
// Link might be in the parent/root graph for cross-subgraph connections
|
||
link = app.graph.links instanceof Map ? app.graph.links.get(linkId) : app.graph.links?.[linkId];
|
||
}
|
||
debugLog("Processing linkId:", linkId, "link:", link);
|
||
if (!link) continue;
|
||
|
||
// Handle negative target_id (subgraph output boundary)
|
||
if (link.target_id < 0) {
|
||
debugLog("Negative target_id detected - subgraph output boundary");
|
||
// This connection goes to a subgraph output
|
||
// We need to find the parent subgraph node and notify nodes connected to its output
|
||
const parentInfo = getParentSubgraphInfo(node);
|
||
if (parentInfo) {
|
||
const { parentNode, parentGraph } = parentInfo;
|
||
// The target_slot on a subgraph output boundary maps to the parent's output slot
|
||
// But we need to figure out which output slot on the parent corresponds to this
|
||
const outputSlotOnParent = link.target_slot;
|
||
debugLog("Found parent subgraph, notifying from output slot:", outputSlotOnParent);
|
||
notifyDownstreamNodes(parentNode, outputSlotOnParent, visited, depth + 1);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Try to find the target node in the current graph first, then app.graph
|
||
let targetNode = graph.getNodeById(link.target_id);
|
||
if (!targetNode && graph !== app.graph) {
|
||
targetNode = app.graph.getNodeById(link.target_id);
|
||
}
|
||
debugLog("link.target_id:", link.target_id, "targetNode:", targetNode);
|
||
if (!targetNode) continue;
|
||
|
||
debugLog("Found target node:", {
|
||
targetType: targetNode.type,
|
||
targetId: targetNode.id,
|
||
hasSyncWithSource: !!targetNode.syncWithSource
|
||
});
|
||
|
||
// 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) {
|
||
debugLog("Calling syncWithSource on", targetNode.type, targetNode.id);
|
||
targetNode.syncWithSource();
|
||
}
|
||
|
||
// Handle graph/output nodes - exit subgraph and notify parent graph's downstream nodes
|
||
if (targetNode.type === "graph/output" || targetNode.type === "GraphOutput") {
|
||
const parentInfo = getParentSubgraphInfo(targetNode);
|
||
if (parentInfo) {
|
||
const { parentNode, parentGraph } = parentInfo;
|
||
// Find which output slot on parent corresponds to this graph/output
|
||
const outputIndex = targetNode.properties?.slot_index ??
|
||
targetNode.properties?.index ??
|
||
targetNode.slot_index ?? 0;
|
||
// Notify downstream nodes connected to the parent subgraph's output
|
||
notifyDownstreamNodes(parentNode, outputIndex, visited, depth + 1);
|
||
}
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
|
||
// If target is a FriendlyPipeEdit, continue propagating through it
|
||
// (it has already been synced above, now notify its downstream nodes)
|
||
if (targetNode.type === "FriendlyPipeEdit" && 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, types, and sources
|
||
this.slotCount = 1;
|
||
this.slotNames = { 1: "slot_1" };
|
||
this.slotTypes = {};
|
||
this.slotSources = {};
|
||
|
||
// 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 = {};
|
||
this.slotSources = {}; // Track source nodes for FRIENDLY_PIPE inputs
|
||
|
||
if (!this.inputs) return;
|
||
|
||
const graph = this.graph || app.graph;
|
||
|
||
for (let i = 0; i < this.inputs.length; i++) {
|
||
const input = this.inputs[i];
|
||
if (input && input.link) {
|
||
const link = graph.links[input.link];
|
||
if (link) {
|
||
const sourceNode = 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;
|
||
|
||
// If this slot receives a FRIENDLY_PIPE, track its source
|
||
if (outputType === "FRIENDLY_PIPE") {
|
||
// Find the original source of this pipe
|
||
const pipeSource = findOriginalSource(sourceNode, link.origin_slot);
|
||
if (pipeSource) {
|
||
this.slotSources[i + 1] = pipeSource;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// Get the source node for a specific slot (used by downstream FriendlyPipeOut)
|
||
nodeType.prototype.getSlotSource = function(slotIndex) {
|
||
return this.slotSources?.[slotIndex] || null;
|
||
};
|
||
|
||
// 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 = {};
|
||
this.slotSources = {};
|
||
|
||
// 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, slotSources) {
|
||
const node = this;
|
||
|
||
// Update outputs to match source
|
||
const targetCount = slotCount || 1;
|
||
const names = slotNames || {};
|
||
const types = slotTypes || {};
|
||
const sources = slotSources || {};
|
||
|
||
// 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.slotSources = sources;
|
||
this.updateSize();
|
||
this.setDirtyCanvas(true, true);
|
||
|
||
// Notify any FriendlyPipeOut nodes connected to our output slots
|
||
this.notifySlotConnections();
|
||
};
|
||
|
||
// Notify FriendlyPipeOut nodes connected to individual output slots
|
||
nodeType.prototype.notifySlotConnections = function() {
|
||
if (!this.outputs) return;
|
||
|
||
const graph = this.graph || app.graph;
|
||
|
||
for (let i = 0; i < this.outputs.length; i++) {
|
||
const output = this.outputs[i];
|
||
if (output.links) {
|
||
for (const linkId of output.links) {
|
||
const link = graph.links instanceof Map ? graph.links.get(linkId) : graph.links?.[linkId];
|
||
if (link) {
|
||
const targetNode = graph.getNodeById(link.target_id);
|
||
if (targetNode && targetNode.syncWithSource) {
|
||
targetNode.syncWithSource();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// Get the source node for a specific slot (used by downstream FriendlyPipeOut)
|
||
nodeType.prototype.getSlotSource = function(slotIndex) {
|
||
return this.slotSources?.[slotIndex] || null;
|
||
};
|
||
|
||
// 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;
|
||
|
||
debugLog("syncWithSource called on node", this.id);
|
||
debugLog("this.graph:", this.graph);
|
||
debugLog("this.inputs:", this.inputs);
|
||
|
||
if (!this.inputs || !this.inputs[0] || !this.inputs[0].link) {
|
||
debugLog("No connection, resetting to default");
|
||
// No connection, reset to default
|
||
this.updateFromSource(1, {}, {});
|
||
return;
|
||
}
|
||
|
||
const linkId = this.inputs[0].link;
|
||
debugLog("linkId:", linkId);
|
||
|
||
const link = graph.links[linkId];
|
||
debugLog("link:", link);
|
||
|
||
if (!link) {
|
||
debugLog("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) {
|
||
debugLog("Negative origin_id detected, this is a subgraph input");
|
||
debugLog("graph._subgraph_node:", graph._subgraph_node);
|
||
debugLog("graph.inputs:", graph.inputs);
|
||
debugLog("graph._inputs:", graph._inputs);
|
||
debugLog("graph.config:", graph.config);
|
||
debugLog("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) {
|
||
debugLog("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) {
|
||
debugLog("Found parent node:", node);
|
||
const result = findSourceThroughParent(node, link.origin_slot, graph);
|
||
if (result) {
|
||
immediateSource = result.node;
|
||
originSlot = result.slot;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (subgraphNode && !immediateSource) {
|
||
const result = findSourceThroughParent(subgraphNode, link.origin_slot, graph);
|
||
if (result) {
|
||
immediateSource = result.node;
|
||
originSlot = result.slot;
|
||
}
|
||
}
|
||
|
||
// Try using the graph.inputs array which defines subgraph inputs
|
||
if (!immediateSource && graph.inputs) {
|
||
debugLog("Trying graph.inputs array");
|
||
// Find the input definition that matches our slot
|
||
const inputDef = graph.inputs[link.origin_slot];
|
||
debugLog("inputDef:", inputDef);
|
||
}
|
||
} else {
|
||
immediateSource = graph.getNodeById(link.origin_id);
|
||
}
|
||
|
||
debugLog("immediateSource:", immediateSource);
|
||
debugLog("immediateSource.type:", immediateSource?.type);
|
||
debugLog("originSlot:", originSlot);
|
||
|
||
if (!immediateSource) {
|
||
debugLog("No immediate source found");
|
||
return;
|
||
}
|
||
|
||
// Special case: if connected to a FriendlyPipeOut's output slot, check if that slot
|
||
// contains a FRIENDLY_PIPE and trace back to its original source
|
||
if (immediateSource.type === "FriendlyPipeOut") {
|
||
// originSlot is 0-indexed, but our slotTypes/slotSources are 1-indexed
|
||
const slotNum = originSlot + 1;
|
||
debugLog("Connected to FriendlyPipeOut output slot", originSlot, "-> slotNum", slotNum);
|
||
// This FriendlyPipeOut is outputting individual slots
|
||
// Check if the slot type is FRIENDLY_PIPE
|
||
const slotType = immediateSource.slotTypes?.[slotNum];
|
||
debugLog("Slot type:", slotType);
|
||
|
||
if (slotType === "FRIENDLY_PIPE") {
|
||
// Need to find the original source of this pipe
|
||
// First, find the FriendlyPipeIn that feeds this FriendlyPipeOut
|
||
const pipeOutInput = immediateSource.inputs?.[0];
|
||
if (pipeOutInput && pipeOutInput.link) {
|
||
// Get the graph where the immediateSource lives
|
||
const sourceGraph = immediateSource.graph || graph;
|
||
// Handle Map or object for links
|
||
const pipeOutLink = sourceGraph.links instanceof Map
|
||
? sourceGraph.links.get(pipeOutInput.link)
|
||
: sourceGraph.links?.[pipeOutInput.link];
|
||
debugLog("pipeOutLink:", pipeOutLink);
|
||
if (pipeOutLink) {
|
||
let pipeInNode = null;
|
||
|
||
// Handle negative origin_id (subgraph input boundary)
|
||
if (pipeOutLink.origin_id < 0) {
|
||
debugLog("FriendlyPipeOut's source is a subgraph input boundary");
|
||
// Need to trace through the subgraph boundary
|
||
const parentInfo = getParentSubgraphInfo(immediateSource);
|
||
if (parentInfo) {
|
||
const { parentNode, parentGraph } = parentInfo;
|
||
const parentInput = parentNode.inputs?.[pipeOutLink.origin_slot];
|
||
if (parentInput && parentInput.link) {
|
||
const parentLink = parentGraph.links instanceof Map
|
||
? parentGraph.links.get(parentInput.link)
|
||
: parentGraph.links?.[parentInput.link];
|
||
if (parentLink) {
|
||
pipeInNode = parentGraph.getNodeById(parentLink.origin_id);
|
||
debugLog("Found pipeInNode through parent:", pipeInNode);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
pipeInNode = sourceGraph.getNodeById(pipeOutLink.origin_id);
|
||
}
|
||
|
||
if (pipeInNode) {
|
||
// Find the original FriendlyPipeIn/Edit
|
||
const pipeSource = findOriginalSource(pipeInNode, pipeOutLink.origin_slot);
|
||
debugLog("pipeSource:", pipeSource);
|
||
if (pipeSource && pipeSource.getSlotSource) {
|
||
// Get the source for this specific slot
|
||
const slotSource = pipeSource.getSlotSource(slotNum);
|
||
debugLog("Found slot source:", slotSource);
|
||
if (slotSource) {
|
||
if (slotSource.updateSlotTypes) {
|
||
slotSource.updateSlotTypes();
|
||
}
|
||
if (slotSource.getTotalSlotCount) {
|
||
this.updateFromSource(
|
||
slotSource.getTotalSlotCount(),
|
||
slotSource.getCombinedSlotNames(),
|
||
slotSource.getCombinedSlotTypes(),
|
||
slotSource.slotSources || {}
|
||
);
|
||
} else {
|
||
this.updateFromSource(
|
||
slotSource.slotCount,
|
||
slotSource.slotNames || {},
|
||
slotSource.slotTypes || {},
|
||
slotSource.slotSources || {}
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Traverse through reroute/subgraph nodes to find the original FriendlyPipeIn or FriendlyPipeEdit
|
||
const sourceNode = findOriginalSource(immediateSource, originSlot);
|
||
debugLog("sourceNode from traversal:", sourceNode);
|
||
|
||
// Use the found source or fall back to immediate source
|
||
const effectiveSource = sourceNode || immediateSource;
|
||
|
||
if (effectiveSource && effectiveSource.slotCount !== undefined) {
|
||
debugLog("Found source with slotCount:", effectiveSource.slotCount);
|
||
|
||
// Check if this is a FriendlyPipeEdit (has getTotalSlotCount method)
|
||
if (effectiveSource.getTotalSlotCount) {
|
||
debugLog("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(),
|
||
effectiveSource.slotSources || {}
|
||
);
|
||
} else {
|
||
// FriendlyPipeIn or other source
|
||
if (effectiveSource.updateSlotTypes) {
|
||
effectiveSource.updateSlotTypes();
|
||
}
|
||
this.updateFromSource(
|
||
effectiveSource.slotCount,
|
||
effectiveSource.slotNames || {},
|
||
effectiveSource.slotTypes || {},
|
||
effectiveSource.slotSources || {}
|
||
);
|
||
}
|
||
} else {
|
||
debugLog("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 = {};
|
||
this.slotSources = {};
|
||
|
||
// Track incoming pipe info
|
||
this.incomingSlotCount = 0;
|
||
this.incomingSlotNames = {};
|
||
this.incomingSlotTypes = {};
|
||
this.incomingSlotSources = {};
|
||
|
||
// 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.incomingSlotTypes = {};
|
||
this.incomingSlotSources = {};
|
||
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) {
|
||
const result = findSourceThroughParent(subgraphNode, link.origin_slot, graph);
|
||
if (result) {
|
||
sourceNode = result.node;
|
||
originSlot = result.slot;
|
||
}
|
||
} else {
|
||
// Search for parent subgraph node
|
||
for (const node of app.graph._nodes || []) {
|
||
if (node.subgraph === graph) {
|
||
const result = findSourceThroughParent(node, link.origin_slot, graph);
|
||
if (result) {
|
||
sourceNode = result.node;
|
||
originSlot = result.slot;
|
||
}
|
||
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();
|
||
this.incomingSlotTypes = effectiveSource.getCombinedSlotTypes();
|
||
this.incomingSlotSources = effectiveSource.getCombinedSlotSources ? effectiveSource.getCombinedSlotSources() : {};
|
||
} else {
|
||
this.incomingSlotCount = effectiveSource.slotCount;
|
||
this.incomingSlotNames = effectiveSource.slotNames || {};
|
||
// Make sure source has latest types
|
||
if (effectiveSource.updateSlotTypes) {
|
||
effectiveSource.updateSlotTypes();
|
||
}
|
||
this.incomingSlotTypes = effectiveSource.slotTypes || {};
|
||
this.incomingSlotSources = effectiveSource.slotSources || {};
|
||
}
|
||
}
|
||
|
||
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() {
|
||
// Make sure our slot types are up to date
|
||
this.updateSlotTypes();
|
||
|
||
const combined = {};
|
||
|
||
// Copy incoming slot types from upstream pipe
|
||
if (this.incomingSlotTypes) {
|
||
for (const [key, value] of Object.entries(this.incomingSlotTypes)) {
|
||
combined[key] = value;
|
||
}
|
||
}
|
||
|
||
// Add our additional slot types with offset indices
|
||
for (let i = 1; i <= this.slotCount; i++) {
|
||
const newIndex = this.incomingSlotCount + i;
|
||
if (this.slotTypes[i]) {
|
||
combined[newIndex] = this.slotTypes[i];
|
||
}
|
||
}
|
||
|
||
return combined;
|
||
};
|
||
|
||
// Get combined slot sources (incoming sources + our additional sources with offset indices)
|
||
nodeType.prototype.getCombinedSlotSources = function() {
|
||
const combined = {};
|
||
|
||
// Copy incoming slot sources from upstream pipe
|
||
if (this.incomingSlotSources) {
|
||
for (const [key, value] of Object.entries(this.incomingSlotSources)) {
|
||
combined[key] = value;
|
||
}
|
||
}
|
||
|
||
// Add our additional slot sources with offset indices
|
||
for (let i = 1; i <= this.slotCount; i++) {
|
||
const newIndex = this.incomingSlotCount + i;
|
||
if (this.slotSources[i]) {
|
||
combined[newIndex] = this.slotSources[i];
|
||
}
|
||
}
|
||
|
||
return combined;
|
||
};
|
||
|
||
// Get the source node for a specific slot (used by downstream FriendlyPipeOut)
|
||
nodeType.prototype.getSlotSource = function(slotIndex) {
|
||
// Check if it's an incoming slot or one of our additional slots
|
||
if (slotIndex <= this.incomingSlotCount) {
|
||
return this.incomingSlotSources?.[slotIndex] || null;
|
||
} else {
|
||
const ourSlotIndex = slotIndex - this.incomingSlotCount;
|
||
return this.slotSources?.[ourSlotIndex] || null;
|
||
}
|
||
};
|
||
|
||
nodeType.prototype.updateSlotTypes = function() {
|
||
this.slotTypes = {};
|
||
this.slotSources = {};
|
||
|
||
if (!this.inputs) return;
|
||
|
||
const graph = this.graph || app.graph;
|
||
|
||
// 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 = graph.links instanceof Map ? graph.links.get(input.link) : graph.links?.[input.link];
|
||
if (link) {
|
||
const sourceNode = 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
|
||
|
||
// If this slot receives a FRIENDLY_PIPE, track its source
|
||
if (outputType === "FRIENDLY_PIPE") {
|
||
const pipeSource = findOriginalSource(sourceNode, link.origin_slot);
|
||
if (pipeSource) {
|
||
this.slotSources[i] = pipeSource;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// 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);
|
||
};
|
||
|
||
// Override onExecutionStart to pass slot info to Python backend
|
||
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);
|
||
}
|
||
}
|
||
};
|
||
}
|