import { app } from "../../../../scripts/app.js"; import { NiftyHelpers } from "./helpers.js"; export class NiftyNode { nodeType = null; nodeData = ""; name = ""; args = {}; get LGraphCanvas() { return app.canvas?.constructor ?? window.LGraphCanvas } get isLoading() { return app.configuringGraph; } constructor(nodeType, nodeData, args = {}) { this.nodeType = nodeType; this.nodeData = nodeData; this.name = nodeData.name; this.args = { ...{ hideTitle: false, color: null, width: null, height: null, forceSize: false, size: null, hideWidgets: [], hideInputs: false, hideOutputs: false, syncOutputs: false, removeInputs: [], dynInputs: null, dynOutputs: null, handleWidgets: null, properties: [], autogrowWidget: null, nodeProps: {}, }, ...args }; this.applyArgs(); } applyHook(hook, callback) { const origHook = this.nodeType.prototype[hook]; const self = this; this.nodeType.prototype[hook] = function(...args) { origHook?.apply(this, args); callback?.call(self, this, ...args); } } applyHookAfter(hook, callback) { const origHook = this.nodeType.prototype[hook]; const self = this; this.nodeType.prototype[hook] = function(...args) { callback?.call(self, this, ...args); origHook?.apply(this, args); } } addEventListener(name, callback) { app.api.addEventListener(name, (event, ...args) => { const node = app.graph.getNodeById(event.detail?.node); if(node && node?.type === this.name) { callback.call(this, event, node, event.detail?.output ?? {}, ...args); } }); } applyArgs() { // Prepare handle widgets // if(this.args.handleWidgets) { // this.args.handleWidgets = this.expandHandleWidgets( // this.args.handleWidgets // ); // } // Set title mode if(this.args.hideTitle) { this.nodeType.title_mode = LiteGraph.NO_TITLE; this.nodeType.prototype.collapse = () => {}; } // Set forced size if(this.args.forceSize) { this.nodeType.prototype.computeSize = () => { return this.args.forceSize; }; } this.applyHook("onNodeCreated", function(node) { // Set intial size if((this.args.width || this.args.height) && !this.isLoading) { let initialWidth = this.args.width ?? (node.size ? node.size[0] : LiteGraph.NODE_WIDTH); let initialHeight = this.args.height ?? (node.size ? node.size[1] : 20); node.size = [initialWidth, initialHeight]; } // Set color if(this.args.color) { const colors = this.LGraphCanvas?.node_colors?.[this.args.color]; if(colors) { node.color = colors.color; node.bgcolor = colors.bgcolor; } } // Set additional node properties for(const [key, value] of Object.entries(this.args.nodeProps)) { node[key] = value; } // Set settings properties this.args.properties.forEach((property) => { node.addProperty(...property); }) // Hide widgets if(this.args.hideWidgets.length) { this.args.hideWidgets.forEach((name) => { this.hideWidget(node, name); }); this.shrinkNode(node); } // Hide inputs if(this.args.hideInputs) { if(typeof this.args.hideInputs === "string") { this.onCallback(node, this.args.hideInputs, (widget, value, node) => { node._nifty_hide_inputs = value; }); } else { node._nifty_hide_inputs = true; } } // Hide outputs if(this.args.hideOutputs) { if(typeof this.args.hideOutputs === "string") { this.onCallback(node, this.args.hideOutputs, (widget, value, node) => { node._nifty_hide_outputs = value; }); } else { node._nifty_hide_outputs = true; } } // Sync output types if(this.args.syncOutputs) { node._nifty_sync_output = this.args.syncOutputs; if(node._nifty_sync_output.inputs) { node._nifty_sync_output.inputs = this.getFlatSlotArray( node._nifty_sync_output.inputs ); } } // Dyn inputs if(this.args.dynInputs) { if(!this.isLoading) { this.handleDynSlots( node, this.args.dynInputs ); } if(this.args.dynInputs.trigger === "widget" && this.args.dynInputs.widget) { this.onCallback(node, this.args.dynInputs.widget, (widget, value, node) => { this.handleDynSlots( node, this.args.dynInputs ); }); } } // Dyn outputs if(this.args.dynOutputs) { if(!this.isLoading) { this.handleDynSlots( node, this.args.dynOutputs, "output" ); } if(this.args.dynOutputs.trigger === "widget" && this.args.dynOutputs.widget) { this.onCallback(node, this.args.dynOutputs.widget, (widget, value, node) => { this.handleDynSlots( node, this.args.dynOutputs, "output" ); }); } } // Handle widgets if(this.args.handleWidgets) { if(!this.isLoading) { this.handleWidgets(node, false); } for(const [name, callback] of Object.entries(this.args.handleWidgets)) { this.onCallback( node, name, (widget, value, node) => { callback.call( this, widget, value, Object.fromEntries(node.widgets.map(w => [w.name, w])), node, true ); } ); } } // Autogrow widgets if(this.args.autogrowWidget) { const sourceWidgetName = this.args.autogrowWidget.source; this.onCallback(node, sourceWidgetName, (widget, value, node) => { this.handleAutogrowWidgets(node); }); } }); this.applyHook("onAfterGraphConfigured", function(node) { // Set forced size if(this.args.forceSize) { node.setSize(this.args.forceSize); } // Hide inputs if(this.args.hideInputs) { if(typeof this.args.hideInputs === "string") { node._nifty_hide_inputs = this.getValue( node, this.args.hideInputs ); } } // Hide outputs if(this.args.hideOutputs) { if(typeof this.args.hideOutputs === "string") { node._nifty_hide_outputs = this.getValue( node, this.args.hideOutputs ); } } // Remove inputs if(this.args.removeInputs) { this.removeInput(node, this.args.removeInputs); } // Dyn inputs if(this.args.dynInputs) { this.handleDynSlots( node, this.args.dynInputs ); } // Dyn outputs if(this.args.dynOutputs) { this.handleDynSlots( node, this.args.dynOutputs, "output" ); } // Handle widgets if(this.args.handleWidgets) { this.handleWidgets(node, false); } }); // Dyn inputs if(this.args.dynInputs && (this.args.dynInputs.trigger !== "widget" || !this.args.dynInputs.widget)) { this.applyHook("onConnectionsChange", function(node, side, slotId, connected, link, slot) { if(side === LiteGraph.INPUT) { this.handleDynSlots( node, this.args.dynInputs ); } }); } // Dyn outputs if(this.args.dynOutputs && (this.args.dynOutputs.trigger !== "widget" || !this.args.dynOutputs.widget)) { this.applyHook("onConnectionsChange", function(node, side, slotId, connected, link, slot) { if(side === LiteGraph.OUTPUT) { this.handleDynSlots( node, this.args.dynOutputs, "output" ); } }); } this.applyHook("onConfigure", function(node) { // Autogrow widgets if(this.args.autogrowWidget) { const sourceWidgetName = this.args.autogrowWidget.source; const sourceWidget = this.getWidget(node, sourceWidgetName); const sourceIndex = node.widgets.indexOf(sourceWidget); const values = node.widgets_values.slice(sourceIndex + 1); const prefix = this.args.autogrowWidget.prefix; const startNr = parseInt(this.args.autogrowWidget.source.replace(prefix, "")) + 1; values.forEach((val, index) => { values[index] = val.trim(); }); while(values.length > 0 && values.at(-1) === "") { values.pop(); } for(let i = 0; i < values.length; i++) { const widgetName = prefix + (startNr + i); this.addAutogrowWidget(node, widgetName, values[i]); } this.handleAutogrowWidgets(node); } }); } hideExecutionTime(node) { if(node.executionDuration) { node.executionDuration = 0; } } handleWidgets(node, isCallback = false) { const widgets = Object.fromEntries(node.widgets.map(w => [w.name, w])); for(const [name, callback] of Object.entries(this.args.handleWidgets)) { const widget = this.getWidget(node, name); if(!widget) { continue; } callback.call(this, widget, widget.value, widgets, node, isCallback); } } addAutogrowWidget(node, widgetName, value = "") { const widget = node.addWidget( "string", widgetName, value, () => { this.handleAutogrowWidgets(node); } ); const input = this.getInput(node, widgetName); if(input) { widget.label = input.label ?? widgetName; } else { widget.label = widgetName; node.addInput(widgetName, "STRING", { widget: { name: widgetName } }); } return widget; } handleAutogrowWidgets(node) { const prefix = this.args.autogrowWidget.prefix; const pattern = new RegExp("^" + prefix + "([1-9]\\d*)$"); const autogrowWidgets = node.widgets.filter(widget => pattern.test(widget.name)); let lastFilled = -1; autogrowWidgets.forEach((widget, index) => { if(widget.value && widget.value.trim() !== "") { lastFilled = index; } }); const needed = Math.max(1, lastFilled + 2); while(autogrowWidgets.length > needed) { const remove = autogrowWidgets.pop(); node.removeWidget(remove); const inputIndex = node.inputs.findIndex(inp => inp.name === remove.name); if(inputIndex !== -1) { node.removeInput(inputIndex); } } while(autogrowWidgets.length < needed) { const widgetName = prefix + (autogrowWidgets.length + 1); const widget = this.addAutogrowWidget(node, widgetName); autogrowWidgets.push(widget); } this.shrinkNode(node); } handleDynSlots(node, args, slotsType = "input") { args = { ...{ slots: slotsType, min: 2, max: 16, type: "*", syncType: "input", shape: slotsType === "input" ? LiteGraph.SlotShape.HollowCircle : undefined, trigger: "auto", // auto/widget widget: null, syncOutputs: false }, ...args }; const isDynamic = (slot) => { if(!slot) { return false; } if(args.slots === "input" && slot.widget) { return false; } for(let i = 1; i <= args.max; i++) { if(slot.name === `${args.name}${i}`) { return true; } } return false; }; let slots = args.slots === "input" ? node.inputs : node.outputs; slots = slots.map((slot, realIndex) => ({ slot, realIndex })); slots = slots.filter(({ slot }) => isDynamic(slot)); const targetCount = this.getDynSlotsTarget( node, args, slots ); for(let index = args.max - 1; index > args.min - 1; index--) { const entry = slots[index]; const slotName = `${args.name}${index+1}`; if(index >= targetCount) { if(entry) { if(args.slots === "input") { node.removeInput(entry.realIndex); if(args.syncOutputs) { node.removeOutput(entry.realIndex); } } else { node.removeOutput(entry.realIndex); } } } else if(!entry) { let slot = null; if(args.slots === "input") { node.addInput(slotName, args.type); slot = this.getInput(node, slotName); if(args.syncOutputs) { node.addOutput(slotName, args.type); } } else { if(args.syncType) { if(args.type === ":first") { args.type = node.outputs[0].type; } node.addOutput(slotName, args.type); } else { node.addOutput(slotName, args.type); } slot = this.getOutput(node, slotName); } if(slot) { slot.localized_name = slotName; slot.shape = args.shape; } } } const allSlots = args.slots === "input" ? node.inputs : node.outputs; const anyInputs = new Set(); for(let i = 0; i < allSlots.length; i++) { const slot = allSlots[i]; if(slot.type === "*") { anyInputs.add(slot.name); } } node._nifty_any_type_input_slots = anyInputs; this.shrinkNode(node); } getDynSlotsTarget(node, args, slots) { if(args.trigger === "widget") { const widget = node.widgets?.find(w => w.name === args.widget); const value = parseInt(widget?.value) || args.min; return Math.min(Math.max(value, args.min), args.max); } const isConnected = (slot) => { if(!slot) return false; return args.slots === "input" ? !!slot.link : !!(slot.links?.length); }; let lastSlot = args.min - 1; for(let index = args.max - 1; index > args.min - 1; index--) { if(isConnected(slots[index]?.slot) || isConnected(slots[index - 1]?.slot)) { lastSlot = index; break; } } return lastSlot + 1; } getWidget(node, name) { return node.widgets.find(w => w.name === name); } getWidgetByType(node, type) { return node.widgets.find(w => w.type === type); } getInput(node, name) { return node.inputs.find(w => w.name === name); } getOutput(node, name) { return node.outputs.find(w => w.name === name); } findNodesByName(startNode, name, fromRoot = false) { const results = []; const startGraph = fromRoot ? app.graph : startNode?.graph; if(!startGraph) { return results; } function walk(graph) { const nodes = graph._nodes || graph.nodes || []; for(const node of nodes) { if(node.title === name || node.type === name) { results.push(node); } if(node.subgraph) { walk(node.subgraph); } } } walk(startGraph); results.sort((a, b) => { const ay = a.pos?.[1] ?? 0; const by = b.pos?.[1] ?? 0; if (ay !== by) return ay - by; const ax = a.pos?.[0] ?? 0; const bx = b.pos?.[0] ?? 0; return ax - bx; }); return results; } findNodesByType(name) { const results = []; const startGraph = app.graph; if(!startGraph) { return results; } function walk(graph) { const nodes = graph._nodes || graph.nodes || []; for(const node of nodes) { if(node.type === name) { results.push(node); } if(node.subgraph) { walk(node.subgraph); } } } walk(startGraph); results.sort((a, b) => { const ay = a.pos?.[1] ?? 0; const by = b.pos?.[1] ?? 0; if (ay !== by) return ay - by; const ax = a.pos?.[0] ?? 0; const bx = b.pos?.[0] ?? 0; return ax - bx; }); return results; } connectByName(node1, outName, node2, inName, removeOutLinks = true) { const sourceIndex = node1.outputs.findIndex(x => x.name === outName); const targetIndex = node2.inputs.findIndex(x => x.name === inName); if(removeOutLinks) { const sourceSlot = node1.outputs.find(o => o.name === outName); for(const linkId of [...sourceSlot.links]) { node1.graph.removeLink(linkId); } } if(sourceIndex !== -1 && targetIndex !== -1) { node1.connect(sourceIndex, node2, targetIndex); } } shrinkNode(node) { const computed = node.computeSize?.() || node.size; node.setSize?.([node.size[0], computed[1]]); } toggleWidget(node, widget, show = true) { if(typeof widget === "string") { widget = this.getWidget(node, widget); if(!widget) { return; } } widget.hidden = !show; widget.disabled = !show; widget.y = !show ? -100 : 0; // if(!show) { // widget.computeSize = () => [0, -4]; // @todo: test // widget.draw = () => {}; // @todo: test // } const input = this.getInput(node, widget.name); if(!show && input?.link != null) { node.graph.removeLink(input.link); } } showWidget(node, widget) { this.toggleWidget(node, widget, true); } hideWidget(node, widget) { this.toggleWidget(node, widget, false); } removeInput(node, name) { NiftyHelpers.removeInput(node, name); } getValue(node, widgetName) { const widget = this.getWidget(node, widgetName); if(!widget) { return null; } const value = widget.value; switch(widget.type) { case "INT": case "int": return parseInt(value ?? 0); case "FLOAT": case "float": return parseFloat(value ?? 0); case "BOOLEAN": case "bool": return Boolean(value); case "STRING": case "string": return value ?? ""; default: return value; } } onCallback(node, widgetName, callback) { if(!Array.isArray(widgetName)) { widgetName = [widgetName]; } widgetName.forEach((widgetNameSingle) => { const widget = this.getWidget(node, widgetNameSingle); if(!widget) { return; } const origCallback = widget.callback; widget.callback = (...args) => { origCallback?.apply(widget, args); callback.call(this, widget, this.getValue(node, widgetNameSingle), node, widgetNameSingle); }; }); } getFlatSlotArray(slots) { return slots.flatMap(str => { const match = str.match(/(\w+)(\d+)\.\.\.(\d+)/); if (!match) return [str]; const [, prefix, start, end] = match; const s = parseInt(start, 10); const e = parseInt(end, 10); return Array.from( { length: e - s + 1 }, (_, i) => `${prefix}${s + i}` ); }); } }