Files
2025-06-19 13:39:06 +10:00

519 lines
18 KiB
JavaScript

import { app } from "../../../scripts/app.js";
//based on KJNodes SetGet: https://github.com/kijai/ComfyUI-KJNodes
// Configuration
const CONFIG = {
NODE: {
name: "OutputGet",
category: "Flux-Continuum/Utilities",
title: "OutputGet",
defaultColor: "#FFF",
prefix: "Output -"
},
CACHE: {
timeout: 1000,
debounceWait: 100
},
ERRORS: {
singletonError: "Error: Only one OutputGet node is allowed.",
noSetNodeError: "No Output SetNode found for",
errorTimeout: 5000
}
};
// Helper functions
function debounce(func, wait = CONFIG.CACHE.debounceWait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function createCachedFunction(fn) {
let cache = null;
let lastUpdate = 0;
return function (...args) {
const now = Date.now();
if (!cache || (now - lastUpdate) > CONFIG.CACHE.timeout) {
cache = fn.apply(this, args);
lastUpdate = now;
}
return cache;
};
}
app.registerExtension({
name: "FluxContinuum.OutputGet",
registerCustomNodes() {
class OutputGetNode extends LGraphNode {
static instance = null;
constructor(title) {
super(title);
if (OutputGetNode.instance) {
alert(CONFIG.ERRORS.singletonError);
setTimeout(() => this.graph?.remove(this), 0);
return;
}
OutputGetNode.instance = this;
if (!this.properties) {
this.properties = {};
}
this.properties.showOutputText = true;
const node = this;
const widget = this.addWidget(
"combo",
"Selected",
"",
function (value, canvas, node, pos, event) {
try {
this.value = value;
node.widgets[0].value = value;
node.onRename();
node.graph._version++;
node.setDirtyCanvas(true, true);
node.updateGetStringTitles();
} catch (error) {
console.error("Error in combo widget callback:", error);
}
},
{
values: () => node.getSetterNodeTitles(node.graph),
}
);
this.addOutput("*", '*');
this.onConnectionsChange = function (slotType, slot, isChangeConnect, link_info, output) {
try {
this.validateLinks();
if (link_info && node.graph) {
const setter = node.findSetter(node.graph);
if (setter) {
let linkType = setter.inputs[0].type;
node.setType(linkType);
node.updateGetStringTitles();
}
}
node.setDirtyCanvas(true, true);
} catch (error) {
console.error("Error in onConnectionsChange:", error);
}
};
this.setName = function (name) {
try {
node.widgets[0].value = name;
node.onRename();
node.serialize();
node.updateGetStringTitles();
} catch (error) {
console.error("Error in setName:", error);
}
};
this.onRename = function () {
try {
const setter = node.findSetter(node.graph);
if (setter) {
let linkType = setter.inputs[0].type;
node.setType(linkType);
node.title = setter.widgets[0].value;
if (app.ui.settings.getSettingValue("KJNodes.nodeAutoColor")) {
setColorAndBgColor.call(node, linkType);
}
node.updateGetStringTitles();
} else {
node.setType('*');
}
} catch (error) {
console.error("Error in onRename:", error);
}
};
this.updateGetStringTitles = debounce(function () {
try {
const getStringNodes = this.graph._nodes.filter((n) => n.type === "OutputGetString");
getStringNodes.forEach((getStringNode) => {
getStringNode.title = "GetString_" + this.title;
});
} catch (error) {
console.error("Error updating GetString titles:", error);
}
}, CONFIG.CACHE.debounceWait);
this.getSetterNodeTitles = createCachedFunction(function (graph) {
return graph._nodes
.filter(node => node.type === 'SetNode' && node.widgets[0].value.startsWith(CONFIG.NODE.prefix))
.map(node => node.widgets[0].value)
.sort();
});
this.defaultVisibility = true;
this.serialize_widgets = true;
this.drawConnection = false;
this.slotColor = CONFIG.NODE.defaultColor;
this.currentSetter = null;
this.canvas = app.canvas;
this.isVirtualNode = true;
}
onRemoved() {
if (OutputGetNode.instance === this) {
OutputGetNode.instance = null;
}
this.drawConnection = false;
this.currentSetter = null;
}
validateLinks() {
if (this.outputs[0].type !== '*' && this.outputs[0].links) {
this.outputs[0].links.filter((linkId) => {
const link = this.graph.links[linkId];
return link && (link.type !== this.outputs[0].type && link.type !== '*');
}).forEach((linkId) => {
this.graph.removeLink(linkId);
});
}
}
setType(type) {
this.outputs[0].name = type;
this.outputs[0].type = type;
this.validateLinks();
}
findSetter(graph) {
const name = this.widgets[0].value;
return graph._nodes.find(
(otherNode) =>
otherNode.type === 'SetNode' &&
otherNode.widgets[0].value === name &&
name !== '' &&
name.startsWith(CONFIG.NODE.prefix)
);
}
goToSetter() {
const setter = this.findSetter(this.graph);
if (setter) {
this.canvas.centerOnNode(setter);
this.canvas.selectNode(setter);
}
}
getInputLink(slot) {
try {
const setter = this.findSetter(this.graph);
if (setter) {
const slotInfo = setter.inputs[slot];
return this.graph.links[slotInfo.link];
} else {
const message = `${CONFIG.ERRORS.noSetNodeError} ${this.widgets[0].value}`;
if (!window.isAlertShown) {
window.isAlertShown = true;
alert(message);
setTimeout(() => (window.isAlertShown = false), CONFIG.ERRORS.errorTimeout);
}
}
} catch (error) {
console.error("Error in getInputLink:", error);
return null;
}
}
getExtraMenuOptions(_, options) {
let menuEntry = this.drawConnection ? "Hide connections" : "Show connections";
options.unshift(
{
content: "Go to output setter",
callback: () => {
this.goToSetter();
},
},
{
content: menuEntry,
callback: () => {
try {
this.currentSetter = this.findSetter(this.graph);
if (!this.currentSetter) return;
let linkType = this.currentSetter.inputs[0].type;
this.drawConnection = !this.drawConnection;
this.slotColor = this.canvas.default_connection_color_byType[linkType];
this.canvas.setDirty(true, true);
} catch (error) {
console.error("Error in menu callback:", error);
}
},
}
);
}
onDrawForeground(ctx, lGraphCanvas) {
if (this.drawConnection) {
this._drawVirtualLink(lGraphCanvas, ctx);
}
}
_drawVirtualLink(lGraphCanvas, ctx) {
if (!this.currentSetter) return;
try {
let start_node_slotpos = this.currentSetter.getConnectionPos(false, 0);
start_node_slotpos = [
start_node_slotpos[0] - this.pos[0],
start_node_slotpos[1] - this.pos[1],
];
let end_node_slotpos = [0, -LiteGraph.NODE_TITLE_HEIGHT * 0.5];
lGraphCanvas.renderLink(
ctx,
start_node_slotpos,
end_node_slotpos,
null,
false,
null,
this.slotColor
);
} catch (error) {
console.error("Error drawing virtual link:", error);
}
}
}
LiteGraph.registerNodeType(CONFIG.NODE.name, OutputGetNode);
OutputGetNode.category = CONFIG.NODE.category;
const originalCheckNodeTypes = LGraphCanvas.prototype.checkNodeTypes;
LGraphCanvas.prototype.checkNodeTypes = function() {
const r = originalCheckNodeTypes.apply(this, arguments);
if (r && r.type === CONFIG.NODE.name && OutputGetNode.hasInstance()) {
r.disabled = true;
r.tooltip = CONFIG.ERRORS.singletonError;
}
return r;
};
},
});
app.registerExtension({
name: "FluxContinuum.OutputGetString",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "OutputGetString") {
return;
}
// Ensure the widget is hidden and updated
const ensureHiddenWidget = (node) => {
let widget = node.widgets?.find((w) => w.name === "title");
if (!widget) {
// If the widget does not exist, create it
widget = { name: "title", value: "", hidden: true, serialize: true };
node.widgets = node.widgets || [];
node.widgets.push(widget);
}
// Keep it hidden and serialized
widget.hidden = true;
return widget;
};
// Update title and widget values
const updateGetStringTitles = (graph) => {
const getStringNodes = graph._nodes.filter((n) => n.type === "OutputGetString");
const outputGetNode = graph._nodes.find((n) => n.type === CONFIG.NODE.name);
getStringNodes.forEach((node) => {
const titleWidget = ensureHiddenWidget(node);
if (outputGetNode) {
// Update the title and widget value
const newTitle = "GetString_" + outputGetNode.title;
node.title = newTitle;
titleWidget.value = newTitle.replace("GetString_", "");
} else {
// Default title if no OutputGet node
node.title = "GetString";
titleWidget.value = "";
}
});
};
// Hijack app.queuePrompt to update titles on execution
const originalQueuePrompt = app.queuePrompt;
app.queuePrompt = async function () {
const graph = app.graph;
if (graph) {
updateGetStringTitles(graph);
}
return await originalQueuePrompt.apply(this, arguments);
};
// Initialize hidden widget on node addition
nodeType.prototype.onAdded = function (graph) {
ensureHiddenWidget(this);
this.updateTitle();
};
// Update title when called
nodeType.prototype.updateTitle = function () {
if (!this.graph) {
return;
}
const outputGetNode = this.findOutputGetNode(this.graph);
if (outputGetNode) {
this.title = "GetString_" + outputGetNode.title;
} else {
this.title = "GetString";
}
};
// Find the OutputGet node in the graph
nodeType.prototype.findOutputGetNode = function (graph) {
return graph._nodes.find((node) => node.type === CONFIG.NODE.name);
};
},
});
class TextDisplay extends LGraphNode {
constructor() {
super();
this.isVirtualNode = true;
this.shape = LiteGraph.BOX_SHAPE;
this.size = [200, 50];
this.resizable = true;
this.properties = {
fontSize: 24,
fontFamily: "Arial",
fontColor: "#ffffff",
textAlign: "center",
bgColor: "transparent",
padding: 10
};
this.widgets_up = true;
this.bgcolor = "#00000000";
}
onAdded(graph) {
// When added to a graph, move to end of nodes array
if (graph && graph._nodes) {
const index = graph._nodes.indexOf(this);
if (index !== -1) {
graph._nodes.splice(index, 1);
graph._nodes.push(this);
}
}
}
onDragFinished() {
// After dragging, ensure node stays at end of array
if (this.graph && this.graph._nodes) {
const index = this.graph._nodes.indexOf(this);
if (index !== -1) {
this.graph._nodes.splice(index, 1);
this.graph._nodes.push(this);
}
}
}
onDrawForeground(ctx) {
if (!this.graph) return;
// Always ensure this node is at the end of the array before drawing
const index = this.graph._nodes.indexOf(this);
if (index !== -1 && index !== this.graph._nodes.length - 1) {
this.graph._nodes.splice(index, 1);
this.graph._nodes.push(this);
}
const outputGetNode = this.findOutputGetNode(this.graph);
if (!outputGetNode || !outputGetNode.widgets[0]) {
this.displayText = "NO OUTPUTGET NODE FOUND";
} else {
this.displayText = outputGetNode.widgets[0].value || "NO SELECTION";
this.displayText = this.displayText.toUpperCase();
}
ctx.save();
ctx.textAlign = this.properties.textAlign;
ctx.textBaseline = "middle";
ctx.fillStyle = this.properties.fontColor || "#ffffff";
ctx.font = `${this.properties.fontSize}px ${this.properties.fontFamily}`;
const x = this.properties.textAlign === "center" ? this.size[0] / 2 :
this.properties.textAlign === "right" ? this.size[0] - 10 : 10;
const y = this.size[1] / 2;
ctx.fillText(this.displayText, x, y);
ctx.restore();
}
findOutputGetNode(graph) {
return graph._nodes.find((node) => node.type === "OutputGet");
}
onResize(size) {
size[0] = Math.max(50, size[0]);
size[1] = Math.max(20, size[1]);
return size;
}
onDblClick(event, pos, canvas) {
LGraphCanvas.active_canvas.showShowNodePanel(this);
}
}
// Node type registration with required static properties
TextDisplay.title = "OutputTextDisplay";
TextDisplay.title_mode = LiteGraph.NO_TITLE;
TextDisplay.collapsable = false;
const oldDrawNode = LGraphCanvas.prototype.drawNode;
LGraphCanvas.prototype.drawNode = function(node, ctx) {
if (node.constructor === TextDisplay) {
const tmp_shape = node.shape;
node.shape = null;
const r = oldDrawNode.apply(this, arguments);
if (node.onDrawForeground) {
node.onDrawForeground(ctx);
}
node.shape = tmp_shape;
return r;
}
return oldDrawNode.apply(this, arguments);
};
app.registerExtension({
name: "FluxContinuum.OutputTextDisplay",
registerCustomNodes() {
LiteGraph.registerNodeType(
"OutputTextDisplay",
Object.assign(TextDisplay, {
title: "OutputTextDisplay",
title_mode: LiteGraph.NO_TITLE,
category: "Flux-Continuum/Utilities",
comfyClass: "OutputTextDisplay",
"@fontSize": { type: "number" },
"@fontFamily": { type: "string" },
"@fontColor": { type: "string", default: "#ffffff" },
"@bgColor": { type: "string", default: "#00000000" },
"@textAlign": { type: "combo", values: ["left", "center", "right"] },
"@padding": { type: "number" }
})
);
TextDisplay.prototype.flags = { no_title: true };
}
});