Add Ordered Passthrough node with auto-wired execution order
Some checks are pending
Publish to Comfy registry / Publish Custom Node to registry (push) Waiting to run
Some checks are pending
Publish to Comfy registry / Publish Custom Node to registry (push) Waiting to run
JS extension auto-creates LiteGraph links between consecutive passthrough nodes based on their order widget (1→2→3→4), hiding the wait_for input and drawing visual chain lines between nodes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-json-dynamic"
|
name = "comfyui-json-dynamic"
|
||||||
version = "1.2.2"
|
version = "1.2.3"
|
||||||
description = "ComfyUI nodes for dynamic JSON loading and string/path utility operations"
|
description = "ComfyUI nodes for dynamic JSON loading and string/path utility operations"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
@@ -155,11 +155,36 @@ class JDL_StringSwitch:
|
|||||||
return (on_false if on_false is not None else default_false,)
|
return (on_false if on_false is not None else default_false,)
|
||||||
|
|
||||||
|
|
||||||
|
class JDL_DependencyPassthrough:
|
||||||
|
"""Passes data through unchanged. Set 'order' in the JS widget to auto-wire
|
||||||
|
execution order between passthrough nodes (1 → 2 → 3 → 4)."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"data": (any_type,),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"wait_for": (any_type,),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = (any_type,)
|
||||||
|
RETURN_NAMES = ("data",)
|
||||||
|
FUNCTION = "passthrough"
|
||||||
|
CATEGORY = "utils/flow"
|
||||||
|
|
||||||
|
def passthrough(self, data, wait_for=None):
|
||||||
|
return (data,)
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"JDL_PathJoin": JDL_PathJoin,
|
"JDL_PathJoin": JDL_PathJoin,
|
||||||
"JDL_StringFormat": JDL_StringFormat,
|
"JDL_StringFormat": JDL_StringFormat,
|
||||||
"JDL_StringExtract": JDL_StringExtract,
|
"JDL_StringExtract": JDL_StringExtract,
|
||||||
"JDL_StringSwitch": JDL_StringSwitch,
|
"JDL_StringSwitch": JDL_StringSwitch,
|
||||||
|
"JDL_DependencyPassthrough": JDL_DependencyPassthrough,
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
@@ -167,4 +192,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"JDL_StringFormat": "String Format",
|
"JDL_StringFormat": "String Format",
|
||||||
"JDL_StringExtract": "String Extract",
|
"JDL_StringExtract": "String Extract",
|
||||||
"JDL_StringSwitch": "String Switch",
|
"JDL_StringSwitch": "String Switch",
|
||||||
|
"JDL_DependencyPassthrough": "Ordered Passthrough",
|
||||||
}
|
}
|
||||||
|
|||||||
295
web/dependency_passthrough.js
Normal file
295
web/dependency_passthrough.js
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
import { app } from "../../scripts/app.js";
|
||||||
|
|
||||||
|
const NODE_TYPE = "JDL_DependencyPassthrough";
|
||||||
|
const WAIT_FOR_INPUT = "wait_for";
|
||||||
|
const CHAIN_COLOR = "#4ea6f7";
|
||||||
|
const CHAIN_COLOR_DIM = "#2a5a8a";
|
||||||
|
|
||||||
|
/** Find the input slot index for wait_for on a node. */
|
||||||
|
function getWaitForSlot(node) {
|
||||||
|
if (!node.inputs) return -1;
|
||||||
|
for (let i = 0; i < node.inputs.length; i++) {
|
||||||
|
if (node.inputs[i].name === WAIT_FOR_INPUT) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get all JDL_DependencyPassthrough nodes in the graph sorted by order. */
|
||||||
|
function getSortedPassthroughNodes(graph) {
|
||||||
|
const nodes = [];
|
||||||
|
for (const node of graph._nodes || []) {
|
||||||
|
if (node.type === NODE_TYPE) {
|
||||||
|
const orderW = node.widgets?.find(w => w.name === "order");
|
||||||
|
nodes.push({ node, order: orderW ? orderW.value : 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.sort((a, b) => a.order - b.order || a.node.id - b.node.id);
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove all links connected to the wait_for input of a node. */
|
||||||
|
function clearWaitForLinks(node, graph) {
|
||||||
|
const slot = getWaitForSlot(node);
|
||||||
|
if (slot < 0) return;
|
||||||
|
const input = node.inputs[slot];
|
||||||
|
if (input && input.link != null) {
|
||||||
|
graph.removeLink(input.link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewire all passthrough nodes: connect each node's data output (slot 0)
|
||||||
|
* to the next-order node's wait_for input.
|
||||||
|
*/
|
||||||
|
function rewireAll(graph) {
|
||||||
|
if (!graph || graph._rewiring) return;
|
||||||
|
graph._rewiring = true;
|
||||||
|
graph._prevNodeMap = null; // invalidate rendering cache
|
||||||
|
try {
|
||||||
|
const sorted = getSortedPassthroughNodes(graph);
|
||||||
|
|
||||||
|
// Clear all wait_for links first
|
||||||
|
for (const { node } of sorted) {
|
||||||
|
clearWaitForLinks(node, graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect consecutive pairs
|
||||||
|
for (let i = 0; i < sorted.length - 1; i++) {
|
||||||
|
const srcNode = sorted[i].node;
|
||||||
|
const dstNode = sorted[i + 1].node;
|
||||||
|
const dstSlot = getWaitForSlot(dstNode);
|
||||||
|
if (dstSlot < 0) continue;
|
||||||
|
// output slot 0 = data output
|
||||||
|
srcNode.connect(0, dstNode, dstSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.setDirtyCanvas(true, true);
|
||||||
|
} finally {
|
||||||
|
graph._rewiring = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a map from node id to its predecessor in the sorted chain.
|
||||||
|
* Cached per graph and invalidated when rewireAll runs.
|
||||||
|
*/
|
||||||
|
function getPrevNodeMap(graph) {
|
||||||
|
if (graph._prevNodeMap) return graph._prevNodeMap;
|
||||||
|
const sorted = getSortedPassthroughNodes(graph);
|
||||||
|
const map = new Map();
|
||||||
|
for (let i = 1; i < sorted.length; i++) {
|
||||||
|
map.set(sorted[i].node.id, sorted[i - 1]);
|
||||||
|
}
|
||||||
|
graph._prevNodeMap = map;
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "jdl.dependency.passthrough",
|
||||||
|
|
||||||
|
async beforeRegisterNodeDef(nodeType, nodeData, appInstance) {
|
||||||
|
if (nodeData.name !== NODE_TYPE) return;
|
||||||
|
|
||||||
|
// --- onNodeCreated: add order widget, hide wait_for ---
|
||||||
|
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
||||||
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
origOnNodeCreated?.apply(this, arguments);
|
||||||
|
|
||||||
|
// Add order widget
|
||||||
|
this.addWidget("number", "order", 1, (v) => {
|
||||||
|
// Clamp to 1-99
|
||||||
|
const clamped = Math.max(1, Math.min(99, Math.round(v)));
|
||||||
|
const orderW = this.widgets?.find(w => w.name === "order");
|
||||||
|
if (orderW && orderW.value !== clamped) orderW.value = clamped;
|
||||||
|
// Rewire after order changes
|
||||||
|
if (this.graph) rewireAll(this.graph);
|
||||||
|
}, { min: 1, max: 99, step: 1, precision: 0 });
|
||||||
|
|
||||||
|
// Hide the wait_for input visually
|
||||||
|
this._hideWaitFor();
|
||||||
|
|
||||||
|
this.setSize(this.computeSize());
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Helper to hide wait_for slot ---
|
||||||
|
nodeType.prototype._hideWaitFor = function () {
|
||||||
|
const slot = getWaitForSlot(this);
|
||||||
|
if (slot >= 0 && this.inputs[slot]) {
|
||||||
|
this.inputs[slot].hidden = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- onConfigure: restore order widget from saved data ---
|
||||||
|
const origOnConfigure = nodeType.prototype.onConfigure;
|
||||||
|
nodeType.prototype.onConfigure = function (info) {
|
||||||
|
origOnConfigure?.apply(this, arguments);
|
||||||
|
|
||||||
|
// Restore order widget value from serialized widget values
|
||||||
|
const orderW = this.widgets?.find(w => w.name === "order");
|
||||||
|
if (orderW && info.widgets_values) {
|
||||||
|
const orderIdx = this.widgets.indexOf(orderW);
|
||||||
|
if (orderIdx >= 0 && orderIdx < info.widgets_values.length) {
|
||||||
|
const v = info.widgets_values[orderIdx];
|
||||||
|
if (typeof v === "number") {
|
||||||
|
orderW.value = Math.max(1, Math.min(99, Math.round(v)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._hideWaitFor();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- onAdded: rewire when node is placed on graph ---
|
||||||
|
const origOnAdded = nodeType.prototype.onAdded;
|
||||||
|
nodeType.prototype.onAdded = function (graph) {
|
||||||
|
origOnAdded?.apply(this, arguments);
|
||||||
|
this._hideWaitFor();
|
||||||
|
// Defer to let graph settle
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (this.graph) rewireAll(this.graph);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- onRemoved: rewire remaining nodes ---
|
||||||
|
const origOnRemoved = nodeType.prototype.onRemoved;
|
||||||
|
nodeType.prototype.onRemoved = function () {
|
||||||
|
const graph = this.graph;
|
||||||
|
origOnRemoved?.apply(this, arguments);
|
||||||
|
if (graph) {
|
||||||
|
queueMicrotask(() => rewireAll(graph));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- onConnectionsChange: prevent manual wait_for edits ---
|
||||||
|
const origOnConnectionsChange = nodeType.prototype.onConnectionsChange;
|
||||||
|
nodeType.prototype.onConnectionsChange = function (type, slotIndex, isConnected, linkInfo, ioSlot) {
|
||||||
|
origOnConnectionsChange?.apply(this, arguments);
|
||||||
|
if (this.graph?._rewiring) return;
|
||||||
|
|
||||||
|
// If user manually disconnected/connected the wait_for slot, re-enforce auto wiring
|
||||||
|
if (type === LiteGraph.INPUT) {
|
||||||
|
const waitSlot = getWaitForSlot(this);
|
||||||
|
if (slotIndex === waitSlot) {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (this.graph) rewireAll(this.graph);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- onDrawForeground: draw chain visualization ---
|
||||||
|
const origOnDrawForeground = nodeType.prototype.onDrawForeground;
|
||||||
|
nodeType.prototype.onDrawForeground = function (ctx) {
|
||||||
|
origOnDrawForeground?.apply(this, arguments);
|
||||||
|
if (!this.graph || this.flags?.collapsed) return;
|
||||||
|
|
||||||
|
const orderW = this.widgets?.find(w => w.name === "order");
|
||||||
|
if (!orderW) return;
|
||||||
|
|
||||||
|
// Draw order badge in top-right corner
|
||||||
|
const orderNum = orderW.value;
|
||||||
|
ctx.save();
|
||||||
|
const badgeX = this.size[0] - 28;
|
||||||
|
const badgeY = -LiteGraph.NODE_TITLE_HEIGHT + 4;
|
||||||
|
const badgeW = 24;
|
||||||
|
const badgeH = LiteGraph.NODE_TITLE_HEIGHT - 8;
|
||||||
|
const radius = 4;
|
||||||
|
|
||||||
|
// Rounded rect background
|
||||||
|
ctx.fillStyle = CHAIN_COLOR;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.roundRect(badgeX, badgeY, badgeW, badgeH, radius);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Order number text
|
||||||
|
ctx.fillStyle = "#fff";
|
||||||
|
ctx.font = "bold 11px sans-serif";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(orderNum, badgeX + badgeW / 2, badgeY + badgeH / 2);
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// Draw dashed chain line from previous-order node
|
||||||
|
const prevMap = getPrevNodeMap(this.graph);
|
||||||
|
const prev = prevMap.get(this.id);
|
||||||
|
if (!prev) return;
|
||||||
|
|
||||||
|
const srcNode = prev.node;
|
||||||
|
// Calculate connection points in graph space, then convert to local
|
||||||
|
const srcX = srcNode.pos[0] + srcNode.size[0] / 2;
|
||||||
|
const srcY = srcNode.pos[1] + srcNode.size[1];
|
||||||
|
const dstX = this.pos[0] + this.size[0] / 2;
|
||||||
|
const dstY = this.pos[1] + 10;
|
||||||
|
|
||||||
|
// Convert to local coords (this node's space)
|
||||||
|
const localSrcX = srcX - this.pos[0];
|
||||||
|
const localSrcY = srcY - this.pos[1];
|
||||||
|
const localDstX = dstX - this.pos[0];
|
||||||
|
const localDstY = dstY - this.pos[1];
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = CHAIN_COLOR_DIM;
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.setLineDash([6, 4]);
|
||||||
|
ctx.globalAlpha = 0.6;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(localSrcX, localSrcY);
|
||||||
|
|
||||||
|
// Curved line for better visual
|
||||||
|
const midY = (localSrcY + localDstY) / 2;
|
||||||
|
ctx.bezierCurveTo(
|
||||||
|
localSrcX, midY,
|
||||||
|
localDstX, midY,
|
||||||
|
localDstX, localDstY
|
||||||
|
);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw a small arrow at the destination
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.globalAlpha = 0.8;
|
||||||
|
ctx.fillStyle = CHAIN_COLOR_DIM;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(localDstX, localDstY);
|
||||||
|
ctx.lineTo(localDstX - 5, localDstY - 8);
|
||||||
|
ctx.lineTo(localDstX + 5, localDstY - 8);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.restore();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/** After the full graph is configured (workflow load), rewire everything. */
|
||||||
|
async setup(appInstance) {
|
||||||
|
function hookGraph(graph) {
|
||||||
|
if (!graph || graph._passthroughHooked) return;
|
||||||
|
graph._passthroughHooked = true;
|
||||||
|
const origAfterConfigure = graph.onAfterConfigure;
|
||||||
|
graph.onAfterConfigure = function () {
|
||||||
|
origAfterConfigure?.apply(this, arguments);
|
||||||
|
queueMicrotask(() => {
|
||||||
|
for (const node of graph._nodes || []) {
|
||||||
|
if (node.type === NODE_TYPE) {
|
||||||
|
node._hideWaitFor?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rewireAll(graph);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook now if graph exists, otherwise poll briefly until it does
|
||||||
|
if (app.graph) {
|
||||||
|
hookGraph(app.graph);
|
||||||
|
} else {
|
||||||
|
let attempts = 0;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (app.graph) {
|
||||||
|
clearInterval(interval);
|
||||||
|
hookGraph(app.graph);
|
||||||
|
} else if (++attempts > 50) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user