first commit

This commit is contained in:
Shaun Johnston
2025-12-22 16:30:00 +10:00
commit 103300a7a0
4 changed files with 507 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
from .friendly_pipe_in import FriendlyPipeIn
from .friendly_pipe_out import FriendlyPipeOut
NODE_CLASS_MAPPINGS = {
"FriendlyPipeIn": FriendlyPipeIn,
"FriendlyPipeOut": FriendlyPipeOut,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"FriendlyPipeIn": "Friendly Pipe In",
"FriendlyPipeOut": "Friendly Pipe Out",
}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+53
View File
@@ -0,0 +1,53 @@
class FriendlyPipeIn:
"""
A pipe input node that bundles up to 80 arbitrary inputs into a single pipe output.
Slots can be dynamically added or removed via the UI.
Each slot can be named, and names are passed through the pipe.
"""
MAX_SLOTS = 80
@classmethod
def INPUT_TYPES(cls):
inputs = {
"required": {},
"optional": {},
"hidden": {
"slot_count": ("INT", {"default": 1}),
"slot_names": ("STRING", {"default": "{}"}),
}
}
# Define all possible slots as optional with wildcard type
for i in range(1, cls.MAX_SLOTS + 1):
inputs["optional"][f"slot_{i}"] = ("*", {"forceInput": True})
return inputs
RETURN_TYPES = ("FRIENDLY_PIPE",)
RETURN_NAMES = ("pipe",)
FUNCTION = "execute"
CATEGORY = "utils/pipe"
def execute(self, slot_count=1, slot_names="{}", **kwargs):
import json
# Parse slot names from JSON string
try:
names_dict = json.loads(slot_names)
except:
names_dict = {}
# Collect all connected slot values into a dictionary
pipe_data = {
"slot_count": slot_count,
"slots": {},
"names": names_dict
}
for i in range(1, self.MAX_SLOTS + 1):
slot_key = f"slot_{i}"
if slot_key in kwargs and kwargs[slot_key] is not None:
pipe_data["slots"][i] = kwargs[slot_key]
return (pipe_data,)
+41
View File
@@ -0,0 +1,41 @@
class FriendlyPipeOut:
"""
A pipe output node that unpacks a FRIENDLY_PIPE into individual outputs.
Output slots dynamically reflect the connected FriendlyPipeIn's configuration.
"""
MAX_SLOTS = 80
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"pipe": ("FRIENDLY_PIPE",),
},
"hidden": {
"slot_count": ("INT", {"default": 1}),
"slot_names": ("STRING", {"default": "{}"}),
}
}
# Define all possible output slots with wildcard type
RETURN_TYPES = tuple(["*"] * MAX_SLOTS)
RETURN_NAMES = tuple([f"slot_{i}" for i in range(1, MAX_SLOTS + 1)])
FUNCTION = "execute"
CATEGORY = "utils/pipe"
OUTPUT_NODE = False
def execute(self, pipe, slot_count=1, slot_names="{}"):
# Extract values from the pipe data
pipe_slots = pipe.get("slots", {})
pipe_slot_count = pipe.get("slot_count", 1)
# Build output tuple - return None for empty/missing slots
outputs = []
for i in range(1, self.MAX_SLOTS + 1):
if i in pipe_slots:
outputs.append(pipe_slots[i])
else:
outputs.append(None)
return tuple(outputs)
+399
View File
@@ -0,0 +1,399 @@
import { app } from "../../scripts/app.js";
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);
}
}
});
function setupFriendlyPipeIn(nodeType, nodeData, app) {
// Store the original onNodeCreated
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
if (origOnNodeCreated) {
origOnNodeCreated.apply(this, arguments);
}
// Initialize slot count and names
this.slotCount = 1;
this.slotNames = {};
// Hide all slots except the first one initially
this.updateVisibleSlots();
// Add the control buttons widget
this.addControlButtons();
// Add initial name widget for slot 1
this.addSlotNameWidget(1);
};
nodeType.prototype.addControlButtons = function() {
const node = this;
// Create a widget that displays the add/subtract buttons
const widget = this.addWidget("button", "➕ Add Slot", null, () => {
if (node.slotCount < 80) {
node.slotCount++;
node.addSlotNameWidget(node.slotCount);
node.updateVisibleSlots();
node.setDirtyCanvas(true, true);
}
});
widget.serialize = false;
const removeWidget = this.addWidget("button", "➖ Remove Slot", null, () => {
if (node.slotCount > 1) {
node.removeSlotNameWidget(node.slotCount);
node.slotCount--;
node.updateVisibleSlots();
node.setDirtyCanvas(true, true);
}
});
removeWidget.serialize = false;
};
nodeType.prototype.addSlotNameWidget = function(slotNum) {
const node = this;
const defaultName = `slot_${slotNum}`;
// Create a text input widget for naming this slot
const nameWidget = this.addWidget("text", `name_${slotNum}`, node.slotNames[slotNum] || defaultName, (value) => {
node.slotNames[slotNum] = value;
node.updateSlotLabel(slotNum, value);
node.updateHiddenSlotNames();
});
nameWidget.slotNum = slotNum;
// Initialize the slot name if not set
if (!node.slotNames[slotNum]) {
node.slotNames[slotNum] = defaultName;
}
// Update the slot label immediately
node.updateSlotLabel(slotNum, node.slotNames[slotNum]);
};
nodeType.prototype.removeSlotNameWidget = function(slotNum) {
// Find and remove the name widget for this slot
const widgetIndex = this.widgets?.findIndex(w => w.slotNum === slotNum);
if (widgetIndex !== undefined && widgetIndex >= 0) {
this.widgets.splice(widgetIndex, 1);
}
// Clean up the slot name
delete this.slotNames[slotNum];
this.updateHiddenSlotNames();
};
nodeType.prototype.updateSlotLabel = function(slotNum, name) {
// Find the input slot and update its label
if (this.inputs) {
const input = this.inputs.find(inp => inp.name === `slot_${slotNum}`);
if (input) {
input.label = name || `slot_${slotNum}`;
}
}
};
nodeType.prototype.updateHiddenSlotNames = function() {
// Update the hidden slot_names widget with JSON of all names
const slotNamesWidget = this.widgets?.find(w => w.name === "slot_names");
if (slotNamesWidget) {
slotNamesWidget.value = JSON.stringify(this.slotNames);
}
};
nodeType.prototype.updateVisibleSlots = function() {
// Update the hidden slot_count value
const slotCountWidget = this.widgets?.find(w => w.name === "slot_count");
if (slotCountWidget) {
slotCountWidget.value = this.slotCount;
}
// Update hidden slot_names value
this.updateHiddenSlotNames();
// Show/hide input slots based on current count
if (this.inputs) {
for (let i = 0; i < this.inputs.length; i++) {
const input = this.inputs[i];
const match = input.name.match(/^slot_(\d+)$/);
if (match) {
const slotNum = parseInt(match[1]);
input.hidden = slotNum > this.slotCount;
// Update label based on stored name
if (!input.hidden && this.slotNames[slotNum]) {
input.label = this.slotNames[slotNum];
}
}
}
}
// Show/hide name widgets based on current count
if (this.widgets) {
for (const widget of this.widgets) {
if (widget.slotNum !== undefined) {
widget.hidden = widget.slotNum > this.slotCount;
}
}
}
// Recalculate node size
this.computeSize();
};
// Override computeSize to account for hidden slots
const origComputeSize = nodeType.prototype.computeSize;
nodeType.prototype.computeSize = function(out) {
const size = origComputeSize ? origComputeSize.apply(this, arguments) : [200, 100];
// Calculate height based on visible slots + widgets
const visibleInputs = this.slotCount || 1;
const widgetHeight = (this.widgets?.length || 0) * 30;
const inputHeight = visibleInputs * 20;
const outputHeight = 20; // One output
const headerHeight = 30;
const padding = 20;
size[1] = Math.max(size[1], headerHeight + inputHeight + widgetHeight + outputHeight + padding);
return size;
};
// Custom drawing to only show visible slots
const origOnDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function(ctx) {
if (origOnDrawForeground) {
origOnDrawForeground.apply(this, arguments);
}
// Hide inputs beyond slotCount by not drawing them
if (this.inputs) {
for (let i = 0; i < this.inputs.length; i++) {
const input = this.inputs[i];
const match = input.name.match(/^slot_(\d+)$/);
if (match) {
const slotNum = parseInt(match[1]);
input.hidden = slotNum > this.slotCount;
}
}
}
};
// Handle serialization to save slot count and names
const origOnSerialize = nodeType.prototype.onSerialize;
nodeType.prototype.onSerialize = function(o) {
if (origOnSerialize) {
origOnSerialize.apply(this, arguments);
}
o.slotCount = this.slotCount;
o.slotNames = this.slotNames;
};
// Handle deserialization to restore slot count and names
const origOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function(o) {
if (origOnConfigure) {
origOnConfigure.apply(this, arguments);
}
// Restore slot names first
if (o.slotNames) {
this.slotNames = o.slotNames;
}
// Restore slot count and create name widgets
if (o.slotCount !== undefined) {
this.slotCount = o.slotCount;
// Add name widgets for all restored slots (skip 1 as it's added in onNodeCreated)
for (let i = 2; i <= this.slotCount; i++) {
this.addSlotNameWidget(i);
}
// Update widget values with restored names
if (this.widgets) {
for (const widget of this.widgets) {
if (widget.slotNum !== undefined && this.slotNames[widget.slotNum]) {
widget.value = this.slotNames[widget.slotNum];
}
}
}
this.updateVisibleSlots();
}
};
}
function setupFriendlyPipeOut(nodeType, nodeData, app) {
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
if (origOnNodeCreated) {
origOnNodeCreated.apply(this, arguments);
}
// Initialize slot count and names
this.slotCount = 1;
this.slotNames = {};
// Hide all output slots except the first one initially
this.updateVisibleOutputs();
};
nodeType.prototype.updateVisibleOutputs = function() {
// Show/hide output slots based on current count
if (this.outputs) {
for (let i = 0; i < this.outputs.length; i++) {
const output = this.outputs[i];
const match = output.name.match(/^slot_(\d+)$/);
if (match) {
const slotNum = parseInt(match[1]);
output.hidden = slotNum > this.slotCount;
// Update label based on stored name
if (!output.hidden && this.slotNames[slotNum]) {
output.label = this.slotNames[slotNum];
} else if (!output.hidden) {
output.label = `slot_${slotNum}`;
}
}
}
}
// Recalculate node size
this.computeSize();
this.setDirtyCanvas(true, true);
};
nodeType.prototype.updateOutputLabels = function(names) {
if (this.outputs) {
for (let i = 0; i < this.outputs.length; i++) {
const output = this.outputs[i];
const match = output.name.match(/^slot_(\d+)$/);
if (match) {
const slotNum = parseInt(match[1]);
if (names[slotNum]) {
output.label = names[slotNum];
} else {
output.label = `slot_${slotNum}`;
}
}
}
}
};
// Handle connection changes to update from connected FriendlyPipeIn
const origOnConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function(type, index, connected, linkInfo) {
if (origOnConnectionsChange) {
origOnConnectionsChange.apply(this, arguments);
}
// When input connection changes, try to get info from connected node
if (type === 1) { // Input connection
this.updateFromConnectedPipe();
}
};
nodeType.prototype.updateFromConnectedPipe = function() {
// Find the connected FriendlyPipeIn node
if (!this.inputs || !this.inputs[0] || !this.inputs[0].link) {
// No connection, reset to default
this.slotCount = 1;
this.slotNames = {};
this.updateVisibleOutputs();
return;
}
const linkId = this.inputs[0].link;
const link = app.graph.links[linkId];
if (!link) return;
const sourceNode = app.graph.getNodeById(link.origin_id);
if (!sourceNode) return;
// Check if it's a FriendlyPipeIn or another FriendlyPipeOut (for chaining)
if (sourceNode.type === "FriendlyPipeIn" || sourceNode.type === "FriendlyPipeOut") {
this.slotCount = sourceNode.slotCount || 1;
this.slotNames = sourceNode.slotNames || {};
this.updateOutputLabels(this.slotNames);
this.updateVisibleOutputs();
}
};
// Override computeSize to account for hidden slots
const origComputeSize = nodeType.prototype.computeSize;
nodeType.prototype.computeSize = function(out) {
const size = origComputeSize ? origComputeSize.apply(this, arguments) : [200, 100];
// Calculate height based on visible outputs
const visibleOutputs = this.slotCount || 1;
const inputHeight = 20; // One input (pipe)
const outputHeight = visibleOutputs * 20;
const headerHeight = 30;
const padding = 20;
size[1] = Math.max(size[1], headerHeight + inputHeight + outputHeight + padding);
return size;
};
// Custom drawing to only show visible outputs
const origOnDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function(ctx) {
if (origOnDrawForeground) {
origOnDrawForeground.apply(this, arguments);
}
// Hide outputs beyond slotCount
if (this.outputs) {
for (let i = 0; i < this.outputs.length; i++) {
const output = this.outputs[i];
const match = output.name.match(/^slot_(\d+)$/);
if (match) {
const slotNum = parseInt(match[1]);
output.hidden = slotNum > this.slotCount;
}
}
}
};
// 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;
};
// Handle deserialization
const origOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function(o) {
if (origOnConfigure) {
origOnConfigure.apply(this, arguments);
}
if (o.slotNames) {
this.slotNames = o.slotNames;
}
if (o.slotCount !== undefined) {
this.slotCount = o.slotCount;
}
this.updateOutputLabels(this.slotNames);
this.updateVisibleOutputs();
};
}