1736 lines
59 KiB
JavaScript
1736 lines
59 KiB
JavaScript
const { app } = window.comfyAPI.app;
|
|
import { connectById } from "./utility.js";
|
|
|
|
// originally based on diffus3's SetGet: https://github.com/diffus3/ComfyUI-extensions
|
|
|
|
// Nodes that allow you to hide connections for cleaner graphs
|
|
|
|
let _typeColorMap;
|
|
function setColorAndBgColor(node, type) {
|
|
if (!_typeColorMap) {
|
|
_typeColorMap = {
|
|
"DEFAULT": LGraphCanvas.node_colors.gray,
|
|
"MODEL": LGraphCanvas.node_colors.blue,
|
|
"LATENT": LGraphCanvas.node_colors.purple,
|
|
"VAE": LGraphCanvas.node_colors.red,
|
|
"WANVAE": LGraphCanvas.node_colors.red,
|
|
"CONDITIONING": LGraphCanvas.node_colors.brown,
|
|
"IMAGE": LGraphCanvas.node_colors.pale_blue,
|
|
"CLIP": LGraphCanvas.node_colors.yellow,
|
|
"FLOAT": LGraphCanvas.node_colors.green,
|
|
"MASK": { color: "#1c5715", bgcolor: "#1f401b"},
|
|
"INT": { color: "#1b4669", bgcolor: "#29699c"},
|
|
"CONTROL_NET": { color: "#156653", bgcolor: "#1c453b"},
|
|
"NOISE": { color: "#2e2e2e", bgcolor: "#242121"},
|
|
"GUIDER": { color: "#3c7878", bgcolor: "#1c453b"},
|
|
"SAMPLER": { color: "#614a4a", bgcolor: "#3b2c2c"},
|
|
"SIGMAS": { color: "#485248", bgcolor: "#272e27"},
|
|
};
|
|
}
|
|
const colors = _typeColorMap[type] || LGraphCanvas.node_colors?.gray;
|
|
if (colors) {
|
|
node.color = colors.color;
|
|
node.bgcolor = colors.bgcolor;
|
|
}
|
|
}
|
|
// LGraphEventMode values
|
|
const MODE_ALWAYS = 0;
|
|
const MODE_BYPASS = 4;
|
|
// graphToPrompt calls in progress; GetNode ignores bypass while > 0
|
|
let _resolvingPrompt = 0;
|
|
|
|
function getDisablePrefix() {
|
|
return app.ui.settings.getSettingValue("KJNodes.disablePrefix") ?? false;
|
|
}
|
|
function prefixedTitle(prefix, name) {
|
|
return (getDisablePrefix() ? "" : prefix + "_") + name;
|
|
}
|
|
function autoColor(node, type) {
|
|
if (!app.ui.settings.getSettingValue("KJNodes.nodeAutoColor")) return;
|
|
if (type === '*') { node.color = null; node.bgcolor = null; }
|
|
else setColorAndBgColor(node, type);
|
|
}
|
|
function addNodeToSelectedOrCursor(nodeType, side) {
|
|
const selected = Object.values(app.canvas.selected_nodes || {});
|
|
if (selected.length > 0) {
|
|
for (const n of selected) window.kjNodes.addNode(nodeType, n, { side, offset: 30 });
|
|
} else {
|
|
const canvas = app.canvas, graph = canvas.graph || app.graph;
|
|
const node = LiteGraph.createNode(nodeType);
|
|
if (!node) return;
|
|
node.pos = [canvas.graph_mouse[0], canvas.graph_mouse[1]];
|
|
graph.add(node);
|
|
canvas.selectNode(node, false);
|
|
canvas.setDirty(true, true);
|
|
}
|
|
}
|
|
// Temporary map for paste rename coordination between Set and Get nodes.
|
|
// Key: old name, Value: new name. Cleared via setTimeout(0) after each paste cycle.
|
|
// This works because both onConfigure calls (Set + Get) fire synchronously within
|
|
// the same paste operation, before the timeout clears the entry.
|
|
const _pasteRenameMap = new Map();
|
|
|
|
let _forceShowAllLinks = false;
|
|
|
|
function getShowLinksMode() {
|
|
if (_forceShowAllLinks) return "always";
|
|
return app.ui.settings.getSettingValue("KJNodes.showSetGetLinks") ?? "never";
|
|
}
|
|
const LGraphNode = LiteGraph.LGraphNode
|
|
|
|
// Cross-graph traversal utilities with lexical scoping for subgraph support.
|
|
// Set nodes propagate downward: a Set in a parent graph is visible to all descendant subgraphs.
|
|
// Get nodes look upward: a Get searches its own graph first, then parent, then grandparent, etc.
|
|
// Duplicate names are allowed across unrelated (sibling) subgraphs.
|
|
|
|
function findRootGraph(graph) {
|
|
if (!graph) return null;
|
|
return graph.rootGraph || graph;
|
|
}
|
|
|
|
// Find which SubgraphNode in parentGraph wraps the given subgraph
|
|
function findSubgraphNodeFor(parentGraph, innerNode) {
|
|
if (!parentGraph?._nodes || !innerNode?.graph) return null;
|
|
for (const n of parentGraph._nodes) {
|
|
if (n.subgraph && n.subgraph === innerNode.graph) return n;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Walk from a subgraph up to root, returning [graph, parent, grandparent, ..., root]
|
|
function getGraphAncestors(graph) {
|
|
if (!graph) return [];
|
|
const root = findRootGraph(graph);
|
|
if (!root || graph === root) return [root];
|
|
|
|
const chain = [graph];
|
|
const visited = new Set([graph]);
|
|
let current = graph;
|
|
// Walk up: find which SubgraphNode wraps current, then get its parent graph
|
|
while (current !== root) {
|
|
let found = false;
|
|
// Search root nodes
|
|
for (const n of root._nodes) {
|
|
if (n.subgraph === current) {
|
|
chain.push(root);
|
|
current = root;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (found) break;
|
|
// Search sibling subgraphs (for nested subgraphs)
|
|
const subgraphs = root._subgraphs || root.subgraphs;
|
|
if (subgraphs) {
|
|
for (const sg of subgraphs.values()) {
|
|
if (sg === current || !sg._nodes) continue;
|
|
for (const n of sg._nodes) {
|
|
if (n.subgraph === current) {
|
|
if (visited.has(sg)) { found = false; break; }
|
|
visited.add(sg);
|
|
chain.push(sg);
|
|
current = sg;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (found) break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
// Can't find parent, add root as fallback
|
|
if (!chain.includes(root)) chain.push(root);
|
|
break;
|
|
}
|
|
}
|
|
return chain;
|
|
}
|
|
|
|
// Get all descendant subgraphs of a graph (children, grandchildren, etc.)
|
|
function getGraphDescendants(graph, _visited) {
|
|
if (!graph?._nodes) return [];
|
|
const visited = _visited || new Set();
|
|
if (visited.has(graph)) return [];
|
|
visited.add(graph);
|
|
const descendants = [];
|
|
for (const n of graph._nodes) {
|
|
if (n.subgraph && !visited.has(n.subgraph)) {
|
|
descendants.push(n.subgraph);
|
|
descendants.push(...getGraphDescendants(n.subgraph, visited));
|
|
}
|
|
}
|
|
return descendants;
|
|
}
|
|
|
|
// Collect nodes of a type from specific graphs
|
|
function collectNodesOfType(graphs, type) {
|
|
const results = [];
|
|
for (const g of graphs) {
|
|
if (!g?._nodes) continue;
|
|
for (const node of g._nodes) {
|
|
if (node.type === type) results.push({ node, graph: g });
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
// Find all nodes of type across ALL graphs (root + all subgraphs). Used for global operations.
|
|
function findAllNodesOfType(graph, type) {
|
|
const root = findRootGraph(graph);
|
|
if (!root) return [];
|
|
const allGraphs = [root];
|
|
const subgraphs = root._subgraphs || root.subgraphs;
|
|
if (subgraphs) {
|
|
for (const sg of subgraphs.values()) allGraphs.push(sg);
|
|
}
|
|
return collectNodesOfType(allGraphs, type);
|
|
}
|
|
|
|
// Scoped setter lookup: search current graph, then ancestors (look up).
|
|
function findSetterByName(graph, name) {
|
|
if (!name) return null;
|
|
for (const g of getGraphAncestors(graph)) {
|
|
if (!g?._nodes) continue;
|
|
for (const node of g._nodes) {
|
|
if (node.type === 'SetNode' && node.widgets[0].value === name) {
|
|
return { node, graph: g };
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Scoped getter lookup: search current graph + descendants (propagate down).
|
|
function findGettersByName(graph, name) {
|
|
if (!name) return [];
|
|
const graphs = [graph, ...getGraphDescendants(graph)];
|
|
return collectNodesOfType(graphs, 'GetNode')
|
|
.filter(entry => entry.node.widgets[0].value === name);
|
|
}
|
|
|
|
// Get all visible SetNode names for a GetNode's combo dropdown.
|
|
// Shows names from current graph + ancestors (what's in scope).
|
|
// Also builds a source map for getOptionLabel. This module-level variable is safe
|
|
// because the values getter and getOptionLabel run in the same synchronous render pass.
|
|
let _setNameSourceMap = new Map();
|
|
|
|
function getVisibleSetNames(graph, filterType) {
|
|
const sourceMap = new Map();
|
|
const ancestors = getGraphAncestors(graph);
|
|
const entries = collectNodesOfType(ancestors, 'SetNode');
|
|
for (const e of entries) {
|
|
const name = e.node.widgets[0].value;
|
|
if (!name) continue;
|
|
if (filterType && filterType !== '*') {
|
|
const setType = e.node.inputs[0]?.type;
|
|
if (setType && setType !== '*') {
|
|
const filterTypes = String(filterType).split(",");
|
|
if (!filterTypes.some(ft => ft === setType || setType.split(",").includes(ft))) continue;
|
|
}
|
|
}
|
|
if (!sourceMap.has(name)) {
|
|
sourceMap.set(name, e.graph === graph ? "local" : "parent");
|
|
}
|
|
}
|
|
_setNameSourceMap = sourceMap;
|
|
return [...sourceMap.keys()].sort();
|
|
}
|
|
|
|
// Force every GetNode (root + subgraphs) to swap its widget.options reference so Vue re-reads values.
|
|
function refreshAllGetNodeCombos(graph) {
|
|
const root = findRootGraph(graph);
|
|
if (!root) return;
|
|
const allGraphs = [root];
|
|
const subgraphs = root._subgraphs || root.subgraphs;
|
|
if (subgraphs) {
|
|
for (const sg of subgraphs.values()) allGraphs.push(sg);
|
|
}
|
|
for (const g of allGraphs) {
|
|
if (!g?._nodes) continue;
|
|
for (const node of g._nodes) {
|
|
if (node.type === 'GetNode') node._refreshComboOptions?.();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Exposed globally for use in contextmenu.js
|
|
window.kjNodes = window.kjNodes || {};
|
|
window.kjNodes.convertOutputsToSetGet = convertOutputsToSetGet;
|
|
window.kjNodes.snapshotSelectedNodes = snapshotSelectedNodes;
|
|
function convertOutputsToSetGet(graph, node) {
|
|
if (!graph || !node) return;
|
|
for (let slotIdx = 0; slotIdx < node.outputs.length; slotIdx++) {
|
|
const output = node.outputs[slotIdx];
|
|
if (!output.links || output.links.length === 0) continue;
|
|
|
|
const linkType = output.type || "*";
|
|
const linkName = output.name || linkType;
|
|
|
|
// Collect targets and link IDs to remove, skipping existing Set/Get nodes
|
|
const targets = [];
|
|
const linksToRemove = [];
|
|
for (const linkId of [...output.links]) {
|
|
const link = getLink(graph, linkId);
|
|
if (link) {
|
|
const targetNode = graph.getNodeById(link.target_id);
|
|
if (targetNode && targetNode.type !== 'SetNode' && targetNode.type !== 'GetNode') {
|
|
targets.push({ targetId: link.target_id, targetSlot: link.target_slot });
|
|
linksToRemove.push(linkId);
|
|
}
|
|
}
|
|
}
|
|
if (targets.length === 0) continue;
|
|
|
|
// Create Set node
|
|
const setNode = LiteGraph.createNode("SetNode");
|
|
if (!setNode) continue;
|
|
setNode.pos = [
|
|
node.pos[0] + node.size[0] + 30,
|
|
node.pos[1] + slotIdx * 60
|
|
];
|
|
graph.add(setNode);
|
|
setNode.flags.collapsed = true;
|
|
|
|
for (const linkId of linksToRemove) graph.removeLink(linkId);
|
|
|
|
// Connect source → Set node input
|
|
node.connect(slotIdx, setNode, 0);
|
|
|
|
// Set the name widget
|
|
setNode.widgets[0].value = linkName;
|
|
setNode.title = prefixedTitle("Set", linkName);
|
|
setNode.validateName(graph);
|
|
const finalName = setNode.widgets[0].value;
|
|
setNode.properties.previousName = finalName;
|
|
|
|
// Create a Get node for each target
|
|
for (const target of targets) {
|
|
const targetNode = graph.getNodeById(target.targetId);
|
|
if (!targetNode) continue;
|
|
|
|
const getNode = LiteGraph.createNode("GetNode");
|
|
if (!getNode) continue;
|
|
getNode.pos = [
|
|
targetNode.pos[0] - getNode.size[0] - 30,
|
|
targetNode.pos[1]
|
|
];
|
|
graph.add(getNode);
|
|
getNode.flags.collapsed = true;
|
|
|
|
getNode.widgets[0].value = finalName;
|
|
getNode.onRename();
|
|
|
|
getNode.connect(0, targetNode, target.targetSlot);
|
|
}
|
|
}
|
|
app.canvas.setDirty(true, true);
|
|
}
|
|
|
|
// Snapshot selection immediately (before right-click changes it)
|
|
function snapshotSelectedNodes(node, typeFilter) {
|
|
const selected = Object.values(app.canvas.selected_nodes || {});
|
|
// Always include the right-clicked node, even if selection state is inconsistent
|
|
const nodeSet = new Set(selected);
|
|
nodeSet.add(node);
|
|
let nodes = [...nodeSet];
|
|
if (typeFilter) nodes = nodes.filter(n => n.type === typeFilter);
|
|
return nodes;
|
|
}
|
|
|
|
// Tracks nodes flagged with has_errors so they can be cleared on the next alert.
|
|
// Always cleared before repopulating, so it only holds nodes from the most recent alert.
|
|
const _errorNodes = new Set();
|
|
function showAlert(message, nodes) {
|
|
const nodeList = nodes ? (Array.isArray(nodes) ? nodes : [nodes]) : [];
|
|
const nodeInfo = nodeList.map(n =>
|
|
`${n.title || n.type} [${Math.round(n.pos[0])}, ${Math.round(n.pos[1])}]`
|
|
).join(', ');
|
|
if (nodeList.length) {
|
|
// Clear previous error flags
|
|
for (const n of _errorNodes) n.has_errors = false;
|
|
_errorNodes.clear();
|
|
setTimeout(() => {
|
|
for (const n of nodeList) {
|
|
n.has_errors = true;
|
|
_errorNodes.add(n);
|
|
}
|
|
app.canvas?.setDirty(true, true);
|
|
}, 100);
|
|
}
|
|
app.extensionManager.toast.add({
|
|
severity: 'warn',
|
|
summary: "KJ Set/Get",
|
|
detail: nodeInfo ? `${message} — ${nodeInfo}` : message,
|
|
life: 5000,
|
|
});
|
|
}
|
|
function convertAllSetGetToLinks(graph) {
|
|
if (!graph) return;
|
|
const rootGraph = findRootGraph(graph);
|
|
|
|
// First pass: handle cross-graph pairs (must run before same-graph
|
|
// because converting cross-graph creates real links that replace
|
|
// the Set/Get, and same-graph pass would miss the removed nodes)
|
|
const allSetEntries = findAllNodesOfType(rootGraph, 'SetNode');
|
|
for (const { node: setNode, graph: setGraph } of [...allSetEntries]) {
|
|
const name = setNode.widgets[0].value;
|
|
if (!name) continue;
|
|
const allGetEntries = findGettersByName(rootGraph, name);
|
|
const crossGraphGetters = allGetEntries.filter(e => e.graph !== setGraph);
|
|
if (crossGraphGetters.length > 0) {
|
|
convertCrossGraphSetGet(setNode, setGraph, crossGraphGetters);
|
|
}
|
|
}
|
|
|
|
// Second pass: handle remaining same-graph pairs
|
|
for (const { node: setNode, graph: g } of [...findAllNodesOfType(rootGraph, 'SetNode')]) {
|
|
convertSetGetToLinks(g, setNode);
|
|
}
|
|
}
|
|
|
|
// Compat shim: older frontends may lack graph.getLink(), and _links may be a Map or
|
|
// plain object depending on the litegraph version. The == null check catches both null
|
|
// and undefined intentionally.
|
|
function getLink(graph, linkId) {
|
|
if (linkId == null) return null;
|
|
if (graph.getLink) return graph.getLink(linkId);
|
|
return graph._links instanceof Map ? graph._links.get(linkId) : graph._links?.[linkId] ?? null;
|
|
}
|
|
|
|
// Collect {targetId, targetSlot} from an output slot's links
|
|
function collectOutputConnections(graph, output) {
|
|
const connections = [];
|
|
if (output?.links) {
|
|
for (const linkId of [...output.links]) {
|
|
const link = getLink(graph, linkId);
|
|
if (link) connections.push({ targetId: link.target_id, targetSlot: link.target_slot });
|
|
}
|
|
}
|
|
return connections;
|
|
}
|
|
|
|
function convertCrossGraphSetGet(setNode, setGraph, crossGraphGetters) {
|
|
// Find what's connected to the SetNode's input
|
|
const setInput = setNode.inputs[0];
|
|
if (setInput.link == null) return;
|
|
const sourceLink = getLink(setGraph, setInput.link);
|
|
if (!sourceLink) return;
|
|
const sourceNode = setGraph.getNodeById(sourceLink.origin_id);
|
|
if (!sourceNode) return;
|
|
const sourceSlot = sourceLink.origin_slot;
|
|
const linkType = setNode.inputs[0].type || '*';
|
|
|
|
for (const { node: getter, graph: getterGraph } of crossGraphGetters) {
|
|
// Collect getter's downstream connections before removing it
|
|
const connections = collectOutputConnections(getterGraph, getter.outputs[0]);
|
|
if (connections.length === 0) {
|
|
getterGraph.remove(getter);
|
|
continue;
|
|
}
|
|
|
|
// Determine direction and create SubgraphInput or SubgraphOutput
|
|
const rootGraph = findRootGraph(setGraph);
|
|
const sgNodeForGetter = findSubgraphNodeFor(rootGraph, getter);
|
|
const sgNodeForSetter = findSubgraphNodeFor(rootGraph, setNode);
|
|
|
|
if (sgNodeForGetter && setGraph === rootGraph) {
|
|
// Set in root, Get in subgraph → create SubgraphInput
|
|
const subgraph = sgNodeForGetter.subgraph;
|
|
const inputName = setNode.widgets[0].value || linkType;
|
|
const newInput = subgraph.addInput(inputName, linkType);
|
|
const inputIndex = subgraph.inputs.indexOf(newInput);
|
|
|
|
// Connect source → SubgraphNode's new input in root graph
|
|
sourceNode.connect(sourceSlot, sgNodeForGetter, inputIndex);
|
|
|
|
// Inside subgraph: connect SubgraphInput slot → getter's targets
|
|
for (const conn of connections) {
|
|
if (conn.targetId === subgraph.outputNode?.id) {
|
|
// GetNode fed a SubgraphOutput — bypass it and connect source directly in parent graph.
|
|
const sgNodeOutput = sgNodeForGetter.outputs[conn.targetSlot];
|
|
if (sgNodeOutput?.links) {
|
|
for (const parentLinkId of [...sgNodeOutput.links]) {
|
|
const parentLink = getLink(rootGraph, parentLinkId);
|
|
if (parentLink) {
|
|
const parentTarget = rootGraph.getNodeById(parentLink.target_id);
|
|
if (parentTarget) {
|
|
sourceNode.connect(sourceSlot, parentTarget, parentLink.target_slot);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Remove the now-unnecessary SubgraphOutput
|
|
const sgOutput = subgraph.outputs[conn.targetSlot];
|
|
if (sgOutput) {
|
|
subgraph.removeOutput(sgOutput);
|
|
}
|
|
} else {
|
|
const targetNode = getterGraph.getNodeById(conn.targetId);
|
|
if (targetNode) {
|
|
newInput.connect(targetNode.inputs[conn.targetSlot], targetNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no connections used the SubgraphInput, remove it
|
|
if (newInput.linkIds.length === 0) {
|
|
subgraph.removeInput(newInput);
|
|
}
|
|
|
|
getterGraph.remove(getter);
|
|
|
|
} else if (sgNodeForSetter && getterGraph === rootGraph) {
|
|
// Set in subgraph, Get in root → create SubgraphOutput
|
|
const subgraph = sgNodeForSetter.subgraph;
|
|
const outputName = setNode.widgets[0].value || linkType;
|
|
const newOutput = subgraph.addOutput(outputName, linkType);
|
|
const outputIndex = subgraph.outputs.indexOf(newOutput);
|
|
|
|
// Inside subgraph: connect source output → SubgraphOutput slot
|
|
newOutput.connect(sourceNode.outputs[sourceSlot], sourceNode);
|
|
|
|
// In root graph: connect SubgraphNode's new output → getter's targets
|
|
for (const conn of connections) {
|
|
const targetNode = getterGraph.getNodeById(conn.targetId);
|
|
if (targetNode) {
|
|
sgNodeForSetter.connect(outputIndex, targetNode, conn.targetSlot);
|
|
}
|
|
}
|
|
|
|
getterGraph.remove(getter);
|
|
|
|
} else {
|
|
// Both in different subgraphs (sibling) — would need both input and output
|
|
// Skip for now, too complex
|
|
console.warn(`KJNodes: Cannot convert cross-graph Set/Get "${setNode.widgets[0].value}" between sibling subgraphs`);
|
|
}
|
|
}
|
|
|
|
// Remove SetNode if all its getters have been handled (check if any same-graph getters remain)
|
|
const remainingGetters = setGraph._nodes.filter(
|
|
n => n.type === 'GetNode' && n.widgets[0].value === setNode.widgets[0].value
|
|
);
|
|
if (remainingGetters.length === 0) {
|
|
setGraph.remove(setNode);
|
|
}
|
|
}
|
|
|
|
function convertSetGetToLinks(graph, setNode) {
|
|
if (!graph || !setNode) return;
|
|
|
|
const name = setNode.widgets[0].value;
|
|
let sameGraphGetters = [];
|
|
if (name) {
|
|
const allGetters = findGettersByName(graph, name);
|
|
const crossGetters = [];
|
|
for (const entry of allGetters) {
|
|
if (entry.graph === graph) sameGraphGetters.push(entry.node);
|
|
else crossGetters.push(entry);
|
|
}
|
|
if (crossGetters.length > 0) {
|
|
convertCrossGraphSetGet(setNode, graph, crossGetters);
|
|
}
|
|
}
|
|
|
|
// Find the source connected to the Set node's input
|
|
const setInput = setNode.inputs[0];
|
|
if (setInput.link == null) return;
|
|
const sourceLink = getLink(graph, setInput.link);
|
|
if (!sourceLink) return;
|
|
const sourceId = sourceLink.origin_id;
|
|
// Source may be the subgraph input boundary, which isn't in graph._nodes
|
|
if (!graph.getNodeById(sourceId) && sourceId !== graph.inputNode?.id) return;
|
|
const sourceSlot = sourceLink.origin_slot;
|
|
|
|
// Collect all consumer connections from Get nodes and SetNode's own output passthrough
|
|
const connections = [];
|
|
for (const getter of sameGraphGetters) {
|
|
connections.push(...collectOutputConnections(graph, getter.outputs[0]));
|
|
}
|
|
connections.push(...collectOutputConnections(graph, setNode.outputs[0]));
|
|
|
|
// Remove all Get nodes (this also removes their links)
|
|
for (const getter of sameGraphGetters) {
|
|
graph.remove(getter);
|
|
}
|
|
// Remove the Set node
|
|
graph.remove(setNode);
|
|
|
|
// Create direct links from source to each consumer
|
|
for (const conn of connections) {
|
|
connectById(graph, sourceId, sourceSlot, conn.targetId, conn.targetSlot);
|
|
}
|
|
|
|
app.canvas?.setDirty(true, true);
|
|
}
|
|
|
|
// region SetNode
|
|
app.registerExtension({
|
|
name: "KJNodes.SetNode",
|
|
registerCustomNodes() {
|
|
class SetNode extends LGraphNode {
|
|
static title = "Set";
|
|
static category = "KJNodes";
|
|
serialize_widgets = true;
|
|
drawConnection = false;
|
|
slotColor = "#FFF";
|
|
canvas = app.canvas;
|
|
|
|
constructor(title) {
|
|
super(title)
|
|
if (!this.properties) {
|
|
this.properties = {
|
|
"previousName": ""
|
|
};
|
|
}
|
|
this.properties["Node name for S&R"] = "SetNode";
|
|
this.properties["aux_id"] = "kijai/ComfyUI-KJNodes";
|
|
this.isVirtualNode = true; // This node is purely frontend and does not impact the resulting prompt so should not be serialized
|
|
|
|
this.addWidget(
|
|
"text",
|
|
"Constant",
|
|
'',
|
|
() => {
|
|
if (!this.graph || app.configuringGraph) return;
|
|
this.validateName(this.graph);
|
|
if (this.widgets[0].value !== '') {
|
|
this.title = prefixedTitle("Set", this.widgets[0].value);
|
|
}
|
|
this.update();
|
|
this.properties.previousName = this.widgets[0].value;
|
|
},
|
|
{}
|
|
)
|
|
|
|
this.addInput("*", "*");
|
|
this.addOutput("*", '*');
|
|
}
|
|
|
|
onConnectionsChange(
|
|
slotType,
|
|
slot,
|
|
isChangeConnect,
|
|
link_info
|
|
) {
|
|
// During graph load, slots are restored by configure — skip side effects
|
|
if (app.configuringGraph) return;
|
|
//On Disconnect
|
|
if (slotType === LiteGraph.INPUT && !isChangeConnect) {
|
|
const outputConnected = this.outputs[0]?.links?.length > 0;
|
|
if (outputConnected) {
|
|
this.inputs[slot].type = this.outputs[0].type;
|
|
this.inputs[slot].name = this.outputs[0].name;
|
|
} else {
|
|
this.inputs[slot].type = '*';
|
|
this.inputs[slot].name = '*';
|
|
this.outputs[0].type = '*';
|
|
this.outputs[0].name = '*';
|
|
this.title = "Set";
|
|
this.color = null;
|
|
this.bgcolor = null;
|
|
}
|
|
this.update();
|
|
}
|
|
if (slotType === LiteGraph.OUTPUT && !isChangeConnect) {
|
|
if (this.outputs && this.outputs[slot]) {
|
|
// Keep type if input has a real connection
|
|
const inputConnected = this.inputs[0]?.link != null;
|
|
if (inputConnected) {
|
|
this.outputs[slot].type = this.inputs[0].type;
|
|
this.outputs[slot].name = this.inputs[0].name;
|
|
} else {
|
|
this.inputs[0].type = '*';
|
|
this.inputs[0].name = '*';
|
|
this.outputs[slot].type = '*';
|
|
this.outputs[slot].name = '*';
|
|
this.color = null;
|
|
this.bgcolor = null;
|
|
}
|
|
}
|
|
}
|
|
//On Connect
|
|
if (link_info && this.graph && slotType === LiteGraph.INPUT && isChangeConnect) {
|
|
const resolve = link_info.resolve(this.graph)
|
|
const resolvedSlot = resolve?.subgraphInput ?? resolve?.output;
|
|
const type = resolvedSlot?.type;
|
|
if (type) {
|
|
if (this.title === "Set"){
|
|
this.title = prefixedTitle("Set", type);
|
|
}
|
|
if (this.widgets[0].value === '' || this.widgets[0].value === '*'){
|
|
// Determine the initial widget value based on naming setting
|
|
const namingMode = app.ui.settings.getSettingValue("KJNodes.setGetNaming") ?? "empty";
|
|
if (namingMode !== "empty") {
|
|
const link = getLink(this.graph, this.inputs[0]?.link);
|
|
const sourceNode = link ? this.graph.getNodeById(link.origin_id) : null;
|
|
const slotName = sourceNode?.outputs?.[link?.origin_slot]?.name || type;
|
|
switch (namingMode) {
|
|
case "slot name": this.widgets[0].value = slotName; break;
|
|
case "slot name (lowercase)": this.widgets[0].value = slotName.toLowerCase(); break;
|
|
case "slot name (UPPERCASE)": this.widgets[0].value = slotName.toUpperCase(); break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.validateName(this.graph);
|
|
this.properties.previousName = this.widgets[0].value;
|
|
this.inputs[0].type = type;
|
|
this.inputs[0].name = type;
|
|
this.outputs[0].type = type;
|
|
this.outputs[0].name = type;
|
|
|
|
autoColor(this, type);
|
|
} else {
|
|
showAlert(`node ${this.title} input undefined.`, this)
|
|
}
|
|
}
|
|
if (link_info && this.graph && slotType === LiteGraph.OUTPUT && isChangeConnect) {
|
|
const inputType = this.inputs[0]?.type;
|
|
if (inputType && inputType !== '*') {
|
|
this.outputs[0].type = inputType;
|
|
this.outputs[0].name = inputType;
|
|
} else {
|
|
const resolve = link_info.resolve(this.graph);
|
|
const type = resolve?.input?.type;
|
|
if (type && type !== '*') {
|
|
this.inputs[0].type = type;
|
|
this.inputs[0].name = type;
|
|
this.outputs[0].type = type;
|
|
this.outputs[0].name = type;
|
|
autoColor(this, type);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Update either way
|
|
this.update();
|
|
}
|
|
|
|
// Returns true if the name was changed
|
|
validateName(graph, sameGraphOnly) {
|
|
let widgetValue = this.widgets[0].value;
|
|
|
|
if (widgetValue !== '') {
|
|
let tries = 0;
|
|
const existingValues = new Set();
|
|
|
|
// sameGraphOnly: only check the immediate graph (for paste/clone)
|
|
// otherwise check full scope (own graph + ancestors)
|
|
const scopeGraphs = sameGraphOnly ? [graph] : getGraphAncestors(graph);
|
|
const scopedSetNodes = collectNodesOfType(scopeGraphs, 'SetNode');
|
|
scopedSetNodes.forEach(entry => {
|
|
if (entry.node !== this) {
|
|
existingValues.add(entry.node.widgets[0].value);
|
|
}
|
|
});
|
|
|
|
const originalValue = widgetValue;
|
|
// Only strip _N suffix during paste to avoid FOO_0_1_2 accumulation.
|
|
// For manual renames, keep the full name as base (user may intend FOO_3).
|
|
const baseName = this._justAdded ? widgetValue.replace(/_\d+$/, '') : widgetValue;
|
|
while (existingValues.has(widgetValue)) {
|
|
widgetValue = baseName + "_" + tries;
|
|
tries++;
|
|
}
|
|
|
|
this.widgets[0].value = widgetValue;
|
|
this.title = prefixedTitle("Set", widgetValue);
|
|
return widgetValue !== originalValue;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
clone() {
|
|
const cloned = super.clone();
|
|
cloned.inputs[0].name = '*';
|
|
cloned.inputs[0].type = '*';
|
|
cloned.properties.previousName = '';
|
|
cloned.size = cloned.computeSize();
|
|
return cloned;
|
|
}
|
|
|
|
onAdded() {
|
|
this._justAdded = true;
|
|
// Vue mode doesn't always re-extract GetNode options on SetNode add; skip during workflow load.
|
|
if (LiteGraph.vueNodesMode && this.graph && !app.configuringGraph) {
|
|
refreshAllGetNodeCombos(this.graph);
|
|
}
|
|
}
|
|
|
|
onRemoved() {
|
|
// Only Vue mode needs the refresh — legacy re-reads the values getter on every click.
|
|
if (!LiteGraph.vueNodesMode) return;
|
|
const g = this.graph;
|
|
if (!g) return;
|
|
// Defer: onRemoved fires before _nodes is spliced, so getters still see this SetNode.
|
|
setTimeout(() => refreshAllGetNodeCombos(g), 0);
|
|
}
|
|
|
|
onConfigure() {
|
|
// Only run paste logic when actually pasting, not during workflow load
|
|
if (this._justAdded && this.graph && !app.configuringGraph) {
|
|
const oldName = this.widgets[0].value;
|
|
this.validateName(this.graph, true);
|
|
this._justAdded = false;
|
|
const newName = this.widgets[0].value;
|
|
if (newName !== oldName) {
|
|
_pasteRenameMap.set(oldName, newName);
|
|
// Clear the map after this paste cycle
|
|
setTimeout(() => _pasteRenameMap.delete(oldName), 0);
|
|
}
|
|
// Reset type and color on paste — nothing is connected yet
|
|
if (this.inputs[0]?.link == null) {
|
|
this.inputs[0].type = '*';
|
|
this.inputs[0].name = '*';
|
|
this.outputs[0].type = '*';
|
|
this.outputs[0].name = '*';
|
|
this.color = null;
|
|
this.bgcolor = null;
|
|
}
|
|
}
|
|
this._justAdded = false;
|
|
}
|
|
|
|
update() {
|
|
if (!this.graph) return;
|
|
|
|
const getters = this.findGetters(this.graph);
|
|
getters.forEach(getter => {
|
|
getter.setType(this.inputs[0].type);
|
|
});
|
|
|
|
if (this.widgets[0].value && this.properties.previousName) {
|
|
const gettersWithPreviousName = this.findGetters(this.graph, true);
|
|
gettersWithPreviousName.forEach(getter => {
|
|
getter.setName(this.widgets[0].value);
|
|
});
|
|
}
|
|
app.canvas?.setDirty(true, true);
|
|
}
|
|
|
|
findGetters(graph, checkForPreviousName) {
|
|
const name = checkForPreviousName ? this.properties.previousName : this.widgets[0].value;
|
|
if (!name || name === '') return [];
|
|
// Scoped: searches own graph + descendant subgraphs
|
|
return findGettersByName(graph, name).map(entry => entry.node);
|
|
}
|
|
|
|
getExtraMenuOptions(_, options) {
|
|
options.unshift(
|
|
{
|
|
content: this.drawConnection ? "Hide connections" : "Show connections",
|
|
callback: () => {
|
|
this.drawConnection = !this.drawConnection;
|
|
const linkType = this.inputs[0].type;
|
|
this.slotColor = this.canvas.default_connection_color_byType[linkType];
|
|
this.canvas.setDirty(true, true);
|
|
},
|
|
},
|
|
{
|
|
content: "Hide all connections",
|
|
callback: () => {
|
|
for (const n of this.graph._nodes) {
|
|
if (n.type === "GetNode" || n.type === "SetNode") n.drawConnection = false;
|
|
}
|
|
this.canvas.setDirty(true, true);
|
|
},
|
|
|
|
},
|
|
);
|
|
options.unshift({
|
|
content: "Convert to links",
|
|
callback: () => {
|
|
const graph = this.graph;
|
|
for (const n of snapshotSelectedNodes(this, 'SetNode')) convertSetGetToLinks(graph, n);
|
|
},
|
|
});
|
|
options.unshift({
|
|
content: "Add paired GetNode",
|
|
callback: () => {
|
|
const graph = this.graph;
|
|
const getNode = LiteGraph.createNode("GetNode");
|
|
if (!getNode) return;
|
|
getNode.pos = [this.pos[0] + this.size[0] + 30, this.pos[1]];
|
|
graph.add(getNode);
|
|
// Set the widget value to match — this drives type, color, and connection
|
|
const name = this.widgets[0].value;
|
|
if (getNode.widgets?.[0]) {
|
|
getNode.widgets[0].value = name;
|
|
}
|
|
getNode.onRename?.();
|
|
app.canvas.selectNode(getNode, false);
|
|
app.canvas.setDirty(true, true);
|
|
},
|
|
});
|
|
// Dynamically add a submenu for all getters
|
|
const allGettersForMenu = this.findGetters(this.graph);
|
|
if (allGettersForMenu.length) {
|
|
let gettersSubmenu = allGettersForMenu.map(getter => {
|
|
const sameGraph = getter.graph === this.graph;
|
|
const sgNode = !sameGraph ? findSubgraphNodeFor(this.graph, getter) : null;
|
|
const label = sameGraph
|
|
? `${getter.title} id: ${getter.id}`
|
|
: `${getter.title} (in subgraph${sgNode ? ': ' + (sgNode.title || sgNode.type) : ''})`;
|
|
return {
|
|
content: label,
|
|
callback: () => {
|
|
if (sameGraph) {
|
|
this.canvas.centerOnNode(getter);
|
|
this.canvas.selectNode(getter, false);
|
|
} else if (sgNode) {
|
|
this.canvas.openSubgraph(sgNode.subgraph, sgNode);
|
|
// Center on the getter after entering the subgraph
|
|
setTimeout(() => {
|
|
this.canvas.centerOnNode(getter);
|
|
this.canvas.selectNode(getter, false);
|
|
}, 0);
|
|
}
|
|
this.canvas.setDirty(true, true);
|
|
},
|
|
};
|
|
});
|
|
|
|
options.unshift({
|
|
content: "Getters",
|
|
has_submenu: true,
|
|
submenu: {
|
|
title: "GetNodes",
|
|
options: gettersSubmenu,
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
LiteGraph.registerNodeType("SetNode", SetNode);
|
|
},
|
|
});
|
|
|
|
// region GetNode
|
|
app.registerExtension({
|
|
name: "KJNodes.GetNode",
|
|
registerCustomNodes() {
|
|
class GetNode extends LGraphNode {
|
|
static title = "Get";
|
|
static category = "KJNodes";
|
|
serialize_widgets = true;
|
|
drawConnection = false;
|
|
currentSetter = null;
|
|
canvas = app.canvas;
|
|
|
|
constructor(title) {
|
|
super(title)
|
|
if (!this.properties) {
|
|
this.properties = {};
|
|
}
|
|
this.properties["Node name for S&R"] = "GetNode";
|
|
this.properties["aux_id"] = "kijai/ComfyUI-KJNodes";
|
|
this.isVirtualNode = true; // This node is purely frontend and does not impact the resulting prompt so should not be serialized
|
|
const comboOptions = {
|
|
getOptionLabel: (value) => {
|
|
if (!value) return "";
|
|
const source = _setNameSourceMap.get(value);
|
|
if (!source || source === "local") return value;
|
|
return `${value} (${source})`;
|
|
},
|
|
};
|
|
Object.defineProperty(comboOptions, 'values', {
|
|
get: () => {
|
|
if (!this.graph) return [];
|
|
let filterType = null;
|
|
if (app.ui.settings.getSettingValue("KJNodes.filterGetNodeOptions") !== false
|
|
&& this.outputs[0]?.links?.length) {
|
|
const linkId = this.outputs[0].links[0];
|
|
const link = getLink(this.graph, linkId);
|
|
if (link) {
|
|
const targetNode = this.graph.getNodeById(link.target_id);
|
|
filterType = targetNode?.inputs?.[link.target_slot]?.type || null;
|
|
}
|
|
}
|
|
return getVisibleSetNames(this.graph, filterType);
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
const constantWidget = this.addWidget("combo", "Constant", "", () => { if (!app.configuringGraph) this.onRename(); }, comboOptions);
|
|
// Legacy-only fix: pass pre-labeled values to ContextMenu so the core filter shows the search input.
|
|
// Sidesteps ComboWidget's empty-array + getOptionLabel bug. Vue mode handles it natively.
|
|
const origOnClick = constantWidget.onClick?.bind(constantWidget);
|
|
constantWidget.onClick = (params) => {
|
|
if (LiteGraph.vueNodesMode) return origOnClick?.(params);
|
|
const { e, canvas, node } = params;
|
|
const x = e.canvasX - node.pos[0];
|
|
const width = constantWidget.width || node.size[0];
|
|
if (x < 40) return constantWidget.decrementValue({ e, node, canvas });
|
|
if (x > width - 40) return constantWidget.incrementValue({ e, node, canvas });
|
|
const rawValues = comboOptions.values;
|
|
const labels = rawValues.map(v => comboOptions.getOptionLabel(v) || v);
|
|
const menu = new LiteGraph.ContextMenu(labels, {
|
|
scale: Math.max(1, canvas.ds.scale),
|
|
event: e,
|
|
className: 'dark',
|
|
callback: (selectedLabel) => {
|
|
const idx = labels.indexOf(selectedLabel);
|
|
if (idx >= 0) constantWidget.setValue(rawValues[idx], { e, node, canvas });
|
|
}
|
|
});
|
|
// Color each entry's left border by its SetNode's type.
|
|
if (!_typeColorMap) setColorAndBgColor({}, ''); // ensure lazy map is initialized
|
|
const entries = menu.root?.querySelectorAll('.litemenu-entry');
|
|
rawValues.forEach((name, i) => {
|
|
if (!entries?.[i]) return;
|
|
const setter = findSetterByName(this.graph, name);
|
|
const type = setter?.node?.inputs?.[0]?.type;
|
|
const c = _typeColorMap?.[type];
|
|
const borderColor = canvas.default_connection_color_byType?.[type]
|
|
|| LGraphCanvas.link_type_colors?.[type]
|
|
|| (c && (c.groupcolor || c.bgcolor || c.color))
|
|
|| '#888';
|
|
entries[i].style.borderLeft = `4px solid ${borderColor}`;
|
|
entries[i].style.paddingLeft = '8px';
|
|
});
|
|
};
|
|
// Fresh options object (live getter preserved) + remove/re-add to force Vue re-extraction.
|
|
this._refreshComboOptions = () => {
|
|
const w = this.widgets?.[0];
|
|
if (!w) return;
|
|
const newOpts = { getOptionLabel: comboOptions.getOptionLabel };
|
|
Object.defineProperty(newOpts, 'values',
|
|
Object.getOwnPropertyDescriptor(comboOptions, 'values'));
|
|
w.options = newOpts;
|
|
const idx = this.widgets.indexOf(w);
|
|
if (idx >= 0) {
|
|
this.widgets.splice(idx, 1);
|
|
this.widgets.splice(idx, 0, w);
|
|
}
|
|
};
|
|
this.addOutput("*", '*');
|
|
}
|
|
|
|
// A bypassed node passes its input through, but GetNode has no inputs, so a bypassed
|
|
// group would drop the value. While building the prompt, report BYPASS as ALWAYS.
|
|
get mode() {
|
|
const mode = super.mode;
|
|
return _resolvingPrompt > 0 && mode === MODE_BYPASS ? MODE_ALWAYS : mode;
|
|
}
|
|
set mode(value) {
|
|
super.mode = value;
|
|
}
|
|
|
|
onConnectionsChange() {
|
|
if (app.configuringGraph) return;
|
|
this.validateLinks();
|
|
}
|
|
|
|
setName(name) {
|
|
this.widgets[0].value = name;
|
|
this.onRename();
|
|
this.serialize();
|
|
}
|
|
|
|
onRename() {
|
|
const setter = this.findSetter(this.graph);
|
|
if (setter) {
|
|
this.setType(setter.inputs[0].type);
|
|
this.title = prefixedTitle("Get", setter.widgets[0].value);
|
|
} else {
|
|
this.setType('*');
|
|
const name = this.widgets[0].value;
|
|
this.title = name ? prefixedTitle("Get", name) : "Get";
|
|
}
|
|
app.canvas?.setDirty(true, true);
|
|
}
|
|
|
|
clone() {
|
|
const cloned = super.clone();
|
|
cloned.size = cloned.computeSize();
|
|
return cloned;
|
|
}
|
|
|
|
validateLinks() {
|
|
if (this.outputs[0].type !== '*' && this.outputs[0].links && this.graph) {
|
|
this.outputs[0].links.filter(linkId => {
|
|
const link = getLink(this.graph, linkId);
|
|
if (!link || !link.type) return false;
|
|
if (link.type === '*') return false;
|
|
const targetNode = this.graph.getNodeById(link.target_id);
|
|
const targetType = targetNode?.inputs?.[link.target_slot]?.type;
|
|
if (targetType === '*') return false;
|
|
if (targetType) {
|
|
const targetTypes = String(targetType).split(",");
|
|
if (targetTypes.includes(this.outputs[0].type)) return false;
|
|
}
|
|
return !link.type.split(",").includes(this.outputs[0].type);
|
|
}).forEach(linkId => {
|
|
this.graph.removeLink(linkId);
|
|
});
|
|
}
|
|
}
|
|
|
|
setType(type) {
|
|
this.outputs[0].name = type;
|
|
this.outputs[0].type = type;
|
|
this.validateLinks();
|
|
autoColor(this, type);
|
|
}
|
|
|
|
findSetter(graph) {
|
|
const name = this.widgets[0].value;
|
|
// Scoped: searches own graph first, then ancestors
|
|
const result = findSetterByName(graph, name);
|
|
return result ? result.node : undefined;
|
|
}
|
|
|
|
goToSetter() {
|
|
if (!this.currentSetter) return;
|
|
const setterGraph = this.currentSetter.graph;
|
|
if (setterGraph && setterGraph !== this.graph) {
|
|
this.canvas.setGraph(setterGraph);
|
|
setTimeout(() => {
|
|
this.canvas.centerOnNode(this.currentSetter);
|
|
this.canvas.selectNode(this.currentSetter, false);
|
|
this.canvas.setDirty(true, true);
|
|
}, 0);
|
|
} else {
|
|
this.canvas.centerOnNode(this.currentSetter);
|
|
this.canvas.selectNode(this.currentSetter, false);
|
|
}
|
|
}
|
|
|
|
onAdded() {
|
|
this._justAdded = true;
|
|
}
|
|
|
|
onConfigure() {
|
|
if (this._justAdded && !app.configuringGraph) {
|
|
const name = this.widgets[0].value;
|
|
if (name) {
|
|
// Check if our paired SetNode was renamed during this paste
|
|
const newName = _pasteRenameMap.get(name);
|
|
if (newName) {
|
|
this.widgets[0].value = newName;
|
|
}
|
|
// Restore type/color from setter after paste
|
|
setTimeout(() => this.onRename(), 0);
|
|
}
|
|
}
|
|
this._justAdded = false;
|
|
}
|
|
|
|
getInputLink(slot) {
|
|
// Same-graph: standard resolveOutput path.
|
|
// Cross-graph: handled by resolveVirtualOutput.
|
|
const name = this.widgets[0].value;
|
|
if (!name || name === '') return null;
|
|
const setter = this.graph?._nodes?.find(
|
|
n => n.type === 'SetNode' && n.widgets[0].value === name
|
|
);
|
|
if (setter) {
|
|
const slotInfo = setter.inputs[slot];
|
|
if (!slotInfo || slotInfo.link == null) return null;
|
|
return getLink(this.graph, slotInfo.link);
|
|
}
|
|
// Scoped lookup: no setter found in own graph or ancestors
|
|
if (name && !findSetterByName(this.graph, name)) {
|
|
showAlert("No SetNode found for " + name + "(" + this.type + ")", this);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
resolveVirtualOutput(slot) {
|
|
const name = this.widgets[0].value;
|
|
// Scoped lookup: own graph first, then ancestors
|
|
const result = findSetterByName(this.graph, name);
|
|
if (!result) return undefined;
|
|
|
|
// Same graph — let the standard getInputLink path handle it
|
|
if (result.graph === this.graph) return undefined;
|
|
|
|
// Warn if multiple SetNodes with this name exist in scope
|
|
const scopeGraphs = getGraphAncestors(this.graph);
|
|
const scopedSetters = collectNodesOfType(scopeGraphs, 'SetNode')
|
|
.filter(e => e.node.widgets[0].value === name);
|
|
if (scopedSetters.length > 1) {
|
|
showAlert(`Multiple SetNodes named "${name}" found in scope. Rename duplicates or use "Convert to links" to resolve`, [this, ...scopedSetters.map(e => e.node)]);
|
|
return undefined;
|
|
}
|
|
|
|
const { node: setter, graph: setterGraph } = result;
|
|
const slotInfo = setter.inputs[slot];
|
|
if (!slotInfo || slotInfo.link == null) return undefined;
|
|
|
|
const link = getLink(setterGraph, slotInfo.link);
|
|
if (!link) return undefined;
|
|
|
|
const sourceNode = setterGraph.getNodeById(link.origin_id);
|
|
if (!sourceNode) return undefined;
|
|
|
|
return { node: sourceNode, slot: link.origin_slot };
|
|
}
|
|
getExtraMenuOptions(_, options) {
|
|
this.currentSetter = this.findSetter(this.graph)
|
|
if (!this.currentSetter) return
|
|
const sameGraph = this.currentSetter.graph === this.graph;
|
|
if (sameGraph) {
|
|
let menuEntry = this.drawConnection ? "Hide connections" : "Show connections";
|
|
options.unshift(
|
|
{
|
|
content: "Convert to links",
|
|
callback: () => {
|
|
const graph = this.graph;
|
|
const setters = new Set(snapshotSelectedNodes(this, 'GetNode')
|
|
.map(n => n.findSetter?.(graph)).filter(Boolean));
|
|
for (const s of setters) convertSetGetToLinks(graph, s);
|
|
},
|
|
},
|
|
{
|
|
content: "Go to setter",
|
|
callback: () => {
|
|
this.goToSetter();
|
|
},
|
|
},
|
|
{
|
|
content: menuEntry,
|
|
callback: () => {
|
|
if (!this.currentSetter) return;
|
|
const linkType = this.currentSetter.inputs[0].type;
|
|
// Toggle on the SetNode so the canvas-level hook draws it
|
|
this.currentSetter.drawConnection = !this.currentSetter.drawConnection;
|
|
this.currentSetter.slotColor = this.canvas.default_connection_color_byType[linkType];
|
|
this.drawConnection = this.currentSetter.drawConnection;
|
|
this.canvas.setDirty(true, true);
|
|
},
|
|
},
|
|
);
|
|
} else {
|
|
// Cross-graph setter — navigate to its graph
|
|
const setterGraph = this.currentSetter.graph;
|
|
const isRoot = setterGraph === findRootGraph(this.graph);
|
|
options.unshift(
|
|
{
|
|
content: `Go to setter (in ${isRoot ? 'parent graph' : 'subgraph'})`,
|
|
callback: () => {
|
|
const canvas = this.canvas;
|
|
canvas.setGraph(setterGraph);
|
|
setTimeout(() => {
|
|
canvas.centerOnNode(this.currentSetter);
|
|
canvas.selectNode(this.currentSetter, false);
|
|
canvas.setDirty(true, true);
|
|
}, 0);
|
|
},
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
LiteGraph.registerNodeType("GetNode", GetNode);
|
|
},
|
|
});
|
|
|
|
// region UI: commands, keybindings, canvas menu, draw hook, settings
|
|
app.registerExtension({
|
|
name: "KJNodes.SetGetUI",
|
|
settings: [
|
|
{
|
|
id: "KJNodes.setGetNaming",
|
|
name: "Default SetNode widget value",
|
|
category: ["KJNodes", "Set & Get", "Default SetNode widget value"],
|
|
tooltip: "Initial Constant value when a Set node is first connected to a slot",
|
|
type: "combo",
|
|
options: ["empty", "slot name", "slot name (lowercase)", "slot name (UPPERCASE)"],
|
|
defaultValue: "empty",
|
|
},
|
|
{
|
|
id: "KJNodes.showSetGetLinks",
|
|
name: "Show links",
|
|
category: ["KJNodes", "Set & Get", "Show links"],
|
|
tooltip: "When to show virtual links between Set/Get pairs",
|
|
type: "combo",
|
|
options: ["never", "selected", "always"],
|
|
defaultValue: "never",
|
|
onChange: () => app.canvas?.setDirty(true, true),
|
|
},
|
|
{
|
|
id: "KJNodes.nodeAutoColor",
|
|
name: "Auto-color nodes",
|
|
category: ["KJNodes", "Set & Get", "Auto-color nodes"],
|
|
tooltip: "Automatically color Set/Get nodes based on their connection type",
|
|
type: "boolean",
|
|
defaultValue: true,
|
|
},
|
|
{
|
|
id: "KJNodes.disablePrefix",
|
|
name: "Disable Set_/Get_ prefix",
|
|
category: ["KJNodes", "Set & Get", "Disable Set_/Get_ prefix"],
|
|
tooltip: "Prevents automatically adding Set_ and Get_ prefixes to node titles",
|
|
defaultValue: false,
|
|
type: "boolean",
|
|
},
|
|
{
|
|
id: "KJNodes.shiftMiddleClickSetGet",
|
|
name: "Shift+middle-click creates Set/Get",
|
|
category: ["KJNodes", "Set & Get", "Shift+middle-click creates Set/Get"],
|
|
tooltip: "Shift+middle-click on a slot creates a connected SetNode (output) or GetNode (input) instead of Reroute",
|
|
type: "boolean",
|
|
defaultValue: true,
|
|
},
|
|
{
|
|
id: "KJNodes.middleClickSetGet",
|
|
name: "Middle-click creates Set/Get",
|
|
category: ["KJNodes", "Set & Get", "Middle-click creates Set/Get"],
|
|
tooltip: "Middle-click on a slot creates Set/Get instead of Reroute (overrides core behavior)",
|
|
type: "boolean",
|
|
defaultValue: false,
|
|
},
|
|
],
|
|
commands: [
|
|
{
|
|
id: "KJNodes.AddSetNodeToSelected",
|
|
label: "Add Set node to selected / at cursor",
|
|
function: () => addNodeToSelectedOrCursor("SetNode", "right"),
|
|
},
|
|
{
|
|
id: "KJNodes.AddGetNodeAtCursor",
|
|
label: "Add Get node to selected / at cursor",
|
|
function: () => addNodeToSelectedOrCursor("GetNode", "left"),
|
|
},
|
|
{
|
|
id: "KJNodes.ToggleForceShowSetGetLinks",
|
|
label: "Toggle force-show all Set/Get connections",
|
|
function: () => {
|
|
_forceShowAllLinks = !_forceShowAllLinks;
|
|
app.canvas.setDirty(true, true);
|
|
},
|
|
},
|
|
],
|
|
keybindings: [
|
|
{
|
|
commandId: "KJNodes.AddSetNodeToSelected",
|
|
combo: { key: "s", ctrl: true, shift: true },
|
|
targetElementId: "graph-canvas",
|
|
},
|
|
{
|
|
commandId: "KJNodes.AddGetNodeAtCursor",
|
|
combo: { key: "g", ctrl: true, shift: true },
|
|
targetElementId: "graph-canvas",
|
|
},
|
|
{
|
|
commandId: "KJNodes.ToggleForceShowSetGetLinks",
|
|
combo: { key: "l", ctrl: true, shift: true },
|
|
targetElementId: "graph-canvas",
|
|
},
|
|
],
|
|
getCanvasMenuItems() {
|
|
return [
|
|
{
|
|
content: "KJNodes",
|
|
has_submenu: true,
|
|
submenu: {
|
|
options: [
|
|
{
|
|
content: "Convert outputs on all selected nodes to Set/Get",
|
|
callback: () => {
|
|
const selected = Object.values(app.canvas.selected_nodes || {});
|
|
if (selected.length === 0) return;
|
|
for (const n of selected) convertOutputsToSetGet(n.graph, n);
|
|
},
|
|
},
|
|
{
|
|
content: "Convert selected Set/Get to links",
|
|
callback: () => {
|
|
const selected = Object.values(app.canvas.selected_nodes || {});
|
|
const setNodes = selected.filter(n => n.type === 'SetNode');
|
|
const getNodes = selected.filter(n => n.type === 'GetNode');
|
|
for (const n of setNodes) convertSetGetToLinks(n.graph, n);
|
|
// Setter may live in an ancestor graph
|
|
const setters = new Set(getNodes.map(n => n.findSetter?.(n.graph)).filter(Boolean));
|
|
for (const s of setters) convertSetGetToLinks(s.graph, s);
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
];
|
|
},
|
|
setup() {
|
|
// Double-click GetNode to jump to its SetNode (works in both legacy and Vue modes)
|
|
document.addEventListener("dblclick", (e) => {
|
|
// Don't interfere when a ContextMenu (e.g. combo dropdown) is open
|
|
if (document.querySelector(".litecontextmenu")) return;
|
|
const canvas = app.canvas;
|
|
if (!canvas) return;
|
|
const selected = Object.values(canvas.selected_nodes || {});
|
|
if (selected.length !== 1) return;
|
|
const node = selected[0];
|
|
if (node.type === "GetNode") {
|
|
node.currentSetter = node.findSetter(node.graph);
|
|
if (node.currentSetter) node.goToSetter();
|
|
}
|
|
});
|
|
// Suppress title rename on collapsed GetNodes so double-click navigates instead
|
|
document.addEventListener("litegraph:canvas", (e) => {
|
|
if (e.detail?.subType !== 'node-double-click') return;
|
|
const node = e.detail.node;
|
|
if (node?.type === "GetNode" && node.flags?.collapsed) {
|
|
e.stopImmediatePropagation();
|
|
}
|
|
});
|
|
|
|
// Monkey-patch: no extension hook exists for filtering visible nodes.
|
|
// Keep Set/Get nodes with visible virtual connections in the visible set
|
|
// even when offscreen, so the link lines don't get clipped.
|
|
const originalComputeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes;
|
|
LGraphCanvas.prototype.computeVisibleNodes = function () {
|
|
const visible = originalComputeVisibleNodes.apply(this, arguments);
|
|
for (const node of this.graph._nodes) {
|
|
if ((node.type === "SetNode" || node.type === "GetNode") && node.drawConnection && !visible.includes(node)) {
|
|
visible.push(node);
|
|
}
|
|
}
|
|
return visible;
|
|
};
|
|
|
|
// Monkey-patch: no extension hook exists for custom canvas drawing.
|
|
// Draws virtual links between Set/Get pairs.
|
|
// Handles per-node "Show connections", "selected" mode, and "always" mode.
|
|
const origCanvasOnDrawBackground = app.canvas.onDrawBackground;
|
|
app.canvas.onDrawBackground = function(ctx, visibleArea) {
|
|
origCanvasOnDrawBackground?.call(this, ctx, visibleArea);
|
|
|
|
const graph = this.graph || app.graph;
|
|
if (!graph?._nodes) return;
|
|
const mode = getShowLinksMode();
|
|
|
|
// Build set of selected Set/Get names for "selected" mode
|
|
let selectedNames = null;
|
|
if (mode === "selected") {
|
|
const sel = Object.values(this.selected_nodes || {});
|
|
if (!sel.length) {
|
|
// No selection — still honor per-node drawConnection
|
|
if (!graph._nodes.some(n => n.type === 'SetNode' && n.drawConnection)) return;
|
|
} else {
|
|
selectedNames = new Set();
|
|
for (const n of sel) {
|
|
if (n.type === 'SetNode' || n.type === 'GetNode') {
|
|
selectedNames.add(n.widgets?.[0]?.value ?? n.title);
|
|
}
|
|
}
|
|
if (!selectedNames.size && !graph._nodes.some(n => n.type === 'SetNode' && n.drawConnection)) return;
|
|
}
|
|
} else if (mode === "never") {
|
|
if (!graph._nodes.some(n => n.type === 'SetNode' && n.drawConnection)) return;
|
|
}
|
|
|
|
// Collect all GetNodes once across current graph + descendants, grouped by name.
|
|
// This avoids re-discovering descendant graphs and re-scanning nodes per SetNode.
|
|
const gettersByName = new Map();
|
|
const scopeGraphs = [graph, ...getGraphDescendants(graph)];
|
|
for (const g of scopeGraphs) {
|
|
if (!g?._nodes) continue;
|
|
for (const node of g._nodes) {
|
|
if (node.type !== 'GetNode') continue;
|
|
const n = node.widgets?.[0]?.value;
|
|
if (!n) continue;
|
|
let list = gettersByName.get(n);
|
|
if (!list) { list = []; gettersByName.set(n, list); }
|
|
list.push(node);
|
|
}
|
|
}
|
|
|
|
for (const setNode of graph._nodes) {
|
|
if (setNode.type !== 'SetNode') continue;
|
|
const name = setNode.widgets?.[0]?.value ?? setNode.title;
|
|
const showByMode = mode === "always" || (mode === "selected" && selectedNames?.has(name));
|
|
if (!showByMode && !setNode.drawConnection) continue;
|
|
|
|
const allGetters = gettersByName.get(name) || [];
|
|
const drawTargets = [];
|
|
const seenSubgraphs = new Set();
|
|
for (const getter of allGetters) {
|
|
if (getter.graph === graph) {
|
|
drawTargets.push(getter);
|
|
} else {
|
|
const sgNode = findSubgraphNodeFor(graph, getter);
|
|
if (sgNode && !seenSubgraphs.has(sgNode)) {
|
|
seenSubgraphs.add(sgNode);
|
|
drawTargets.push(sgNode);
|
|
}
|
|
}
|
|
}
|
|
if (!drawTargets.length) continue;
|
|
|
|
const linkType = setNode.inputs[0]?.type;
|
|
const slotColor = app.canvas.default_connection_color_byType?.[linkType]
|
|
|| LGraphCanvas.link_type_colors?.[linkType]
|
|
|| setNode.bgcolor
|
|
|| (setNode.slotColor !== "#FFF" ? setNode.slotColor : null) || "#AAA";
|
|
|
|
const startPos = setNode.getConnectionPos(false, 0);
|
|
|
|
for (const target of drawTargets) {
|
|
const endPos = target.getConnectionPos(true, 0);
|
|
const highlighted = setNode.is_selected || target.is_selected;
|
|
const color = highlighted ? "#FFF" : slotColor;
|
|
app.canvas.renderLink(
|
|
ctx,
|
|
startPos,
|
|
endPos,
|
|
null,
|
|
false,
|
|
null,
|
|
color,
|
|
LiteGraph.RIGHT,
|
|
LiteGraph.LEFT
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
app.ui.settings.addSetting({
|
|
id: "KJNodes.filterGetNodeOptions",
|
|
name: "Filter Get node options by type",
|
|
category: ["KJNodes", "Set & Get", "Filter Get node options by type"],
|
|
tooltip: "When a Get node is connected, only show Set nodes with compatible types in the dropdown",
|
|
type: "boolean",
|
|
defaultValue: true,
|
|
});
|
|
app.ui.settings.addSetting({
|
|
id: "KJNodes.convertAllSetGet",
|
|
name: "Convert ALL Set/Get to links",
|
|
category: ["KJNodes", "Set & Get", "Convert ALL Set/Get to links"],
|
|
tooltip: "Replaces all Set/Get node pairs with direct links, including across subgraph boundaries (irreversible)",
|
|
type: () => {
|
|
const btn = document.createElement("button");
|
|
btn.textContent = "Convert ALL Set/Get to links";
|
|
btn.style.cssText = "padding: 6px 12px; cursor: pointer;";
|
|
btn.onclick = () => {
|
|
if (confirm("This will replace ALL Set/Get pairs with direct links. This is irreversible. Continue?")) {
|
|
convertAllSetGetToLinks(app.graph);
|
|
app.canvas?.setDirty(true, true);
|
|
app.extensionManager.toast.add({
|
|
severity: 'info',
|
|
summary: "KJ Set/Get",
|
|
detail: "All Set/Get nodes converted to direct links",
|
|
life: 3000,
|
|
});
|
|
}
|
|
};
|
|
return btn;
|
|
},
|
|
defaultValue: false,
|
|
});
|
|
|
|
// Middle-click Set/Get: let the core do slot hit detection, then intercept
|
|
// createDefaultNodeForSlot to create Set/Get instead of reroute.
|
|
let _fromMiddleClick = false;
|
|
let _lastMiddleClickShift = false;
|
|
const origProcessMiddle = LGraphCanvas.prototype._processMiddleButton;
|
|
LGraphCanvas.prototype._processMiddleButton = function(e, node) {
|
|
_fromMiddleClick = true;
|
|
_lastMiddleClickShift = !!e.shiftKey;
|
|
const wantSetGet = _lastMiddleClickShift
|
|
? app.ui.settings.getSettingValue("KJNodes.shiftMiddleClickSetGet") ?? true
|
|
: app.ui.settings.getSettingValue("KJNodes.middleClickSetGet") ?? false;
|
|
const saved = LiteGraph.middle_click_slot_add_default_node;
|
|
if (wantSetGet) {
|
|
LiteGraph.middle_click_slot_add_default_node = true;
|
|
} else {
|
|
// Workaround: core slotDefaults.init() resets this flag to true
|
|
const coreSetting = app.ui.settings.getSettingValue("Comfy.Node.MiddleClickRerouteNode");
|
|
if (coreSetting === false) LiteGraph.middle_click_slot_add_default_node = false;
|
|
}
|
|
const result = origProcessMiddle.call(this, e, node);
|
|
LiteGraph.middle_click_slot_add_default_node = saved;
|
|
return result;
|
|
};
|
|
const origCreateDefault = LGraphCanvas.prototype.createDefaultNodeForSlot;
|
|
LGraphCanvas.prototype.createDefaultNodeForSlot = function(optPass) {
|
|
if (!_fromMiddleClick) return origCreateDefault.call(this, optPass);
|
|
_fromMiddleClick = false;
|
|
const wantSetGet = _lastMiddleClickShift
|
|
? app.ui.settings.getSettingValue("KJNodes.shiftMiddleClickSetGet") ?? true
|
|
: app.ui.settings.getSettingValue("KJNodes.middleClickSetGet") ?? false;
|
|
if (!wantSetGet) return origCreateDefault.call(this, optPass);
|
|
|
|
const isFrom = optPass.nodeFrom && optPass.slotFrom !== null;
|
|
const sourceNode = isFrom ? optPass.nodeFrom : optPass.nodeTo;
|
|
const slotIndex = isFrom ? optPass.slotFrom : optPass.slotTo;
|
|
if (!sourceNode || slotIndex == null) return origCreateDefault.call(this, optPass);
|
|
const graph = this.graph || app.graph;
|
|
if (!graph) return origCreateDefault.call(this, optPass);
|
|
|
|
const newNode = LiteGraph.createNode(isFrom ? "SetNode" : "GetNode");
|
|
if (!newNode) return false;
|
|
const bounding = sourceNode.getBounding();
|
|
newNode.pos = [
|
|
isFrom ? bounding[0] + bounding[2] + 30 : bounding[0] - 200,
|
|
optPass.position?.[1] ?? bounding[1]
|
|
];
|
|
graph.add(newNode);
|
|
if (isFrom) sourceNode.connect(slotIndex, newNode, 0);
|
|
else newNode.connect(0, sourceNode, slotIndex);
|
|
graph.change();
|
|
this.setDirty(true, true);
|
|
return true;
|
|
};
|
|
}
|
|
});
|
|
|
|
// Counts graphToPrompt calls for bypassed GetNodes (see GetNode.mode), plus a temporary cross-subgraph Set/Get patch for
|
|
// frontends without resolveVirtualOutput; remove only the patch part when no longer needed.
|
|
app.registerExtension({
|
|
name: "KJNodes.CrossGraphSetGet",
|
|
setup() {
|
|
let patched = false;
|
|
|
|
const originalGraphToPrompt = app.graphToPrompt.bind(app);
|
|
app.graphToPrompt = async function(...args) {
|
|
if (!patched) {
|
|
try {
|
|
const subgraphNode = app.graph._nodes.find(n => typeof n.getInnerNodes === 'function');
|
|
if (subgraphNode) {
|
|
const tempMap = new Map();
|
|
const dtos = subgraphNode.getInnerNodes(tempMap, []);
|
|
if (dtos.length > 0) {
|
|
const proto = Object.getPrototypeOf(dtos[0]);
|
|
const DtoClass = proto.constructor;
|
|
const nativeSource = proto.resolveOutput.toString();
|
|
const hasNativeSupport = nativeSource.includes('resolveVirtualOutput');
|
|
console.log(`[KJNodes] Cross-graph Set/Get: frontend native support ${hasNativeSupport ? 'detected, skipping patch' : 'not found, applying patch'}`);
|
|
if (!hasNativeSupport) {
|
|
const origResolveOutput = proto.resolveOutput;
|
|
proto.resolveOutput = function(slot, type, visited) {
|
|
if (typeof this.node?.resolveVirtualOutput === 'function') {
|
|
const virtualSource = this.node.resolveVirtualOutput(slot);
|
|
if (virtualSource) {
|
|
const inputNodeDto = [...this.nodesByExecutionId.values()]
|
|
.find(dto => dto instanceof DtoClass && dto.node === virtualSource.node);
|
|
if (inputNodeDto) {
|
|
return inputNodeDto.resolveOutput(virtualSource.slot, type, visited);
|
|
}
|
|
throw new Error(`KJNodes: No DTO found for cross-graph source node [${virtualSource.node.id}]`);
|
|
}
|
|
}
|
|
return origResolveOutput.call(this, slot, type, visited);
|
|
};
|
|
}
|
|
patched = true;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('[KJNodes] Failed to probe ExecutableNodeDTO for cross-graph patch:', e);
|
|
}
|
|
}
|
|
_resolvingPrompt++;
|
|
try {
|
|
return await originalGraphToPrompt(...args);
|
|
} finally {
|
|
_resolvingPrompt--;
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
// Monkey-patch: no hook exists to extend the link context menu.
|
|
// Appends a "Convert to Set/Get" entry by counting DOM menus before/after
|
|
// the original call to find the newly created one. Fragile but unavoidable.
|
|
app.registerExtension({
|
|
name: "KJNodes.LinkToSetGet",
|
|
setup() {
|
|
const originalShowLinkMenu = LGraphCanvas.prototype.showLinkMenu;
|
|
LGraphCanvas.prototype.showLinkMenu = function(segment, e, presentationLink) {
|
|
const menusBefore = document.querySelectorAll(".litecontextmenu").length;
|
|
const result = originalShowLinkMenu.apply(this, arguments);
|
|
|
|
const graph = this.graph;
|
|
if (!graph) return result;
|
|
|
|
// Get the full link object to access target info; a Reroute segment's id is a reroute id, not a link id
|
|
const linkId = presentationLink?.id ?? (segment.linkIds instanceof Set ? segment.linkIds.values().next().value : segment.id);
|
|
const link = getLink(graph, linkId);
|
|
if (!link || link.origin_id == null || link.target_id == null) return result;
|
|
|
|
// Find the newly created context menu
|
|
const menus = document.querySelectorAll(".litecontextmenu");
|
|
if (menus.length <= menusBefore) return result;
|
|
const lastMenu = menus[menus.length - 1];
|
|
if (!lastMenu) return result;
|
|
|
|
const entries = lastMenu.querySelector(".litemenu-entry")?.parentElement;
|
|
if (!entries) return result;
|
|
|
|
// Create separator
|
|
const separator = document.createElement("div");
|
|
separator.className = "litemenu-entry separator";
|
|
entries.appendChild(separator);
|
|
|
|
// Create menu entry
|
|
const menuItem = document.createElement("div");
|
|
menuItem.className = "litemenu-entry submenu";
|
|
menuItem.textContent = "Convert to Set/Get";
|
|
entries.appendChild(menuItem);
|
|
|
|
const canvas = this;
|
|
menuItem.addEventListener("click", function() {
|
|
// Close menu
|
|
lastMenu.remove();
|
|
|
|
// Subgraph IO boundaries aren't in graph._nodes
|
|
const originNode = graph.getNodeById(link.origin_id)
|
|
?? (link.origin_id === graph.inputNode?.id ? graph.inputNode : null);
|
|
const targetNode = graph.getNodeById(link.target_id)
|
|
?? (link.target_id === graph.outputNode?.id ? graph.outputNode : null);
|
|
if (!originNode || !targetNode) return;
|
|
|
|
const outputSlot = originNode.outputs?.[link.origin_slot] ?? originNode.slots?.[link.origin_slot];
|
|
const linkType = outputSlot?.type || "*";
|
|
const linkName = outputSlot?.name || linkType;
|
|
|
|
// Create Set node near the source
|
|
const setNode = LiteGraph.createNode("SetNode");
|
|
if (!setNode) return;
|
|
setNode.pos = [
|
|
originNode.pos[0] + originNode.size[0] + 30,
|
|
originNode.pos[1]
|
|
];
|
|
graph.add(setNode);
|
|
|
|
// Create Get node near the target
|
|
const getNode = LiteGraph.createNode("GetNode");
|
|
if (!getNode) return;
|
|
graph.add(getNode);
|
|
getNode.pos = [
|
|
targetNode.pos[0] - getNode.size[0] - 30,
|
|
targetNode.pos[1]
|
|
];
|
|
|
|
// Remove the original link
|
|
graph.removeLink(link.id);
|
|
|
|
// Connect source -> Set node input
|
|
connectById(graph, link.origin_id, link.origin_slot, setNode.id, 0);
|
|
|
|
// Set the name widget on the Set node
|
|
setNode.widgets[0].value = linkName;
|
|
setNode.title = prefixedTitle("Set", linkName);
|
|
setNode.validateName(graph);
|
|
setNode.properties.previousName = setNode.widgets[0].value;
|
|
|
|
// Update Get node to match
|
|
const finalName = setNode.widgets[0].value;
|
|
getNode.widgets[0].value = finalName;
|
|
getNode.onRename();
|
|
|
|
// Connect Get node output -> target node input
|
|
connectById(graph, getNode.id, 0, link.target_id, link.target_slot);
|
|
|
|
canvas.setDirty(true, true);
|
|
});
|
|
|
|
return result;
|
|
};
|
|
}
|
|
});
|