Files
masslevel-ComfyUI-masslevel…/web/variable_text_processor.js
T
2026-03-07 10:42:40 +01:00

236 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Masslevel Nodes - Variable Text Processor
*
* Native LiteGraph "Rename Slot" sets input.label (display only) but NOT
* input.name (the backend kwarg key). We intercept the label setter via
* Object.defineProperty so native rename correctly updates both the pin name
* and properties.variables, keeping everything in sync.
*/
import { app } from "/scripts/app.js";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function sanitizeName(raw) {
let name = raw.trim().replace(/[^a-zA-Z0-9_]/g, "_");
if (/^[0-9]/.test(name)) name = "_" + name;
return name.length > 0 ? name : null;
}
function variableExists(node, varName) {
return (node.properties?.variables ?? []).includes(varName);
}
function getVariableInputs(node) {
const tracked = node.properties?.variables ?? [];
return (node.inputs ?? []).filter(i => tracked.includes(i.name));
}
// ---------------------------------------------------------------------------
// Label watcher — makes native "Rename Slot" behave correctly.
//
// LiteGraph's rename dialog does: input.label = userTypedName
// We intercept that assignment and route it through renameVariable() so:
// - input.name is updated (backend kwarg key)
// - properties.variables is updated (our tracking)
// - links are reconnected on the new slot
//
// The watcher is installed after every addInput() call and removed implicitly
// when renameVariable() destroys+recreates the slot.
// ---------------------------------------------------------------------------
function installLabelWatcher(node, input) {
if (input.__masslevel_watched) return;
input.__masslevel_watched = true;
let _label = input.label;
Object.defineProperty(input, "label", {
configurable: true,
enumerable: true,
get() { return _label; },
set(newLabel) {
const oldName = input.name;
const isTracked = (node.properties?.variables ?? []).includes(oldName);
if (isTracked && newLabel && newLabel !== oldName) {
const sanitized = sanitizeName(newLabel);
if (!sanitized) {
console.warn(`[masslevel] Invalid rename target "${newLabel}" — ignoring.`);
return;
}
if (variableExists(node, sanitized)) {
console.warn(`[masslevel] Variable "${sanitized}" already exists — ignoring native rename.`);
return;
}
_label = newLabel;
setTimeout(() => renameVariable(node, oldName, sanitized), 0);
} else {
_label = newLabel;
}
},
});
}
// ---------------------------------------------------------------------------
// Core operations
// ---------------------------------------------------------------------------
function addVariable(node, varName) {
if (!node.properties) node.properties = {};
if (!node.properties.variables) node.properties.variables = [];
node.properties.variables.push(varName);
node.addInput(varName, "*");
const input = node.inputs[node.inputs.length - 1];
installLabelWatcher(node, input);
node.setDirtyCanvas(true, true);
}
function removeVariable(node, varName) {
node.properties.variables = (node.properties.variables ?? []).filter(v => v !== varName);
const slot = (node.inputs ?? []).findIndex(i => i.name === varName);
if (slot !== -1) {
const input = node.inputs[slot];
if (input.link != null) node.graph.removeLink(input.link);
node.removeInput(slot);
}
node.setDirtyCanvas(true, true);
}
function renameVariable(node, oldName, newName) {
const slot = (node.inputs ?? []).findIndex(i => i.name === oldName);
if (slot === -1) {
console.warn(`[masslevel] renameVariable: slot "${oldName}" not found`);
return;
}
const input = node.inputs[slot];
const existingLinkId = input.link;
let originId = null;
let originSlot = null;
if (existingLinkId != null && node.graph) {
const link = node.graph.links[existingLinkId];
if (link) { originId = link.origin_id; originSlot = link.origin_slot; }
node.graph.removeLink(existingLinkId);
}
node.removeInput(slot);
const idx = (node.properties.variables ?? []).indexOf(oldName);
if (idx !== -1) node.properties.variables[idx] = newName;
node.addInput(newName, "*");
const newSlot = node.inputs.length - 1;
if (newSlot !== slot) {
const moved = node.inputs.splice(newSlot, 1)[0];
node.inputs.splice(slot, 0, moved);
}
installLabelWatcher(node, node.inputs[slot]);
if (originId !== null && node.graph) {
const originNode = node.graph.getNodeById(originId);
if (originNode) originNode.connect(originSlot, node, slot);
}
node.setDirtyCanvas(true, true);
}
// ---------------------------------------------------------------------------
// Extension registration
// ---------------------------------------------------------------------------
app.registerExtension({
name: "Masslevel.VariableTextProcessor",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "VariableTextProcessor") return;
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origOnNodeCreated?.call(this);
if (!this.properties) this.properties = {};
if (!this.properties.variables) this.properties.variables = [];
this.addWidget("button", "+ Add Variable", null, () => {
const raw = prompt("Variable name\n(use as {variable_name} in template):");
if (!raw) return;
const name = sanitizeName(raw);
if (!name) {
alert("Invalid variable name. Use letters, numbers, and underscores only.");
return;
}
if (variableExists(this, name)) {
alert(`Variable "${name}" already exists.`);
return;
}
addVariable(this, name);
});
};
const origOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (config) {
origOnConfigure?.call(this, config);
const variables = this.properties?.variables ?? [];
for (const varName of variables) {
let input = (this.inputs ?? []).find(i => i.name === varName);
if (!input) {
this.addInput(varName, "*");
input = this.inputs[this.inputs.length - 1];
}
installLabelWatcher(this, input);
}
};
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (canvas, options) {
origGetExtraMenuOptions?.call(this, canvas, options);
const varInputs = getVariableInputs(this);
if (varInputs.length === 0) return;
options.push(null);
const self = this;
options.push({
content: "Remove Variable Pin",
submenu: {
options: varInputs.map(input => ({
content: input.name,
callback() {
if (confirm(`Remove variable "${input.name}"?`)) {
removeVariable(self, input.name);
}
},
})),
},
});
options.push({
content: "Rename Variable Pin",
submenu: {
options: varInputs.map(input => ({
content: input.name,
callback() {
const raw = prompt(`Rename "${input.name}" to:`, input.name);
if (!raw || raw.trim() === input.name) return;
const newName = sanitizeName(raw);
if (!newName) { alert("Invalid name."); return; }
if (variableExists(self, newName)) {
alert(`Variable "${newName}" already exists.`);
return;
}
renameVariable(self, input.name, newName);
},
})),
},
});
};
},
});