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:
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