/** * 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); }, })), }, }); }; }, });