Initial release
This commit is contained in:
@@ -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);
|
||||
},
|
||||
})),
|
||||
},
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user