1331 lines
62 KiB
JavaScript
1331 lines
62 KiB
JavaScript
// Standalone Set/Get Node Implementation for ComfyUI
|
|
|
|
import { app } from "../../../scripts/app.js";
|
|
|
|
// Get LiteGraph instance
|
|
const LiteGraph = window.LiteGraph;
|
|
|
|
if (!LiteGraph) {
|
|
console.error("LiteGraph not found. Cannot register Standalone Set/Get nodes.");
|
|
} else {
|
|
|
|
// --- Configuration ---
|
|
const disablePrefix = false; // Set to true to remove "Set_" and "Get_" prefixes from titles
|
|
const DEBUG = false; // Set true for verbose console logging
|
|
let isExecutionActive = false; // Pause expensive UI updates during execution
|
|
|
|
// --- Helper Functions ---
|
|
|
|
function debugLog(...args) { if (DEBUG) console.log(...args); }
|
|
function debugWarn(...args) { if (DEBUG) console.warn(...args); }
|
|
|
|
// Placeholder/Simple implementation for color setting
|
|
// Replace with your actual color logic if needed
|
|
function setColorAndBgColor(type) {
|
|
// Example: Log the type instead of setting colors
|
|
// console.log(`Node ${this.id}: Setting color based on type: ${type}`);
|
|
// Default colors or simple logic:
|
|
this.color = "#333";
|
|
this.bgcolor = "#555";
|
|
if (type && type !=='*') {
|
|
const colorMap = {
|
|
"MODEL": { color: "#233", bgcolor: "#355" },
|
|
"CONDITIONING": { color: "#322", bgcolor: "#533" },
|
|
"LATENT": { color: "#223", bgcolor: "#335" },
|
|
"VAE": { color: "#232", bgcolor: "#353" },
|
|
"IMAGE": { color: "#332", bgcolor: "#553" },
|
|
// Add other types as needed
|
|
};
|
|
const colors = colorMap[type.toUpperCase()];
|
|
if (colors) {
|
|
this.color = colors.color;
|
|
this.bgcolor = colors.bgcolor;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Node Conversion/Creation Helpers ---
|
|
function getNodeVarName(node) {
|
|
const v = node?.widgets?.[0]?.value;
|
|
return (typeof v === "string") ? v : (v == null ? "" : String(v));
|
|
}
|
|
|
|
function readVec2(v) {
|
|
// Accept Array, Float32Array, etc.
|
|
if (v && typeof v === "object" && typeof v.length === "number" && v.length >= 2) {
|
|
const x = Number(v[0]);
|
|
const y = Number(v[1]);
|
|
if (Number.isFinite(x) && Number.isFinite(y)) return [x, y];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getNodePos(node) {
|
|
return readVec2(node?.pos) || readVec2(node?.position) || null;
|
|
}
|
|
|
|
function getNodeWidth(node) {
|
|
const size = readVec2(node?.size);
|
|
if (size) return size[0];
|
|
if (typeof node?.computeSize === "function") {
|
|
const s = node.computeSize();
|
|
const cs = readVec2(s);
|
|
if (cs) return cs[0];
|
|
}
|
|
return 180; // reasonable fallback
|
|
}
|
|
|
|
function computeSetNames(graph) {
|
|
if (!graph || !graph._nodes) return [];
|
|
return [...new Set(
|
|
graph._nodes
|
|
.filter(n => n.type === "SetInputNode" && n.widgets && n.widgets[0])
|
|
.map(n => n.widgets[0].value)
|
|
.filter(name => name !== null && name !== undefined && name !== "")
|
|
)].sort();
|
|
}
|
|
|
|
function updateAllGetterCombos(graph, force = false) {
|
|
if (!graph || !graph._nodes) return;
|
|
const names = computeSetNames(graph);
|
|
const signature = names.join("\u001f");
|
|
|
|
if (!force && graph._setGetNameSignature === signature) return;
|
|
graph._setGetNameSignature = signature;
|
|
graph._setGetNameList = names;
|
|
|
|
const getters = graph._nodes.filter(n => n.type === "GetOutputNode");
|
|
getters.forEach(getter => {
|
|
if (getter.setComboValues) {
|
|
getter.setComboValues();
|
|
}
|
|
});
|
|
}
|
|
|
|
function getRootGraph() {
|
|
return app?.graph || app?.rootGraph || app?.rootGraphInternal || null;
|
|
}
|
|
|
|
function flushSetGetUpdates(graph) {
|
|
if (!graph || !graph._nodes) return;
|
|
|
|
// First run pending Set updates
|
|
graph._nodes.forEach(node => {
|
|
if (node.type === "SetInputNode" && node._pendingUpdate) {
|
|
node._pendingUpdate = false;
|
|
node.update?.();
|
|
}
|
|
});
|
|
|
|
// Refresh combo list after names settle
|
|
updateAllGetterCombos(graph, true);
|
|
|
|
// Then run pending Get renames
|
|
graph._nodes.forEach(node => {
|
|
if (node.type === "GetOutputNode" && node._pendingRename) {
|
|
node._pendingRename = false;
|
|
node.onRename?.();
|
|
}
|
|
});
|
|
|
|
// Redraw once after flush
|
|
if (graph.list_of_graphcanvas) {
|
|
graph.list_of_graphcanvas.forEach(c => c.setDirty(true, true));
|
|
}
|
|
}
|
|
|
|
function disconnectAllLinks(node) {
|
|
const graph = node?.graph;
|
|
if (!graph) return;
|
|
|
|
// Remove incoming links
|
|
if (Array.isArray(node.inputs)) {
|
|
for (const input of node.inputs) {
|
|
if (typeof input?.link === "number") {
|
|
if (typeof graph.removeLink === "function") {
|
|
graph.removeLink(input.link);
|
|
} else {
|
|
console.warn("Set/Get: graph.removeLink missing; cannot remove link", input.link);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove outgoing links
|
|
if (Array.isArray(node.outputs)) {
|
|
for (const output of node.outputs) {
|
|
if (Array.isArray(output?.links) && output.links.length) {
|
|
// copy because removeLink mutates graph.links / links arrays
|
|
const links = output.links.slice();
|
|
for (const linkId of links) {
|
|
if (typeof graph.removeLink === "function") {
|
|
graph.removeLink(linkId);
|
|
} else {
|
|
console.warn("Set/Get: graph.removeLink missing; cannot remove link", linkId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function resolveRegisteredTypeKey(type) {
|
|
const reg = LiteGraph?.registered_node_types;
|
|
if (!reg || !type) return type;
|
|
if (reg[type]) return type;
|
|
// Some LiteGraph builds use "category/name" keys; try suffix match.
|
|
const suffix1 = `/${type}`;
|
|
for (const key of Object.keys(reg)) {
|
|
if (key === type) return key;
|
|
if (key.endsWith(suffix1)) return key;
|
|
}
|
|
return type;
|
|
}
|
|
|
|
function safeCreateNode(type) {
|
|
if (!type) return null;
|
|
const typeKey = resolveRegisteredTypeKey(type);
|
|
|
|
// Most LiteGraph builds
|
|
if (typeof LiteGraph?.createNode === "function") {
|
|
try {
|
|
const n = LiteGraph.createNode(typeKey);
|
|
if (n && !n.type) n.type = typeKey;
|
|
return n;
|
|
} catch (e) {
|
|
console.warn(`Set/Get: LiteGraph.createNode("${typeKey}") threw`, e);
|
|
}
|
|
}
|
|
|
|
// Fallback: registry-based construction (varies by LiteGraph version)
|
|
const reg = LiteGraph?.registered_node_types?.[typeKey];
|
|
if (!reg) {
|
|
console.warn(`Set/Get: Node type "${typeKey}" not found in LiteGraph.registered_node_types`);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// Some versions store the class directly
|
|
if (typeof reg === "function") {
|
|
const n = new reg();
|
|
if (!n.type) n.type = typeKey;
|
|
return n;
|
|
}
|
|
// Other versions store an object containing the class
|
|
if (typeof reg?.class === "function") {
|
|
const n = new reg.class();
|
|
if (!n.type) n.type = typeKey;
|
|
return n;
|
|
}
|
|
} catch (e) {
|
|
console.warn(`Set/Get: failed constructing node "${typeKey}" from registry`, e);
|
|
}
|
|
|
|
console.warn(`Set/Get: unsupported registry entry for node type "${typeKey}"`, reg);
|
|
return null;
|
|
}
|
|
|
|
function safeGraphAddNode(graph, node) {
|
|
if (!graph || !node) return false;
|
|
try {
|
|
if (typeof graph.add === "function") { graph.add(node); return true; }
|
|
if (typeof graph.addNode === "function") { graph.addNode(node); return true; }
|
|
} catch (e) {
|
|
console.warn("Set/Get: graph add failed", e);
|
|
return false;
|
|
}
|
|
console.warn("Set/Get: graph has no add/addNode method");
|
|
return false;
|
|
}
|
|
|
|
function graphHasNode(graph, node) {
|
|
if (!graph || !node) return false;
|
|
if (typeof node.id === "number" && graph._nodes_by_id && graph._nodes_by_id[node.id]) return true;
|
|
if (Array.isArray(graph._nodes) && graph._nodes.includes(node)) return true;
|
|
return false;
|
|
}
|
|
|
|
function safeGraphRemoveNode(graph, node) {
|
|
if (!graph || !node) return false;
|
|
try {
|
|
if (typeof graph.remove === "function") { graph.remove(node); return true; }
|
|
if (typeof graph.removeNode === "function") { graph.removeNode(node); return true; }
|
|
if (typeof node.id === "number" && typeof graph.remove === "function") { graph.remove(node.id); return true; }
|
|
} catch (e) {
|
|
console.warn("Set/Get: graph remove failed", e);
|
|
return false;
|
|
}
|
|
console.warn("Set/Get: graph has no remove/removeNode method");
|
|
return false;
|
|
}
|
|
|
|
function initConvertedNodeNameAndUI(newNode, name) {
|
|
if (!newNode?.widgets?.[0]) return;
|
|
newNode.widgets[0].value = name ?? "";
|
|
|
|
// Ensure title/type updates happen
|
|
if (newNode.type === "GetOutputNode" && typeof newNode.onRename === "function") {
|
|
newNode.onRename();
|
|
} else if (newNode.type === "SetInputNode") {
|
|
if (newNode.widgets[0].value !== "") {
|
|
newNode.title = (!disablePrefix ? "Set_" : "") + newNode.widgets[0].value;
|
|
} else {
|
|
newNode.title = "Set (Input)";
|
|
}
|
|
if (newNode.properties) {
|
|
newNode.properties.previousName = newNode.widgets[0].value;
|
|
}
|
|
if (typeof newNode.update === "function") {
|
|
newNode.update();
|
|
}
|
|
}
|
|
}
|
|
|
|
function selectAndRedraw(node) {
|
|
if (app?.canvas) {
|
|
app.canvas.selectNode(node, false);
|
|
app.canvas.setDirty(true, true);
|
|
}
|
|
if (node?.graph?.list_of_graphcanvas) {
|
|
node.graph.list_of_graphcanvas.forEach(c => c.setDirty(true, true));
|
|
}
|
|
}
|
|
|
|
function getSelectedNodesFromCanvas(canvas) {
|
|
// ComfyUI/LiteGraph variants:
|
|
// - Nodes 2.0: canvas.selectedItems is a Set (nodes, groups, etc)
|
|
// - Older: canvas.selected_nodes is an object map
|
|
let items = [];
|
|
try {
|
|
if (canvas?.selectedItems && typeof canvas.selectedItems[Symbol.iterator] === "function") {
|
|
items = Array.from(canvas.selectedItems);
|
|
} else if (canvas?.selected_nodes && typeof canvas.selected_nodes === "object") {
|
|
items = Object.values(canvas.selected_nodes);
|
|
} else if (app?.canvas?.selected_nodes && typeof app.canvas.selected_nodes === "object") {
|
|
items = Object.values(app.canvas.selected_nodes);
|
|
}
|
|
} catch (_) {
|
|
items = [];
|
|
}
|
|
|
|
// Filter to node-like objects
|
|
return items.filter(n =>
|
|
n &&
|
|
typeof n === "object" &&
|
|
typeof n.type === "string" &&
|
|
n.graph
|
|
);
|
|
}
|
|
|
|
function convertSelectedSetGetNodes(canvas) {
|
|
const selected = getSelectedNodesFromCanvas(canvas)
|
|
.filter(n => n.type === "SetInputNode" || n.type === "GetOutputNode");
|
|
if (selected.length === 0) return;
|
|
|
|
// Snapshot list to avoid mutation while converting/removing nodes
|
|
const toConvert = selected.slice();
|
|
const created = [];
|
|
|
|
for (const node of toConvert) {
|
|
const targetType = node.type === "SetInputNode" ? "GetOutputNode" : "SetInputNode";
|
|
const newNode = convertNodeInPlace(node, targetType, { select: false });
|
|
if (newNode) created.push(newNode);
|
|
}
|
|
|
|
// Select one of the new nodes (optional, but avoids ending up with no selection after removal)
|
|
const first = created[0];
|
|
if (first) {
|
|
const c = canvas || app?.canvas;
|
|
if (c?.selectNode) {
|
|
try { c.selectNode(first, false); } catch (_) {}
|
|
}
|
|
}
|
|
|
|
// Ensure redraw
|
|
if (app?.canvas?.setDirty) app.canvas.setDirty(true, true);
|
|
if (first?.graph?.list_of_graphcanvas) {
|
|
first.graph.list_of_graphcanvas.forEach(gc => gc.setDirty(true, true));
|
|
}
|
|
}
|
|
|
|
function createSiblingNodeToRight(sourceNode, targetType) {
|
|
const graph = sourceNode?.graph;
|
|
if (!graph) return null;
|
|
|
|
const name = getNodeVarName(sourceNode);
|
|
const newNode = safeCreateNode(targetType);
|
|
if (!newNode) {
|
|
console.warn("Set/Get: Create sibling failed - could not create node", targetType);
|
|
return null;
|
|
}
|
|
|
|
const sourcePos = getNodePos(sourceNode) || (app?.canvas?.graph_mouse ? readVec2(app.canvas.graph_mouse) : null) || [0, 0];
|
|
const offsetX = getNodeWidth(sourceNode) + 50;
|
|
newNode.pos = [sourcePos[0] + offsetX, sourcePos[1]];
|
|
|
|
// Keep mode consistent with source (useful if user is working with bypassed groups)
|
|
if (typeof sourceNode.mode === "number") {
|
|
newNode.mode = sourceNode.mode;
|
|
}
|
|
|
|
if (!safeGraphAddNode(graph, newNode)) {
|
|
console.warn("Set/Get: Create sibling failed - could not add node to graph", targetType);
|
|
return null;
|
|
}
|
|
if (!graphHasNode(graph, newNode)) {
|
|
console.warn("Set/Get: Create sibling warning - graph add returned, but node not found in graph", newNode);
|
|
}
|
|
initConvertedNodeNameAndUI(newNode, name);
|
|
selectAndRedraw(newNode);
|
|
return newNode;
|
|
}
|
|
|
|
function convertNodeInPlace(sourceNode, targetType, options = {}) {
|
|
const graph = sourceNode?.graph;
|
|
if (!graph) return null;
|
|
|
|
const name = getNodeVarName(sourceNode);
|
|
const pos = getNodePos(sourceNode) || (app?.canvas?.graph_mouse ? readVec2(app.canvas.graph_mouse) : null) || [0, 0];
|
|
const mode = (typeof sourceNode.mode === "number") ? sourceNode.mode : undefined;
|
|
|
|
// User requested: no confirmation, just disconnect anything active.
|
|
disconnectAllLinks(sourceNode);
|
|
|
|
const newNode = safeCreateNode(targetType);
|
|
if (!newNode) {
|
|
console.warn("Set/Get: Convert failed - could not create node", targetType);
|
|
return null;
|
|
}
|
|
|
|
newNode.pos = [pos[0], pos[1]];
|
|
if (typeof mode === "number") newNode.mode = mode;
|
|
|
|
if (!safeGraphAddNode(graph, newNode)) {
|
|
console.warn("Set/Get: Convert failed - could not add new node to graph", targetType);
|
|
return null;
|
|
}
|
|
if (!graphHasNode(graph, newNode)) {
|
|
console.warn("Set/Get: Convert aborted - new node not found in graph, not deleting original", newNode);
|
|
return null;
|
|
}
|
|
initConvertedNodeNameAndUI(newNode, name);
|
|
|
|
const removed = safeGraphRemoveNode(graph, sourceNode);
|
|
if (!removed) {
|
|
console.warn("Set/Get: Convert warning - failed to remove original node", sourceNode);
|
|
}
|
|
if (options.select !== false) {
|
|
selectAndRedraw(newNode);
|
|
} else {
|
|
// Avoid viewport/selection churn for batch operations; still redraw.
|
|
if (app?.canvas?.setDirty) app.canvas.setDirty(true, true);
|
|
if (newNode?.graph?.list_of_graphcanvas) {
|
|
newNode.graph.list_of_graphcanvas.forEach(c => c.setDirty(true, true));
|
|
}
|
|
}
|
|
return newNode;
|
|
}
|
|
|
|
// --- Main Extension ---
|
|
|
|
app.registerExtension({
|
|
name: "Standalone.SetGetNodes", // Keep the extension name or change if desired
|
|
/**
|
|
* ComfyUI (new) context menu API (Nodes 2.0+):
|
|
* Prefer this over node.getExtraMenuOptions(), which is deprecated.
|
|
* Docs: https://docs.comfy.org/custom-nodes/js/context-menu-migration
|
|
*/
|
|
getNodeMenuItems(node) {
|
|
try {
|
|
if (!node) return [];
|
|
const t = node.type;
|
|
|
|
// If multiple nodes are selected, offer a bulk convert right from the node menu too.
|
|
const selected = getSelectedNodesFromCanvas(app?.canvas);
|
|
const selectedSetGet = selected.filter(n => n.type === "SetInputNode" || n.type === "GetOutputNode");
|
|
const bulkItems = (selectedSetGet.length > 1)
|
|
? [
|
|
null,
|
|
{
|
|
content: `Convert selected Set/Get nodes (${selectedSetGet.length})`,
|
|
callback: () => { convertSelectedSetGetNodes(app?.canvas); },
|
|
},
|
|
]
|
|
: [];
|
|
|
|
if (t === "SetInputNode") {
|
|
return [
|
|
...bulkItems,
|
|
null,
|
|
{
|
|
content: "Convert to Get (Output)",
|
|
callback: () => {
|
|
convertNodeInPlace(node, "GetOutputNode");
|
|
},
|
|
},
|
|
{
|
|
content: "Create Get (Output) to the right",
|
|
callback: () => {
|
|
createSiblingNodeToRight(node, "GetOutputNode");
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
if (t === "GetOutputNode") {
|
|
return [
|
|
...bulkItems,
|
|
null,
|
|
{
|
|
content: "Convert to Set (Input)",
|
|
callback: () => {
|
|
convertNodeInPlace(node, "SetInputNode");
|
|
},
|
|
},
|
|
{
|
|
content: "Create Set (Input) to the right",
|
|
callback: () => {
|
|
createSiblingNodeToRight(node, "SetInputNode");
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
return [];
|
|
} catch (e) {
|
|
console.error("[SetInputGetOutput] getNodeMenuItems error", e);
|
|
return [];
|
|
}
|
|
},
|
|
/**
|
|
* Canvas context menu (recommended API):
|
|
* Adds a bulk action when selection contains Set/Get nodes.
|
|
*/
|
|
getCanvasMenuItems(canvas) {
|
|
const selected = getSelectedNodesFromCanvas(canvas);
|
|
const hasAny = selected.some(n => n.type === "SetInputNode" || n.type === "GetOutputNode");
|
|
if (!hasAny) return [];
|
|
return [
|
|
null,
|
|
{
|
|
content: "Convert selected Set/Get nodes",
|
|
callback: () => { convertSelectedSetGetNodes(canvas); },
|
|
},
|
|
];
|
|
},
|
|
// Pause expensive UI updates during execution
|
|
onExecutionStart() {
|
|
isExecutionActive = true;
|
|
},
|
|
onExecutionEnd() {
|
|
isExecutionActive = false;
|
|
const graph = getRootGraph();
|
|
if (graph) {
|
|
flushSetGetUpdates(graph);
|
|
}
|
|
},
|
|
onExecutionError() {
|
|
isExecutionActive = false;
|
|
const graph = getRootGraph();
|
|
if (graph) {
|
|
flushSetGetUpdates(graph);
|
|
}
|
|
},
|
|
registerCustomNodes() {
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// ~~ SET NODE -> SetInputNode ~~
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
class SetInputNode extends LGraphNode {
|
|
defaultVisibility = true;
|
|
serialize_widgets = true;
|
|
drawConnection = false;
|
|
currentGetters = null;
|
|
slotColor = "#FFF"; // Default link color
|
|
canvas = app.canvas;
|
|
menuEntry = "Show connections";
|
|
|
|
constructor(title) {
|
|
super(title || "Set (Input)");
|
|
if (!this.properties) {
|
|
this.properties = { "previousName": "" };
|
|
}
|
|
this.properties.showOutputText = SetInputNode.defaultVisibility;
|
|
this._updateScheduled = false;
|
|
this._lastName = "";
|
|
this._lastInputType = "";
|
|
this._pendingUpdate = false;
|
|
|
|
const node = this;
|
|
|
|
this.addWidget(
|
|
"text",
|
|
"Input Name",
|
|
'', // Initial value
|
|
(value, widget, evt) => { // Widget callback
|
|
// No validation needed here anymore for duplicates
|
|
if (node.widgets[0].value !== '') {
|
|
node.title = (!disablePrefix ? "Set_" : "") + node.widgets[0].value;
|
|
} else {
|
|
node.title = "Set (Input)" // Reset title if name is cleared
|
|
}
|
|
node.properties.previousName = node.widgets[0].value; // Store for update checks
|
|
node.requestUpdate(); // Trigger updates (debounced)
|
|
},
|
|
{}
|
|
);
|
|
|
|
this.addInput("*", "*"); // Default input
|
|
|
|
// --- Connection Handling ---
|
|
this.onConnectionsChange = function(
|
|
slotType, slot, isChangeConnect, link_info, output
|
|
) {
|
|
// On Disconnect Input
|
|
if (slotType === LiteGraph.INPUT && !isChangeConnect) {
|
|
// If the link is removed, reset the input type if it's the primary input
|
|
if(slot === 0) {
|
|
this.inputs[0].type = '*';
|
|
this.inputs[0].name = '*';
|
|
// Optionally reset title if name wasn't manually set
|
|
if (this.widgets[0].value === '') {
|
|
this.title = "Set (Input)";
|
|
}
|
|
setColorAndBgColor.call(this, '*'); // Reset color
|
|
}
|
|
}
|
|
|
|
// On Connect Input
|
|
if (link_info && node.graph && slotType === LiteGraph.INPUT && isChangeConnect && slot === 0) {
|
|
const fromNode = node.graph.getNodeById(link_info.origin_id);
|
|
if (fromNode && fromNode.outputs && fromNode.outputs[link_info.origin_slot]) {
|
|
const type = fromNode.outputs[link_info.origin_slot].type || '*';
|
|
|
|
// Update input type and name
|
|
this.inputs[0].type = type;
|
|
this.inputs[0].name = type;
|
|
|
|
// Auto-name based on type if widget name is empty/default
|
|
if (this.widgets[0].value === '' || this.widgets[0].value === '*') {
|
|
this.widgets[0].value = type;
|
|
this.title = (!disablePrefix ? "Set_" : "") + type;
|
|
this.properties.previousName = type;
|
|
}
|
|
|
|
setColorAndBgColor.call(this, type);
|
|
} else {
|
|
console.warn("SetInputNode: Connected input node or slot not found.", link_info);
|
|
}
|
|
}
|
|
|
|
// Update getters regardless of connection change type
|
|
this.requestUpdate();
|
|
}; // End onConnectionsChange
|
|
|
|
// --- Name Validation (Removed Duplicate Check) ---
|
|
this.validateName = function(graph) {
|
|
// Name validation logic for duplicates is removed
|
|
this.requestUpdate(); // Ensure update is called
|
|
};
|
|
|
|
// --- Cloning ---
|
|
this.clone = function () {
|
|
const cloned = SetInputNode.prototype.clone.apply(this);
|
|
if (cloned.inputs && cloned.inputs[0]) {
|
|
cloned.inputs[0].name = '*';
|
|
cloned.inputs[0].type = '*';
|
|
}
|
|
cloned.properties.previousName = ''; // Reset previous name on clone
|
|
// Keep the widget value on clone if desired, or clear it:
|
|
// if(cloned.widgets && cloned.widgets[0]) cloned.widgets[0].value = '';
|
|
cloned.size = cloned.computeSize();
|
|
return cloned;
|
|
};
|
|
|
|
// --- Node Lifecycle Callbacks ---
|
|
this.onAdded = function(graph) {
|
|
this.validateName(graph); // Calls update
|
|
updateAllGetterCombos(graph, true);
|
|
};
|
|
|
|
this.onRemoved = function() {
|
|
if (!this.graph) return;
|
|
// Update combo boxes on removal
|
|
setTimeout(() => { if (this.graph) updateAllGetterCombos(this.graph, true); }, 0);
|
|
};
|
|
|
|
// Override onModeChange to trigger update
|
|
const originalOnModeChange = this.onModeChange;
|
|
this.onModeChange = function(mode) {
|
|
if (originalOnModeChange) {
|
|
originalOnModeChange.apply(this, arguments);
|
|
}
|
|
// When mode changes (bypass), trigger update after a short delay
|
|
// LiteGraph.BYPASS == 4
|
|
debugLog(`SetInputNode ${this.id} (${this.widgets[0].value}) mode changed to: ${mode}`);
|
|
setTimeout(() => { if (this.requestUpdate) this.requestUpdate(); }, 50);
|
|
};
|
|
|
|
|
|
// --- Core Update Logic ---
|
|
this.requestUpdate = function() {
|
|
if (isExecutionActive) {
|
|
this._pendingUpdate = true;
|
|
return;
|
|
}
|
|
if (this._updateScheduled) return;
|
|
this._updateScheduled = true;
|
|
requestAnimationFrame(() => {
|
|
this._updateScheduled = false;
|
|
if (this.update) this.update();
|
|
});
|
|
};
|
|
|
|
this.update = function() {
|
|
const node = this; // Use 'node' reference for safety in callbacks/timeouts
|
|
if (!node.graph) { return; }
|
|
if (isExecutionActive) {
|
|
node._pendingUpdate = true;
|
|
return;
|
|
}
|
|
|
|
const currentName = node.widgets[0].value;
|
|
const inputType = (node.inputs && node.inputs[0]) ? node.inputs[0].type : '*';
|
|
const inputTypeChanged = inputType !== node._lastInputType;
|
|
if (inputTypeChanged) {
|
|
node._lastInputType = inputType;
|
|
}
|
|
|
|
// Find Getters linked to current name and tell them to update
|
|
const currentGetters = this.findGetters(node.graph, false);
|
|
currentGetters.forEach(getter => {
|
|
if (getter.onRename) {
|
|
// Use timeout to allow graph state to update before getter reacts
|
|
setTimeout(() => { if (getter.graph) getter.onRename(); }, 0);
|
|
} else if (inputTypeChanged && getter.setType && getter.outputs[0].type !== inputType) {
|
|
// Fallback if onRename is unavailable
|
|
setTimeout(() => { if (getter.graph) getter.setType(inputType); }, 0);
|
|
}
|
|
});
|
|
|
|
// Handle Getters linked to a previous name (if name changed)
|
|
if (node.properties.previousName && currentName !== node.properties.previousName) {
|
|
debugLog(`SetInputNode ${node.id} name changed from ${node.properties.previousName} to ${currentName}`);
|
|
const previousGetters = this.findGetters(node.graph, true);
|
|
previousGetters.forEach(getter => {
|
|
// Check if the getter is still potentially linked by the old name in its widget
|
|
if (getter.onRename && getter.widgets[0].value === node.properties.previousName) {
|
|
debugLog(`Updating previous getter ${getter.id} (was linked to ${node.properties.previousName})`);
|
|
setTimeout(() => { if (getter.graph) getter.onRename(); }, 0);
|
|
}
|
|
});
|
|
node.properties.previousName = ''; // Clear previous name after handling
|
|
}
|
|
|
|
// Only rebuild combo list if the set names actually changed
|
|
if (currentName !== node._lastName) {
|
|
node._lastName = currentName;
|
|
updateAllGetterCombos(node.graph);
|
|
}
|
|
|
|
// Redraw only if we're actively drawing virtual connections
|
|
if (node.drawConnection && node.graph && node.graph.list_of_graphcanvas) {
|
|
node.graph.list_of_graphcanvas.forEach(canvas => canvas.setDirty(true, true));
|
|
}
|
|
};
|
|
|
|
// --- Helper: Find Linked Getters ---
|
|
// findGetters - Ensure bypass check uses mode 4 for the GetNode
|
|
this.findGetters = function(graph, checkForPreviousName) {
|
|
const name = checkForPreviousName ? this.properties.previousName : this.widgets[0].value;
|
|
if (!graph || name === '') return [];
|
|
|
|
return graph._nodes.filter(otherNode =>
|
|
otherNode.type === 'GetOutputNode' && // Check for GetOutputNode
|
|
otherNode.widgets && otherNode.widgets[0] &&
|
|
otherNode.widgets[0].value === name //&&
|
|
// We don't check the GetNode's bypass status here, Set should update all regardless
|
|
// otherNode.mode !== 4 // Check if the GetNode itself is bypassed
|
|
);
|
|
};
|
|
|
|
// Node is virtual (doesn't execute on backend)
|
|
this.isVirtualNode = true;
|
|
|
|
} // End constructor
|
|
|
|
// --- Context Menu ---
|
|
getExtraMenuOptions(_, options) {
|
|
this.menuEntry = this.drawConnection ? "Hide connections" : "Show connections";
|
|
options.unshift(
|
|
{
|
|
content: "Convert to Get (Output)",
|
|
callback: () => {
|
|
convertNodeInPlace(this, "GetOutputNode");
|
|
},
|
|
},
|
|
{
|
|
content: "Create Get (Output) to the right",
|
|
callback: () => {
|
|
createSiblingNodeToRight(this, "GetOutputNode");
|
|
},
|
|
},
|
|
{
|
|
content: this.menuEntry,
|
|
callback: () => {
|
|
// Need graph context
|
|
if (!this.graph) return;
|
|
this.currentGetters = this.findGetters(this.graph);
|
|
if (!this.currentGetters || this.currentGetters.length === 0) return;
|
|
|
|
// Determine color from the first active getter found or default
|
|
let activeGetter = this.currentGetters.find(g => g.mode !== 4 && g.mode !== 2);
|
|
let linkType = activeGetter ? (activeGetter.outputs[0].type || '*') : (this.inputs[0].type || '*');
|
|
|
|
// Use default connection color from canvas based on type
|
|
this.slotColor = this.canvas?.default_connection_color_byType?.[linkType] || "#FFF";
|
|
|
|
this.drawConnection = !this.drawConnection;
|
|
this.menuEntry = this.drawConnection ? "Hide connections" : "Show connections"; // Update label
|
|
this.canvas?.setDirty(true, true);
|
|
},
|
|
},
|
|
{
|
|
content: "Hide all connections",
|
|
callback: () => {
|
|
if (!this.graph) return;
|
|
const allSetGet = this.graph._nodes.filter(otherNode =>
|
|
otherNode.type === "GetOutputNode" || otherNode.type === "SetInputNode"
|
|
);
|
|
allSetGet.forEach(node => { node.drawConnection = false; });
|
|
this.canvas?.setDirty(true, true);
|
|
},
|
|
},
|
|
{
|
|
content: "Go to first active Getter",
|
|
callback: () => {
|
|
if (!this.graph) return;
|
|
const firstActiveGetter = this.findGetters(this.graph)
|
|
.find(getter => getter.mode !== 4 && getter.mode !== 2); // Find first non-bypassed, non-muted
|
|
if (firstActiveGetter && this.canvas) {
|
|
this.canvas.centerOnNode(firstActiveGetter);
|
|
this.canvas.selectNode(firstActiveGetter, false);
|
|
} else {
|
|
debugLog("No active GetOutput nodes found for this SetInput node.");
|
|
}
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
// --- Drawing Connections ---
|
|
onDrawForeground(ctx, graphcanvas) {
|
|
if (this.flags.collapsed || !this.drawConnection) {
|
|
return;
|
|
}
|
|
|
|
this.currentGetters = this.findGetters(this.graph);
|
|
if (!this.currentGetters || this.currentGetters.length === 0) return;
|
|
|
|
const lGraphCanvas = graphcanvas || app.canvas; // Use passed canvas or global fallback
|
|
if (!lGraphCanvas) return; // Need canvas to draw
|
|
|
|
ctx.save();
|
|
ctx.lineWidth = 1; // Thinner line for virtual connections
|
|
ctx.strokeStyle = this.slotColor || "#FFF";
|
|
|
|
const startPos = this.localToGlobal([this.size[0], LiteGraph.NODE_SLOT_HEIGHT / 2]);
|
|
|
|
for (const getter of this.currentGetters) {
|
|
// Only draw lines to non-bypassed, non-muted Getters
|
|
if (getter.mode === 4 || getter.mode === 2) continue; // LiteGraph.BYPASS == 4, LiteGraph.NEVER == 2
|
|
|
|
if (!getter.isVisibleOnCanvas(lGraphCanvas)) continue; // Don't draw if offscreen
|
|
|
|
const endPos = getter.localToGlobal([0, LiteGraph.NODE_SLOT_HEIGHT / 2]);
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(startPos[0], startPos[1]);
|
|
// Simple bezier curve
|
|
ctx.bezierCurveTo(
|
|
startPos[0] + 100, startPos[1],
|
|
endPos[0] - 100, endPos[1],
|
|
endPos[0], endPos[1]
|
|
);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
}
|
|
|
|
} // End SetInputNode Class
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// ~~ GET NODE -> GetOutputNode ~~
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
class GetOutputNode extends LGraphNode {
|
|
defaultVisibility = true;
|
|
serialize_widgets = true;
|
|
drawConnection = false;
|
|
slotColor = "#FFF";
|
|
currentSetter = null;
|
|
canvas = app.canvas;
|
|
|
|
constructor(title) {
|
|
super(title || "Get (Output)");
|
|
if (!this.properties) { this.properties = {}; }
|
|
this.properties.showOutputText = GetOutputNode.defaultVisibility;
|
|
this._validateScheduled = false;
|
|
this._pendingRename = false;
|
|
|
|
const node = this;
|
|
|
|
this.addWidget(
|
|
"combo",
|
|
"Output Name",
|
|
"", // Initial value
|
|
(value, widget, event) => { // Widget callback
|
|
this.onRename(); // Trigger update when combo value changes
|
|
},
|
|
{ // Combo options
|
|
values: () => { // Function to populate the combo box
|
|
if (!node.graph) return [];
|
|
const setterNodes = node.graph._nodes.filter((otherNode) =>
|
|
otherNode.type == 'SetInputNode' // Look for SetInputNode
|
|
);
|
|
// Get unique names and sort them
|
|
const uniqueNames = [...new Set(
|
|
setterNodes
|
|
.map((otherNode) => otherNode.widgets && otherNode.widgets[0] ? otherNode.widgets[0].value : null)
|
|
.filter(name => name !== null && name !== '') // Filter out null/empty names
|
|
)].sort();
|
|
return uniqueNames;
|
|
}
|
|
}
|
|
);
|
|
|
|
this.addOutput("*", '*'); // Default output
|
|
|
|
// --- Connection Validation ---
|
|
this.onConnectionsChange = function(
|
|
slotType, slot, isChangeConnect, link_info, output
|
|
) {
|
|
// Only validate on output connections (not input)
|
|
// Delay validation to ensure connection is fully established
|
|
if (slotType === LiteGraph.OUTPUT && slot === 0 && this.graph) {
|
|
setTimeout(() => {
|
|
if (this.graph && this.validateLinks) {
|
|
this.validateLinks();
|
|
}
|
|
}, 50);
|
|
}
|
|
};
|
|
|
|
// --- Name/Type Update Logic ---
|
|
this.setName = function(name) {
|
|
if (this.widgets && this.widgets[0]) {
|
|
this.widgets[0].value = name;
|
|
this.onRename();
|
|
// Note: Serialization might happen automatically depending on widget config
|
|
// If manual serialization needed: LiteGraph.doSerialize(this);
|
|
}
|
|
};
|
|
|
|
this.onRename = function() {
|
|
if (!this.graph) { return; } // Ensure graph context
|
|
if (isExecutionActive) {
|
|
this._pendingRename = true;
|
|
return;
|
|
}
|
|
|
|
const setter = this.findSetter(this.graph); // Finds the first *active* setter
|
|
let linkType = '*';
|
|
let titleName = (this.widgets && this.widgets[0] ? this.widgets[0].value : null) || "unset";
|
|
|
|
if (setter && setter.inputs && setter.inputs[0]) {
|
|
linkType = setter.inputs[0].type || '*'; // Get type from active setter's input
|
|
titleName = setter.widgets[0].value; // Use setter's name for title
|
|
} else {
|
|
// If no active setter found, keep widget value but use '*' type
|
|
linkType = '*';
|
|
}
|
|
|
|
// --- Critical: Update Output Type ---
|
|
const desiredTitle = (!disablePrefix ? "Get_" : "") + titleName;
|
|
const output = this.outputs && this.outputs[0] ? this.outputs[0] : null;
|
|
const typeChanged = output ? output.type !== linkType : true;
|
|
const titleChanged = this.title !== desiredTitle;
|
|
|
|
if (!typeChanged && !titleChanged) {
|
|
return;
|
|
}
|
|
|
|
if (this.outputs && this.outputs[0]) {
|
|
if (typeChanged) {
|
|
debugLog(`GetOutputNode ${this.id} (${titleName}): Updating output type from ${this.outputs[0].type} to ${linkType}`);
|
|
this.outputs[0].type = linkType;
|
|
this.outputs[0].name = linkType; // Keep name and type consistent
|
|
// Important: Notify LiteGraph that connections may need revalidation
|
|
this.graph.links_dirty = true;
|
|
// Delay validation to ensure type change is processed and connections are established
|
|
if (!this._validateScheduled) {
|
|
this._validateScheduled = true;
|
|
setTimeout(() => {
|
|
this._validateScheduled = false;
|
|
if (this.graph && this.validateLinks) {
|
|
this.validateLinks();
|
|
}
|
|
}, 100);
|
|
}
|
|
}
|
|
} else if (!this.outputs || this.outputs.length === 0) {
|
|
this.addOutput("output", linkType); // Safety add output
|
|
this.graph.links_dirty = true;
|
|
}
|
|
// --- End Critical Section ---
|
|
|
|
this.title = desiredTitle;
|
|
|
|
if (typeChanged) {
|
|
setColorAndBgColor.call(this, linkType); // Update color
|
|
}
|
|
|
|
// Redraw the node
|
|
if ((typeChanged || titleChanged) && this.graph && this.graph.list_of_graphcanvas) {
|
|
this.graph.list_of_graphcanvas.forEach(canvas => canvas.setDirty(true, true));
|
|
}
|
|
};
|
|
|
|
// Method called by SetNode to update combo box options
|
|
this.setComboValues = function() {
|
|
if (this.widgets && this.widgets[0] && this.widgets[0].type === 'combo' && this.graph) {
|
|
const comboWidget = this.widgets[0];
|
|
const uniqueNames = this.graph._setGetNameList || computeSetNames(this.graph);
|
|
// Check if options actually changed before updating
|
|
if (JSON.stringify(comboWidget.options.values) !== JSON.stringify(uniqueNames)) {
|
|
comboWidget.options.values = uniqueNames;
|
|
// Ensure current value still exists, if not, maybe clear or default?
|
|
if (!uniqueNames.includes(comboWidget.value) && comboWidget.value !== '') {
|
|
// console.log(`GetOutputNode ${this.id}: Current value "${comboWidget.value}" no longer exists. Clearing.`);
|
|
// comboWidget.value = ''; // Optionally clear if linked SetNode removed
|
|
// this.onRename(); // Trigger update if value changed
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// --- Cloning ---
|
|
this.clone = function () {
|
|
const cloned = GetOutputNode.prototype.clone.apply(this);
|
|
cloned.size = cloned.computeSize();
|
|
// Ensure combo value is preserved on clone
|
|
if (this.widgets && this.widgets[0] && cloned.widgets && cloned.widgets[0]) {
|
|
cloned.widgets[0].value = this.widgets[0].value;
|
|
}
|
|
// Trigger rename on cloned node after adding to graph (onAdded might be better)
|
|
setTimeout(() => { if(cloned.graph) cloned.onRename(); }, 0);
|
|
return cloned;
|
|
};
|
|
|
|
this.onAdded = function(graph) {
|
|
// Ensure the node updates its type/title when added to the graph
|
|
// Delay to allow graph to finish loading connections
|
|
setTimeout(() => {
|
|
if (this.graph && this.onRename) {
|
|
this.onRename();
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
// Override onModeChange to trigger update (e.g., if GetNode itself is bypassed)
|
|
const originalOnModeChange = this.onModeChange;
|
|
this.onModeChange = function(mode) {
|
|
if (originalOnModeChange) {
|
|
originalOnModeChange.apply(this, arguments);
|
|
}
|
|
// LiteGraph.BYPASS == 4
|
|
debugLog(`GetOutputNode ${this.id} (${this.widgets[0].value}) mode changed to: ${mode}`);
|
|
// Trigger rename to re-evaluate connections/drawing
|
|
setTimeout(() => { if (this.onRename) this.onRename(); }, 50);
|
|
};
|
|
|
|
// --- Link Validation ---
|
|
this.validateLinks = function() {
|
|
if (!this.graph || !this.outputs || !this.outputs[0] || !this.outputs[0].links) {
|
|
return;
|
|
}
|
|
|
|
const outputType = this.outputs[0].type;
|
|
const nodeName = (this.widgets && this.widgets[0]) ? this.widgets[0].value : 'unnamed';
|
|
|
|
// Don't validate if output type is generic/wildcard
|
|
if (!outputType || outputType === '*' || outputType === '') {
|
|
debugLog(`GetOutputNode ${this.id} (${nodeName}): Skipping validation - output type is wildcard (${outputType})`);
|
|
return;
|
|
}
|
|
|
|
debugLog(`GetOutputNode ${this.id} (${nodeName}): Validating ${this.outputs[0].links.length} links with output type "${outputType}"`);
|
|
|
|
const linksToRemove = [];
|
|
for (const linkId of this.outputs[0].links) {
|
|
const link = this.graph.links[linkId];
|
|
if (link) {
|
|
const targetNode = this.graph.getNodeById(link.target_id);
|
|
if (targetNode && targetNode.inputs && targetNode.inputs[link.target_slot]) {
|
|
const targetType = targetNode.inputs[link.target_slot].type;
|
|
const targetNodeType = targetNode.type || 'unknown';
|
|
|
|
// Normalize types for comparison (case-insensitive, handle common variations)
|
|
const normalizedOutputType = (outputType || '').toUpperCase().trim();
|
|
const normalizedTargetType = (targetType || '').toUpperCase().trim();
|
|
|
|
// Only remove links if there's a CLEAR incompatibility:
|
|
// - Target type must be explicitly defined (not undefined/null/empty/'*')
|
|
// - Target type must NOT match output type (case-insensitive)
|
|
// - Treat undefined/null/empty/'*' as wildcards (compatible with anything)
|
|
if (targetType &&
|
|
normalizedTargetType !== '*' &&
|
|
normalizedTargetType !== '' &&
|
|
normalizedOutputType !== normalizedTargetType) {
|
|
console.warn(`GetOutputNode ${this.id} (${nodeName}): Removing incompatible link ${linkId} to ${targetNodeType} (Output: "${outputType}", Target: "${targetType}")`);
|
|
linksToRemove.push(linkId);
|
|
} else {
|
|
debugLog(`GetOutputNode ${this.id} (${nodeName}): Link ${linkId} to ${targetNodeType} is compatible (Output: "${outputType}", Target: "${targetType || '*'}")`);
|
|
}
|
|
} else {
|
|
debugWarn(`GetOutputNode ${this.id} (${nodeName}): Link ${linkId} - target node or input slot not found`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (linksToRemove.length > 0) {
|
|
console.warn(`GetOutputNode ${this.id} (${nodeName}): Removing ${linksToRemove.length} incompatible link(s)`);
|
|
linksToRemove.forEach(linkId => this.graph.removeLink(linkId));
|
|
} else {
|
|
debugLog(`GetOutputNode ${this.id} (${nodeName}): All links validated successfully`);
|
|
}
|
|
};
|
|
|
|
// --- Set Output Type ---
|
|
this.setType = function(type) {
|
|
if (this.outputs && this.outputs[0]) {
|
|
if (this.outputs[0].type !== type) {
|
|
this.outputs[0].name = type;
|
|
this.outputs[0].type = type;
|
|
this.validateLinks(); // Validate after type change
|
|
this.graph.links_dirty = true; // Notify graph
|
|
}
|
|
}
|
|
};
|
|
|
|
// --- Helper: Find Active Setter ---
|
|
this.findSetter = function(graph) {
|
|
if (!graph) return null;
|
|
const name = (this.widgets && this.widgets[0]) ? this.widgets[0].value : null;
|
|
if (!name || name === '') return null;
|
|
|
|
const matchingSetters = graph._nodes.filter(otherNode =>
|
|
otherNode.type === 'SetInputNode' && // Look for SetInputNode
|
|
otherNode.widgets && otherNode.widgets[0] &&
|
|
otherNode.widgets[0].value === name
|
|
);
|
|
|
|
// LiteGraph.BYPASS is mode 4, LiteGraph.NEVER (muted) is mode 2
|
|
const activeSetter = matchingSetters.find(setter =>
|
|
setter.mode !== 4 && setter.mode !== 2
|
|
);
|
|
return activeSetter || null; // Return the first non-bypassed, non-muted or null
|
|
};
|
|
|
|
// --- Navigation ---
|
|
this.goToSetter = function() {
|
|
if (!this.graph || !this.canvas) return;
|
|
const setter = this.findSetter(this.graph);
|
|
if (setter) {
|
|
this.canvas.centerOnNode(setter);
|
|
this.canvas.selectNode(setter, false);
|
|
} else {
|
|
debugLog("GetOutputNode: No active SetInput node found to go to.");
|
|
}
|
|
};
|
|
|
|
// Node is virtual
|
|
this.isVirtualNode = true;
|
|
|
|
} // End constructor
|
|
|
|
// --- Get Input Info (for Backend Execution) ---
|
|
// This tells LiteGraph where the data actually comes from.
|
|
getInputInfo(slot) {
|
|
// Ensure graph context
|
|
if (!this.graph) {
|
|
return null;
|
|
}
|
|
// Find the active (non-bypassed) setter node linked by name
|
|
const setter = this.findSetter(this.graph);
|
|
|
|
// Check if setter exists, has an input, and that input is connected
|
|
if (!setter || !setter.inputs || !setter.inputs[0] || setter.inputs[0].link === null) {
|
|
// console.log(`GetOutputNode ${this.id} (${this.widgets[0].value}): getInputInfo - No active setter or setter input link found.`);
|
|
return null; // No active linked input found
|
|
}
|
|
|
|
// Get the ID of the link connected to the setter's input
|
|
const linkId = setter.inputs[0].link;
|
|
const link = this.graph.links[linkId];
|
|
|
|
if (!link) {
|
|
console.error(`GetOutputNode ${this.id}: getInputInfo - Link with ID ${linkId} not found in graph.`);
|
|
return null; // Link not found (shouldn't happen in a consistent graph)
|
|
}
|
|
|
|
// Find the actual source node connected to the setter
|
|
const originNode = this.graph.getNodeById(link.origin_id);
|
|
if (!originNode) {
|
|
console.error(`GetOutputNode ${this.id}: getInputInfo - Origin node with ID ${link.origin_id} not found.`);
|
|
return null;
|
|
}
|
|
|
|
// Determine the output slot index and type from the origin node
|
|
const originSlotIndex = link.origin_slot;
|
|
const originOutputType = (originNode.outputs && originNode.outputs[originSlotIndex])
|
|
? originNode.outputs[originSlotIndex].type
|
|
: '*';
|
|
|
|
// Return the information about the actual data source
|
|
// console.log(`GetOutputNode ${this.id} (${this.widgets[0].value}): Providing input info from Setter ${setter.id} -> Origin Node ${originNode.id}, Slot ${originSlotIndex} (Type: ${originOutputType})`);
|
|
return {
|
|
node_id: originNode.id,
|
|
slot: originSlotIndex,
|
|
type: originOutputType
|
|
};
|
|
}
|
|
|
|
// --- Get Input Link (for internal LiteGraph/ComfyUI connection logic) ---
|
|
// This function might be queried by the execution logic to find the
|
|
// link object providing data *to* the node connected *from* this GetNode.
|
|
getInputLink(slot) {
|
|
// This node provides output on slot 0. If queried for other slots, return null.
|
|
if (slot !== 0) return null;
|
|
|
|
// Ensure graph context
|
|
if (!this.graph) {
|
|
return null;
|
|
}
|
|
|
|
// Find the active (non-bypassed) setter node linked by name
|
|
const setter = this.findSetter(this.graph);
|
|
|
|
// Check if setter exists, has an input, and that input is connected
|
|
if (!setter || !setter.inputs || !setter.inputs[0] || typeof setter.inputs[0].link !== 'number') {
|
|
// console.log(`GetOutputNode ${this.id} (${this.widgets[0].value}): getInputLink - No active setter or setter input link found.`);
|
|
return null; // No active linked input found
|
|
}
|
|
|
|
// Get the ID of the link connected to the setter's input
|
|
const linkId = setter.inputs[0].link;
|
|
const link = this.graph.links[linkId];
|
|
|
|
if (!link) {
|
|
console.error(`GetOutputNode ${this.id}: getInputLink - Link with ID ${linkId} not found in graph.`);
|
|
return null;
|
|
}
|
|
|
|
// Return the actual link object originating from the node connected to the SetNode.
|
|
// The execution logic might trace this link's origin_id/origin_slot.
|
|
// console.log(`GetOutputNode ${this.id} (${this.widgets[0].value}): getInputLink returning link ${linkId} (Origin: ${link.origin_id}, Slot: ${link.origin_slot})`);
|
|
return link;
|
|
}
|
|
|
|
// --- Context Menu ---
|
|
getExtraMenuOptions(_, options) {
|
|
let menuEntry = this.drawConnection ? "Hide connections" : "Show connections";
|
|
options.unshift(
|
|
{
|
|
content: "Convert to Set (Input)",
|
|
callback: () => {
|
|
convertNodeInPlace(this, "SetInputNode");
|
|
},
|
|
},
|
|
{
|
|
content: "Create Set (Input) to the right",
|
|
callback: () => {
|
|
createSiblingNodeToRight(this, "SetInputNode");
|
|
},
|
|
},
|
|
{
|
|
content: "Go to active setter",
|
|
callback: () => { this.goToSetter(); },
|
|
},
|
|
{
|
|
content: menuEntry,
|
|
callback: () => {
|
|
if (!this.graph) return;
|
|
this.currentSetter = this.findSetter(this.graph);
|
|
if (!this.currentSetter) return; // Only toggle if active setter found
|
|
|
|
let linkType = (this.currentSetter.inputs[0].type || '*');
|
|
this.slotColor = this.canvas?.default_connection_color_byType?.[linkType] || "#FFF";
|
|
this.drawConnection = !this.drawConnection;
|
|
menuEntry = this.drawConnection ? "Hide connections" : "Show connections"; // Update label
|
|
this.canvas?.setDirty(true, true);
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
// --- Drawing Connections ---
|
|
onDrawForeground(ctx, graphcanvas) {
|
|
if (this.flags.collapsed || !this.drawConnection || this.mode === 4 /* Bypassed */ || this.mode === 2 /* Muted */) {
|
|
return;
|
|
}
|
|
|
|
this.currentSetter = this.findSetter(this.graph);
|
|
if (!this.currentSetter) {
|
|
// Optional: Turn off drawing if no active setter found?
|
|
// this.drawConnection = false;
|
|
return;
|
|
}
|
|
|
|
if (!this.currentSetter.isVisibleOnCanvas(graphcanvas)) return; // Don't draw if offscreen
|
|
|
|
const lGraphCanvas = graphcanvas || app.canvas;
|
|
if (!lGraphCanvas) return;
|
|
|
|
ctx.save();
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeStyle = this.slotColor || "#FFF";
|
|
|
|
// Start from the Get node's output
|
|
const startPos = this.localToGlobal([this.size[0], LiteGraph.NODE_SLOT_HEIGHT / 2]);
|
|
// End at the Set node's input
|
|
const endPos = this.currentSetter.localToGlobal([0, LiteGraph.NODE_SLOT_HEIGHT / 2]);
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(startPos[0], startPos[1]);
|
|
ctx.bezierCurveTo(
|
|
startPos[0] + 100, startPos[1],
|
|
endPos[0] - 100, endPos[1],
|
|
endPos[0], endPos[1]
|
|
);
|
|
ctx.stroke();
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
|
|
} // End GetOutputNode Class
|
|
|
|
// --- Register Nodes with LiteGraph ---
|
|
LiteGraph.registerNodeType("SetInputNode", SetInputNode);
|
|
LiteGraph.registerNodeType("GetOutputNode", GetOutputNode);
|
|
|
|
// Set category for node menu organization
|
|
SetInputNode.category = "Utilities/InputOutput";
|
|
GetOutputNode.category = "Utilities/InputOutput";
|
|
|
|
debugLog("Input/Output Nodes Registered");
|
|
|
|
} // End registerCustomNodes
|
|
}); // End app.registerExtension
|
|
|
|
} // End else (LiteGraph found)
|