Initial release
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 masslevel
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,107 @@
|
||||
# ComfyUI-masslevel-TextProcessing
|
||||
|
||||
Text processing utilities for ComfyUI, by [masslevel](https://github.com/masslevel).
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Nodes
|
||||
|
||||
### Variable Text Processor (masslevel)
|
||||
|
||||
Processes a multiline text template by replacing `{variable_name}` placeholders with dynamically wired input values. Pins can be added, removed, and renamed at any time — the pin name is the variable name.
|
||||
|
||||
**Category:** `masslevel/Text`
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Clone or download this repository into your `ComfyUI/custom_nodes/` directory:
|
||||
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/masslevel/ComfyUI-masslevel-TextProcessing
|
||||
```
|
||||
|
||||
Restart ComfyUI.
|
||||
|
||||
---
|
||||
|
||||
## Variable Text Processor — Usage
|
||||
|
||||
### Basic concept
|
||||
|
||||
Write a template with `{placeholder}` tokens, wire any value into a named pin, and the node outputs the fully resolved string.
|
||||
|
||||
```
|
||||
Template:
|
||||
resolution: {video_width}x{video_height}
|
||||
fps: {video_fps}
|
||||
model: {model_name}
|
||||
cfg: {sampler_cfg}
|
||||
|
||||
Wired:
|
||||
video_width → 1920
|
||||
video_height → 1080
|
||||
video_fps → 25
|
||||
model_name → "model.safetensors"
|
||||
sampler_cfg → 25.0
|
||||
|
||||
Output:
|
||||
resolution: 1920x1080
|
||||
fps: 25
|
||||
model: model.safetensors
|
||||
cfg: 25.0
|
||||
```
|
||||
|
||||
### Adding variables
|
||||
|
||||
Click the **+ Add Variable** button on the node body. A prompt will ask for a name — use letters, numbers, and underscores only (e.g. `fps`, `model_name`, `subject`). The name becomes the `{placeholder}` token in your template.
|
||||
|
||||
### Removing variables
|
||||
|
||||
Right-click the node body → **Remove Variable Pin** → select the variable to remove. Any connected wire is cleanly disconnected first.
|
||||
|
||||
### Renaming variables
|
||||
|
||||
Two methods — both keep the connected wire in place:
|
||||
|
||||
- **Context menu:** Right-click the node body → **Rename Variable Pin** → select the variable and enter a new name.
|
||||
- **Native rename:** Right-click the connection point on the pin → **Rename Slot** → enter a new name.
|
||||
|
||||
### Inputs
|
||||
|
||||
| Input | Type | Description |
|
||||
|---|---|---|
|
||||
| `template` | STRING (multiline) | The template text containing `{placeholder}` tokens. Can be typed directly or wired from an external multiline string node. |
|
||||
| *(dynamic)* | STRING / INT / FLOAT | One pin per variable. The pin name must match the `{placeholder}` in the template exactly. INT and FLOAT values are automatically converted to string. |
|
||||
|
||||
### Output
|
||||
|
||||
| Output | Type | Description |
|
||||
|---|---|---|
|
||||
| `text` | STRING | The fully resolved template. Unmatched `{placeholders}` with no connected pin are passed through unchanged. |
|
||||
|
||||
### Notes
|
||||
|
||||
- Variable names and their order persist across workflow save and reload.
|
||||
- Unresolved placeholders are passed through unchanged — they will not error.
|
||||
- The template field itself can be wired from an external multiline text node.
|
||||
|
||||
---
|
||||
|
||||
## Compatibility
|
||||
|
||||
- ComfyUI (latest)
|
||||
- Python 3.10+
|
||||
- No additional dependencies required
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
masslevel-Text_processing — ComfyUI custom nodes by masslevel.
|
||||
Text processing and template utilities.
|
||||
"""
|
||||
|
||||
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
@@ -0,0 +1,330 @@
|
||||
{
|
||||
"id": "d5f301a1-3d95-4760-8c89-58d16bdb9056",
|
||||
"revision": 0,
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 14,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 3,
|
||||
"type": "PrimitiveInt",
|
||||
"pos": [
|
||||
1300,
|
||||
8430
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "INT",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
10
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveInt"
|
||||
},
|
||||
"widgets_values": [
|
||||
1920,
|
||||
"fixed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "PrimitiveInt",
|
||||
"pos": [
|
||||
1300,
|
||||
8550
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "INT",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
11
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveInt"
|
||||
},
|
||||
"widgets_values": [
|
||||
1080,
|
||||
"fixed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "PrimitiveInt",
|
||||
"pos": [
|
||||
1300,
|
||||
8670
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "INT",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
12
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveInt"
|
||||
},
|
||||
"widgets_values": [
|
||||
25,
|
||||
"fixed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "PrimitiveString",
|
||||
"pos": [
|
||||
1300,
|
||||
8790
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
14
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveString"
|
||||
},
|
||||
"widgets_values": [
|
||||
"model.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "PrimitiveFloat",
|
||||
"pos": [
|
||||
1300,
|
||||
8890
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "FLOAT",
|
||||
"type": "FLOAT",
|
||||
"links": [
|
||||
13
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveFloat"
|
||||
},
|
||||
"widgets_values": [
|
||||
25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "PreviewAny",
|
||||
"pos": [
|
||||
2400,
|
||||
8530
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
160
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "VariableTextProcessor",
|
||||
"pos": [
|
||||
1800,
|
||||
8530
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
270
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "video_width",
|
||||
"type": "*",
|
||||
"link": 10
|
||||
},
|
||||
{
|
||||
"name": "video_height",
|
||||
"type": "*",
|
||||
"link": 11
|
||||
},
|
||||
{
|
||||
"name": "video_fps",
|
||||
"type": "*",
|
||||
"link": 12
|
||||
},
|
||||
{
|
||||
"name": "model_name",
|
||||
"type": "*",
|
||||
"link": 14
|
||||
},
|
||||
{
|
||||
"name": "sampler_cfg",
|
||||
"type": "*",
|
||||
"link": 13
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
9
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VariableTextProcessor",
|
||||
"variables": [
|
||||
"video_width",
|
||||
"video_height",
|
||||
"video_fps",
|
||||
"model_name",
|
||||
"sampler_cfg"
|
||||
]
|
||||
},
|
||||
"widgets_values": [
|
||||
"resolution: {video_width}x{video_height}\nfps: {video_fps}\nmodel: {model_name}\ncfg: {sampler_cfg}",
|
||||
null
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
9,
|
||||
10,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
10,
|
||||
3,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
11,
|
||||
4,
|
||||
0,
|
||||
10,
|
||||
1,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
12,
|
||||
5,
|
||||
0,
|
||||
10,
|
||||
2,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
13,
|
||||
6,
|
||||
0,
|
||||
10,
|
||||
4,
|
||||
"FLOAT"
|
||||
],
|
||||
[
|
||||
14,
|
||||
8,
|
||||
0,
|
||||
10,
|
||||
3,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"workflowRendererVersion": "LG",
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
-1200,
|
||||
-8330
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.39.19",
|
||||
"VHS_latentpreview": false,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Masslevel Nodes - Variable Text Processor
|
||||
Processes a multiline text template, replacing {variable} placeholders
|
||||
with dynamically wired input values.
|
||||
"""
|
||||
|
||||
|
||||
class AnyType(str):
|
||||
"""
|
||||
A special type that matches any ComfyUI type for wildcard inputs.
|
||||
By overriding __ne__ to always return False, it satisfies ComfyUI's
|
||||
type compatibility checks for any connected output type.
|
||||
"""
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# Singleton wildcard type instance
|
||||
any_type = AnyType("*")
|
||||
|
||||
|
||||
class VariableTextProcessor:
|
||||
"""
|
||||
Processes a multiline text template by replacing {variable_name}
|
||||
placeholders with values from dynamically named input pins.
|
||||
|
||||
Dynamic inputs are managed entirely from the JS frontend — users
|
||||
add/remove/rename variable pins via the node's UI. Each pin name
|
||||
corresponds directly to a {placeholder} in the template.
|
||||
|
||||
Unresolved placeholders (no connected pin) pass through unchanged.
|
||||
INT and FLOAT inputs are automatically converted to string.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict:
|
||||
return {
|
||||
"required": {
|
||||
"template": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "Enter your template here.\nUse {variable_name} as placeholders.",
|
||||
}),
|
||||
},
|
||||
# Dynamic variable inputs are added by the JS frontend.
|
||||
# They arrive here as **kwargs at execution time.
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs) -> bool:
|
||||
"""
|
||||
Bypass ComfyUI's strict input validation so dynamically added
|
||||
variable pins (not declared in INPUT_TYPES) are accepted without
|
||||
type errors.
|
||||
"""
|
||||
return True
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("text",)
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "masslevel/Text"
|
||||
|
||||
def process(self, template: str, **kwargs) -> tuple[str]:
|
||||
"""
|
||||
Replace all {key} placeholders in the template with the
|
||||
corresponding kwarg values. Unmatched placeholders are left as-is.
|
||||
|
||||
Args:
|
||||
template: The multiline template string containing {placeholders}.
|
||||
**kwargs: Named variable inputs wired from the frontend pins.
|
||||
|
||||
Returns:
|
||||
A tuple containing the processed string.
|
||||
"""
|
||||
result = template
|
||||
|
||||
for key, value in kwargs.items():
|
||||
placeholder = f"{{{key}}}"
|
||||
if placeholder in result:
|
||||
result = result.replace(placeholder, str(value))
|
||||
|
||||
return (result,)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Node registration mappings
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"VariableTextProcessor": VariableTextProcessor,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"VariableTextProcessor": "Variable Text Processor (masslevel)",
|
||||
}
|
||||
@@ -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