519 lines
18 KiB
JavaScript
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 };
|
|
}
|
|
}); |