Initial release

This commit is contained in:
masslevel
2026-03-07 10:42:40 +01:00
commit 8b57717806
7 changed files with 797 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
/**
* 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);
},
})),
},
});
};
},
});