1065 lines
39 KiB
JavaScript
1065 lines
39 KiB
JavaScript
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);
|
||
};
|
||
}
|