New version

This commit is contained in:
mcDandy
2026-07-02 14:58:00 +02:00
parent cb136e6818
commit 885896aab6
10 changed files with 559 additions and 1261 deletions
+8 -2
View File
@@ -223,7 +223,7 @@ You can also get the node from comfy manager under the name of More math.
- supports up to `3` coordinate inputs,
- uses sampling coordinates derived from the provided coordinate tensors,
- intended for spatial remapping / resampling.
- `get_value(tensor, position)`: read value at an N-D position using flat offset math.
- `get_value(tensor, position)`: read value at an N-D position using flat offset math. Superseeded by tensor[position] indexing.
---
@@ -233,7 +233,7 @@ You can also get the node from comfy manager under the name of More math.
- `blur(x, sigma)` / `gaussian`: Gaussian blur using separable convolution.
- `edge(x, [kernel_size])`: Sobel-style edge detection.
- `ezconvolution(tensor, ...)` / `ezconv`: convolution with auto layout handling.
- `convolution(tensor, ...)` / `conv`: direct convolution.
- `convolution(tensor, ...)` / `conv`: direct convolution. Expects [batch, channel, spatial...] layout.
#### 3.2 Mask Morphology
- `dilate(x, [kernel_size])`: dilation.
@@ -356,6 +356,12 @@ You can also get the node from comfy manager under the name of More math.
- `int(x)`: convert to int32 or nested int values.
- `float(x)`: convert to float or nested float values.
___
### Generators
- `text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])` - renders text to an 2D tensor
---
### 11) State / Stack
+1 -1
View File
@@ -25,7 +25,7 @@ def _parse_strengths(Expression, stack):
return (list(result), stack)
class BatchLoraHooksNode(io.ComfyNode):
"""Scale existing bypass LoRA injections on a model/clip pair."""
"""Scale existing bypass LoRA injections on a model/clip pair. Works with models/loaders/Load lora (Bypass) (For Debugging) or more_math batch lora node."""
@staticmethod
def _get_bypass_hooks(injections):
+6 -4
View File
@@ -1,3 +1,5 @@
from email.mime import base
from comfy_api.latest import io
import folder_paths
from .Stack import MrmthStack
@@ -15,9 +17,9 @@ class BatchLoraNode(io.ComfyNode):
"""
@staticmethod
def _parse_strengths(Expression, stack,lora_list,lora_count):
def _parse_strengths(Expression, stack,lora_list,lora_count,base_model):
tree = None
variables = {"lora_names": lora_list, "lora_count": lora_count}
variables = {"lora_names": lora_list, "lora_count": lora_count,"base_model": base_model}
if isinstance(Expression,str):
tree = parse_expr(Expression)
else:
@@ -70,8 +72,8 @@ class BatchLoraNode(io.ComfyNode):
or os.path.normpath(lora).startswith(selected_folder + os.sep)
]
model_strengths, stack = cls._parse_strengths(model_strength, stack, lora_path, len(lora_path))
clip_strengths, stack = cls._parse_strengths(clip_strength, stack, lora_path, len(lora_path))
model_strengths, stack = cls._parse_strengths(model_strength, stack, lora_path, len(lora_path), add_no_lora)
clip_strengths, stack = cls._parse_strengths(clip_strength, stack, lora_path, len(lora_path), add_no_lora)
print(stack)
model_lora = [model] if add_no_lora else []
clip_lora = [clip] if add_no_lora else []
File diff suppressed because it is too large Load Diff
+3 -12
View File
@@ -1,8 +1,5 @@
from re import S
import time
import numpy as np
import os
from numpy._core.multiarray import scalar
import torch
import math
import inspect
@@ -18,6 +15,9 @@ from ..helper_functions import generate_dim_variables
from .noise_utils import NoiseUtils
import struct
from PIL import Image, ImageDraw, ImageFont
import numpy as np
class ReturnSignal:
__slots__ = ("value",)
@@ -2051,14 +2051,6 @@ class UnifiedMathVisitor(MathExprVisitor):
Renders text to a normalised float32 2D tensor [H, W] using Pillow.
"""
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
raise ImportError(
f"{ctx.start.line}:{ctx.start.column}: "
"text_image() requires the 'Pillow' package. "
"Install it with: pip install Pillow"
)
# --- Evaluate arguments ---
def _to_str(v):
@@ -2461,7 +2453,6 @@ class UnifiedMathVisitor(MathExprVisitor):
img = img.rotate(angle, expand=True, fillcolor=0)
# --- Convert to tensor on self.device ---
import numpy as np
arr = np.array(img, dtype=np.float32) / 255.0
return torch.from_numpy(arr).to(device=self.device)
+5 -5
View File
@@ -74,8 +74,8 @@ INBUILT_FUNCTION_META = {
'elastic': {'min_args': 3, 'max_args': 3, 'snippet': 'elastic()', 'description': 'elastic_ease(a, b, t) - elastic ease between a and b'},
'elastic_ease': {'min_args': 3, 'max_args': 3, 'snippet': 'elastic_ease()', 'description': 'elastic_ease(a, b, t) - elastic ease between a and b'},
'entropy': {'min_args': 1, 'max_args': 1, 'snippet': 'entropy()', 'description': 'entropy(x) - computes shanon entropy'},
'erf': {'min_args': 1, 'max_args': 1, 'snippet': 'erf()', 'description': 'erf(x) - computes the error function -- TODO: MORE DESCRIPTIVE --'},
'erfinv': {'min_args': 1, 'max_args': 1, 'snippet': 'erfinv()', 'description': 'erfinv(x) - computes the inverse error function -- TODO: MORE DESCRIPTIVE --'},
'erf': {'min_args': 1, 'max_args': 1, 'snippet': 'erf()', 'description': 'erf(x) - computes the error function (torch.erf)'},
'erfinv': {'min_args': 1, 'max_args': 1, 'snippet': 'erfinv()', 'description': 'erfinv(x) - computes the inverse error function (torch.erfinv)'},
'erode': {'min_args': 1, 'max_args': 2, 'snippet': 'erode()', 'description': 'erode(x, [kernel]) - applies erosion to x. kernel is size of used matrix.'},
'exp': {'min_args': 1, 'max_args': 1, 'snippet': 'exp()', 'description': 'exp(x) - applies e^x to value (per element)'},
'ezconv': {'min_args': 2, 'max_args': None, 'snippet': 'ezconv()', 'description': 'ezconvolution(tensor, [kernel_sizes...], kernel) - applies convolution with kernel. Attempts to correctly convolve over size dimensions.'},
@@ -91,7 +91,7 @@ INBUILT_FUNCTION_META = {
'flow_apply': {'min_args': 2, 'max_args': 2, 'snippet': 'flow_apply()', 'description': 'flow_apply(image, flow) - applies optical flow to an image'},
'flow_mag': {'min_args': 1, 'max_args': 1, 'snippet': 'flow_mag()', 'description': 'flow_magnitude(x) - computes the magnitude of optical flow (or any other structure with last dimension of 2)'},
'flow_magnitude': {'min_args': 1, 'max_args': 1, 'snippet': 'flow_magnitude()', 'description': 'flow_magnitude(x) - computes the magnitude of optical flow (or any other structure with last dimension of 2)'},
'flow_to_image': {'min_args': 1, 'max_args': 1, 'snippet': 'flow_to_image()', 'description': 'flow_to_image(x) - converts optical flow x to an RGB image. Up-Down is <COLOR1> Left-right <COLOR2>'},
'flow_to_image': {'min_args': 1, 'max_args': 1, 'snippet': 'flow_to_image()', 'description': 'flow_to_image(x) - converts optical flow x to an RGB image. Up-Down is blue-yellow Left-right green-magenta.'},
'fract': {'min_args': 1, 'max_args': 1, 'snippet': 'fract()', 'description': 'fract(x) - returns the fractional part of x: x - floor(x)'},
'gamma': {'min_args': 1, 'max_args': 1, 'snippet': 'gamma()', 'description': 'gamma(x) - computes the gamma function (per element)'},
'gaussian': {'min_args': 2, 'max_args': 3, 'snippet': 'gaussian()', 'description': 'gaussian(x, sigma, [reshape]) - applies a Gaussian blur to x with specified sigma. if reshape has value of 1.0, then it tries orienting the input such that channel is in the direction of filter. Otherwise it expect color dimension to be the last.'},
@@ -215,7 +215,7 @@ INBUILT_FUNCTION_META = {
'sqrt': {'min_args': 1, 'max_args': 1, 'snippet': 'sqrt()', 'description': 'sqrt(x) - applies sqere root per element. Negative numbers return NaN (not a number)'},
'stack_clear': {'min_args': 1, 'max_args': 1, 'snippet': 'stack_clear()', 'description': 'stack_clear(slot) - clears the contents of slot stack'},
'stack_get': {'min_args': 1, 'max_args': 1, 'snippet': 'stack_get()', 'description': 'stack_get(slot) - returns the last pushed value in slot without popping it'},
'stack_has': {'min_args': 1, 'max_args': 1, 'snippet': 'stack_has()', 'description': 'stack_has(slot) - returns 1.0 if slot exists in state storage and is not empty else returns 0.0'},
'stack_has': {'min_args': 1, 'max_args': 1, 'snippet': 'stack_has()', 'description': 'stack_has(slot) - returns 1.0 if slot exists in stack and is not empty else returns 0.0'},
'stack_pop': {'min_args': 1, 'max_args': 1, 'snippet': 'stack_pop()', 'description': 'stack_pop(slot) - removes and returns the last element of slot stack'},
'stack_push': {'min_args': 2, 'max_args': 2, 'snippet': 'stack_push()', 'description': 'stack_push(slot, val) - pushes val onto the end of slot stack'},
'std': {'min_args': 1, 'max_args': 1, 'snippet': 'std()', 'description': 'std(x) - computes the standard deviation of elements of x'},
@@ -237,7 +237,7 @@ INBUILT_FUNCTION_META = {
'topk_indices': {'min_args': 2, 'max_args': 2, 'snippet': 'topk_indices()', 'description': 'topk_indices(x, k) - returns the indices of the k largest elements of x'},
'trim': {'min_args': 1, 'max_args': 1, 'snippet': 'trim()', 'description': 'trim(x) - removes leading and trailing whitespace from string'},
'turbulence': {'min_args': 2, 'max_args': 5, 'snippet': 'turbulence()', 'description': 'plasma_noise(seed, scale, [octaves], [offset], [shape]) - generates Plasma/Turbulence noise - same as perlin but default octaves 4'},
'unique': {'min_args': 1, 'max_args': 1, 'snippet': 'unique()', 'description': 'unique(x) - returns the unique elements (sorted,) --TODO: FINISH --'},
'unique': {'min_args': 1, 'max_args': 1, 'snippet': 'unique()', 'description': 'unique(x) - returns the unique elements (sorted, flatened)'},
'upper': {'min_args': 1, 'max_args': 1, 'snippet': 'upper()', 'description': 'upper(x) - converts string to uppercase'},
'var': {'min_args': 1, 'max_args': 1, 'snippet': 'var()', 'description': 'var(x) - computes the variance of elements of x'},
'voronoi': {'min_args': 2, 'max_args': 5, 'snippet': 'voronoi()', 'description': 'cellular_noise(seed, scale, [jitter], [offset], [shape]) - generates Cellular/Voronoi noise'},
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "more_math"
version = "1.0.1"
version = "1.1.0"
tooltip = "Adds math nodes for numbers and types which do not need it."
authors = [
{name = "Daniel Martinek", email = "danda.martinek@gmail.com"}
+5 -5
View File
@@ -77,8 +77,8 @@ export const FUNCTION_META = {
elastic: { minArgs: 3, maxArgs: 3, snippet: "elastic()", description: "elastic_ease(a, b, t) - elastic ease between a and b" },
elastic_ease: { minArgs: 3, maxArgs: 3, snippet: "elastic_ease()", description: "elastic_ease(a, b, t) - elastic ease between a and b" },
entropy: { minArgs: 1, maxArgs: 1, snippet: "entropy()", description: "entropy(x) - computes shanon entropy" },
erf: { minArgs: 1, maxArgs: 1, snippet: "erf()", description: "erf(x) - computes the error function -- TODO: MORE DESCRIPTIVE --" },
erfinv: { minArgs: 1, maxArgs: 1, snippet: "erfinv()", description: "erfinv(x) - computes the inverse error function -- TODO: MORE DESCRIPTIVE --" },
erf: { minArgs: 1, maxArgs: 1, snippet: "erf()", description: "erf(x) - computes the error function (torch.erf)" },
erfinv: { minArgs: 1, maxArgs: 1, snippet: "erfinv()", description: "erfinv(x) - computes the inverse error function (torch.erfinv)" },
erode: { minArgs: 1, maxArgs: 2, snippet: "erode()", description: "erode(x, [kernel]) - applies erosion to x. kernel is size of used matrix." },
exp: { minArgs: 1, maxArgs: 1, snippet: "exp()", description: "exp(x) - applies e^x to value (per element)" },
ezconv: { minArgs: 2, maxArgs: null, snippet: "ezconv()", description: "ezconvolution(tensor, [kernel_sizes...], kernel) - applies convolution with kernel. Attempts to correctly convolve over size dimensions." },
@@ -94,7 +94,7 @@ export const FUNCTION_META = {
flow_apply: { minArgs: 2, maxArgs: 2, snippet: "flow_apply()", description: "flow_apply(image, flow) - applies optical flow to an image" },
flow_mag: { minArgs: 1, maxArgs: 1, snippet: "flow_mag()", description: "flow_magnitude(x) - computes the magnitude of optical flow (or any other structure with last dimension of 2)" },
flow_magnitude: { minArgs: 1, maxArgs: 1, snippet: "flow_magnitude()", description: "flow_magnitude(x) - computes the magnitude of optical flow (or any other structure with last dimension of 2)" },
flow_to_image: { minArgs: 1, maxArgs: 1, snippet: "flow_to_image()", description: "flow_to_image(x) - converts optical flow x to an RGB image. Up-Down is <COLOR1> Left-right <COLOR2>" },
flow_to_image: { minArgs: 1, maxArgs: 1, snippet: "flow_to_image()", description: "flow_to_image(x) - converts optical flow x to an RGB image. Up-Down is blue-yellow Left-right green-magenta." },
fract: { minArgs: 1, maxArgs: 1, snippet: "fract()", description: "fract(x) - returns the fractional part of x: x - floor(x)" },
gamma: { minArgs: 1, maxArgs: 1, snippet: "gamma()", description: "gamma(x) - computes the gamma function (per element)" },
gaussian: { minArgs: 2, maxArgs: 3, snippet: "gaussian()", description: "gaussian(x, sigma, [reshape]) - applies a Gaussian blur to x with specified sigma. if reshape has value of 1.0, then it tries orienting the input such that channel is in the direction of filter. Otherwise it expect color dimension to be the last." },
@@ -218,7 +218,7 @@ export const FUNCTION_META = {
sqrt: { minArgs: 1, maxArgs: 1, snippet: "sqrt()", description: "sqrt(x) - applies sqere root per element. Negative numbers return NaN (not a number)" },
stack_clear: { minArgs: 1, maxArgs: 1, snippet: "stack_clear()", description: "stack_clear(slot) - clears the contents of slot stack" },
stack_get: { minArgs: 1, maxArgs: 1, snippet: "stack_get()", description: "stack_get(slot) - returns the last pushed value in slot without popping it" },
stack_has: { minArgs: 1, maxArgs: 1, snippet: "stack_has()", description: "stack_has(slot) - returns 1.0 if slot exists in state storage and is not empty else returns 0.0" },
stack_has: { minArgs: 1, maxArgs: 1, snippet: "stack_has()", description: "stack_has(slot) - returns 1.0 if slot exists in stack and is not empty else returns 0.0" },
stack_pop: { minArgs: 1, maxArgs: 1, snippet: "stack_pop()", description: "stack_pop(slot) - removes and returns the last element of slot stack" },
stack_push: { minArgs: 2, maxArgs: 2, snippet: "stack_push()", description: "stack_push(slot, val) - pushes val onto the end of slot stack" },
std: { minArgs: 1, maxArgs: 1, snippet: "std()", description: "std(x) - computes the standard deviation of elements of x" },
@@ -240,7 +240,7 @@ export const FUNCTION_META = {
topk_indices: { minArgs: 2, maxArgs: 2, snippet: "topk_indices()", description: "topk_indices(x, k) - returns the indices of the k largest elements of x" },
trim: { minArgs: 1, maxArgs: 1, snippet: "trim()", description: "trim(x) - removes leading and trailing whitespace from string" },
turbulence: { minArgs: 2, maxArgs: 5, snippet: "turbulence()", description: "plasma_noise(seed, scale, [octaves], [offset], [shape]) - generates Plasma/Turbulence noise - same as perlin but default octaves 4" },
unique: { minArgs: 1, maxArgs: 1, snippet: "unique()", description: "unique(x) - returns the unique elements (sorted,) --TODO: FINISH --" },
unique: { minArgs: 1, maxArgs: 1, snippet: "unique()", description: "unique(x) - returns the unique elements (sorted, flatened)" },
upper: { minArgs: 1, maxArgs: 1, snippet: "upper()", description: "upper(x) - converts string to uppercase" },
var: { minArgs: 1, maxArgs: 1, snippet: "var()", description: "var(x) - computes the variance of elements of x" },
voronoi: { minArgs: 2, maxArgs: 5, snippet: "voronoi()", description: "cellular_noise(seed, scale, [jitter], [offset], [shape]) - generates Cellular/Voronoi noise" },
-514
View File
@@ -1,514 +0,0 @@
import { app } from "/scripts/app.js";
const NODE_IDS = new Set(["mrmth_ScriptInput"]);
const NODE_NAMES = new Set(["ScriptTextInput"]);
const STYLE_ID = "mrmth-script-line-numbers";
const KEYWORDS = new Set(["if", "else", "while", "for", "in", "break", "continue", "return", "none", "None", "null", "NULL"]);
const CONSTANTS = new Set(["pi", "PI", "e", "E"]);
const FUNCTIONS = new Set([
"sin", "cos", "tan", "asin", "acos", "atan", "atan2", "sinh", "cosh", "tanh", "asinh", "acosh", "atanh",
"abs", "sqrt", "ln", "log", "exp", "smin", "smax", "tmin", "tmax", "tnorm", "snorm", "floor", "ceil",
"round", "gamma", "pow", "sigm", "clamp", "fft", "ifft", "angle", "print", "print_shape", "pshp", "nvl",
"nan_to_num", "lerp", "step", "smoothstep", "fract", "relu", "softplus", "gelu", "sign", "map", "ezconvolution",
"ezconv", "convolution", "conv", "swap", "permute", "perm", "reshape", "rshp", "range", "topk", "botk", "pinv",
"sum", "mean", "std", "var", "quartile", "quartil", "percentile", "prcnt", "quantile", "dot", "moment","erf","erfinv", "any",
"all", "edge", "blur", "gaussian", "median", "mode", "cumsum", "cumprod", "topk_ind", "topk_indices", "botk_ind",
"botk_indices", "cubic_ease", "cubic", "elastic_ease", "elastic", "sine_ease", "sine", "smootherstep", "dist",
"distance", "remap", "cossim", "cosine_similarity", "count", "cnt", "length", "flatten", "append", "get_value",
"flow_apply", "batch_shuffle", "shuffle", "motion_mask", "flow_to_image", "overlay", "pad", "cross", "matmul", "rife",
"bnot", "bitwise_not", "bitcount", "popcount", "popcnt", "shape", "band", "bitwise_and", "bxor", "bitwise_xor",
"bor", "bitwise_or", "tensor", "stack_push", "stack_pop", "stack_clear", "stack_has", "stack_get", "timestamp","now",
"sort", "argsort", "argmin", "argmax", "softmax", "softmin", "unique", "flip", "cov", "corr", "correlation", "entropy",
"crop", "cat", "concatenate", "concat", "float","int", "linspace", "logspace", "roll","select",
"noise", "randn", "random_normal", "rand", "randu", "random_uniform", "randc", "random_cauchy", "rande",
"random_exponential", "randln", "random_log_normal", "randb", "random_bernoulli", "randp", "random_poisson", "randg",
"random_gamma", "randbeta", "random_beta", "randl", "random_laplace", "randgumbel", "random_gumbel", "randw",
"random_weibull", "randchi2", "random_chi2", "randt", "random_studentt", "perlin", "perlin_noise", "cellular", "voronoi",
"worley", "cellular_noise", "voronoi_noise", "plasma", "turbulence", "plasma_noise", "ridged", "ridged_noise",
"domain_warp", "domain_warp_noise",
"upper", "lower", "split", "join", "substring", "substr", "find", "trim", "replace",
"dilate", "erode", "morph_open", "morph_close",
"rgb_to_hsv", "hsv_to_rgb",
"rgb_to_oklab", "oklab_to_rgb", "rgb_to_cielab", "cielab_to_rgb",
"int_to_rgb", "rgb_to_int",
"where", "histogram", "hist", "flow_mag", "flow_magnitude", "flow_ang", "flow_angle",
"interpolate_linear", "interpolate_area", "interpolate_nearest","interpolate_nearest_exact"]);
const BRACKET_PAIRS = {
'(': ')',
'[': ']',
'{': '}'
};
const OPENING_BRACKETS = new Set(['(', '[', '{']);
const CLOSING_BRACKETS = new Set([')', ']', '}']);
function escapeHtml(value) {
return value.replace(/[&<>"']/g, (ch) => {
switch (ch) {
case "&":
return "&amp;";
case "<":
return "&lt;";
case ">":
return "&gt;";
case '"':
return "&quot;";
case "'":
return "&#39;";
default:
return ch;
}
});
}
function tokenize(text) {
const tokens = [];
const pattern = /#.*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\\r\n])*"|'(?:\\.|[^'\\\r\n])*'|\b\d+(?:\.\d*)?(?:[eE][+-]?\d+)?\b|\B\.\d+(?:[eE][+-]?\d+)?\b|==|!=|>=|<=|<<|>>|->|\+=|-=|\*=|\/=|%=|[+\-*/%^=<>|?:,;()\[\]{}]|\b[a-zA-Z_][a-zA-Z_0-9]*\b|\s+|./g;
let match;
const bracketStack = [];
const depthByType = { '(': 0, '[': 0, '{': 0 };
while ((match = pattern.exec(text)) !== null) {
const value = match[0];
let type = "text";
let depth = 0;
if (value.startsWith("#") || value.startsWith("/*")) {
type = "comment";
} else if (value.startsWith('"') || value.startsWith("'")) {
type = "string";
} else if (OPENING_BRACKETS.has(value)) {
depth = depthByType[value];
depthByType[value]++;
bracketStack.push(value);
type = "bracket";
} else if (CLOSING_BRACKETS.has(value)) {
if (bracketStack.length > 0) {
const lastOpen = bracketStack[bracketStack.length - 1];
if (BRACKET_PAIRS[lastOpen] === value) {
// Use depth of bracket type being closed, before decrement
depth = depthByType[lastOpen] - 1;
depthByType[lastOpen]--;
bracketStack.pop();
} else {
// Mismatched closing bracket
depth = 0;
}
} else {
// Unmatched closing bracket
depth = 0;
}
type = "bracket";
} else if (/^[+\-*/%^=<>|?:,;]|==|!=|>=|<=|<<|>>|->$/.test(value)) {
type = "operator";
} else if (/^(?:\d+\.\d*|\d+|\.\d+)(?:[eE][+-]?\d+)?$/.test(value)) {
type = "number";
} else if (/^[a-zA-Z_]/.test(value)) {
if (KEYWORDS.has(value)) {
type = "keyword";
} else if (CONSTANTS.has(value)) {
type = "constant";
} else if (FUNCTIONS.has(value)) {
type = "function";
} else {
type = "variable";
}
}
tokens.push({ type, value, depth });
}
return tokens;
}
function renderHighlight(text) {
const tokens = tokenize(text);
return tokens
.map((token) => {
if (token.type === "text") {
return escapeHtml(token.value);
}
if (token.type === "bracket") {
const depthClass = token.depth % 6;
return `<span class="mrmth-token-bracket mrmth-bracket-${depthClass}">${escapeHtml(token.value)}</span>`;
}
return `<span class="mrmth-token-${token.type}">${escapeHtml(token.value)}</span>`;
})
.join("");
}
function hasScriptInput(nodeData) {
const input = nodeData?.input || {};
const required = input.required || {};
const optional = input.optional || {};
return Object.prototype.hasOwnProperty.call(required, "script") || Object.prototype.hasOwnProperty.call(optional, "script");
}
function isTargetNode(nodeData) {
if (!nodeData) {
return false;
}
if (NODE_IDS.has(nodeData.node_id)) {
return true;
}
if (NODE_IDS.has(nodeData.name)) {
return true;
}
if (NODE_NAMES.has(nodeData.name)) {
return true;
}
if (NODE_NAMES.has(nodeData.display_name)) {
return true;
}
return hasScriptInput(nodeData);
}
function ensureStyles() {
if (document.getElementById(STYLE_ID)) {
return;
}
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
.mrmth-line-editor {
display: flex;
align-items: stretch;
width: 100%;
height: 100%;
min-height: 100%;
gap: 0;
overflow: hidden;
}
.mrmth-line-numbers {
padding: 6px 8px;
text-align: right;
user-select: none;
color: #9aa0a6;
background: rgba(0, 0, 0, 0.4);
border-right: 1px solid rgba(255, 255, 255, 0.1);
font-family: monospace;
font-size: 12px;
line-height: 1.4;
white-space: pre;
overflow: hidden;
box-sizing: border-box;
}
.mrmth-line-numbers-content {
white-space: pre;
}
.mrmth-editor-container {
position: relative;
flex: 1;
min-height: 30px;
overflow: hidden;
}
.mrmth-line-input {
position: relative;
z-index: 2;
flex: 1;
width: 100%;
height: 100%;
box-sizing: border-box;
font-family: monospace;
line-height: 1.4;
white-space: pre-wrap;
word-wrap: break-word;
resize: none;
background: transparent;
color: transparent;
}
.mrmth-syntax-layer {
position: absolute;
inset: 0;
margin: 0;
z-index: 1;
pointer-events: none;
white-space: pre-wrap;
word-wrap: break-word;
overflow: hidden;
mix-blend-mode: normal;
opacity: 1;
}
.mrmth-token-comment { color: #7f8c8d; }
.mrmth-token-number { color: #f39c12; }
.mrmth-token-constant { color: #f1c40f; }
.mrmth-token-keyword { color: #e74c3c; }
.mrmth-token-function { color: #8e44ad; }
.mrmth-token-variable { color: #ecf0f1; }
.mrmth-token-operator { color: #95a5a6; }
.mrmth-token-string { color: #4ea658; }
.mrmth-token-bracket { font-weight: bold; }
.mrmth-bracket-0 { color: #ffd700; }
.mrmth-bracket-1 { color: #da70d6; }
.mrmth-bracket-2 { color: #87cefa; }
.mrmth-bracket-3 { color: #98fb98; }
.mrmth-bracket-4 { color: #ff6347; }
.mrmth-bracket-5 { color: #ffa500; }
`;
document.head.appendChild(style);
}
function measureLineHeight(inputEl) {
const probe = document.createElement("span");
const style = getComputedStyle(inputEl);
probe.style.position = "absolute";
probe.style.visibility = "hidden";
probe.style.whiteSpace = "pre";
probe.style.fontFamily = style.fontFamily;
probe.style.fontSize = style.fontSize;
probe.style.fontWeight = style.fontWeight;
probe.style.fontStyle = style.fontStyle;
probe.style.lineHeight = style.lineHeight;
probe.textContent = "A";
document.body.appendChild(probe);
const height = probe.getBoundingClientRect().height;
document.body.removeChild(probe);
return height;
}
function attachLineNumbers(widget) {
const inputEl = widget?.inputEl;
if (!inputEl || inputEl.dataset.mrmthLineNumbers) {
return false;
}
const parent = inputEl.parentElement;
if (!parent) {
return false;
}
ensureStyles();
const wrapper = document.createElement("div");
wrapper.className = "mrmth-line-editor";
const gutter = document.createElement("div");
gutter.className = "mrmth-line-numbers";
const gutterContent = document.createElement("div");
gutterContent.className = "mrmth-line-numbers-content";
const editorContainer = document.createElement("div");
editorContainer.className = "mrmth-editor-container";
const syntaxLayer = document.createElement("pre");
syntaxLayer.className = "mrmth-syntax-layer";
parent.replaceChild(wrapper, inputEl);
wrapper.appendChild(gutter);
gutter.appendChild(gutterContent);
wrapper.appendChild(editorContainer);
editorContainer.appendChild(syntaxLayer);
editorContainer.appendChild(inputEl);
wrapper.style.height = "100%";
parent.style.height = "100%";
inputEl.classList.add("mrmth-line-input");
inputEl.dataset.mrmthLineNumbers = "true";
const inputStyle = getComputedStyle(inputEl);
const lineHeightPx = measureLineHeight(inputEl);
const applyAlpha = (color, alpha) => {
const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i);
if (!rgbaMatch) {
return `rgba(0, 0, 0, ${alpha})`;
}
const r = Number(rgbaMatch[1]);
const g = Number(rgbaMatch[2]);
const b = Number(rgbaMatch[3]);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
const baseBackground = inputStyle.backgroundColor || "rgba(0, 0, 0, 0.4)";
const overlayBackground = applyAlpha(baseBackground, 0.4);
syntaxLayer.style.height = "100%";
syntaxLayer.style.background = "transparent";
inputEl.style.height = "100%";
inputEl.style.background = overlayBackground;
gutter.style.fontFamily = inputStyle.fontFamily;
gutter.style.fontSize = inputStyle.fontSize;
gutter.style.lineHeight = inputStyle.lineHeight;
gutter.style.paddingTop = inputStyle.paddingTop;
gutter.style.paddingBottom = inputStyle.paddingBottom;
syntaxLayer.style.fontFamily = inputStyle.fontFamily;
syntaxLayer.style.fontSize = inputStyle.fontSize;
syntaxLayer.style.lineHeight = inputStyle.lineHeight;
syntaxLayer.style.padding = `${inputStyle.paddingTop} ${inputStyle.paddingRight} ${inputStyle.paddingBottom} ${inputStyle.paddingLeft}`;
syntaxLayer.style.width = "100%";
syntaxLayer.style.boxSizing = "border-box";
syntaxLayer.style.overflowY = "hidden";
inputEl.style.background = overlayBackground;
inputEl.style.color = "transparent";
inputEl.style.caretColor = "#ffffff";
inputEl.style.mixBlendMode = "lighten";
const updateNumbers = () => {
const rawLines = inputEl.value.split("\n");
const textLineCount = Math.max(1, rawLines.length);
const gutterLines = [];
// Sync syntaxLayer width to match inputEl's actual content width
const scrollbarWidth = inputEl.offsetWidth - inputEl.clientWidth;
syntaxLayer.style.width = `calc(100% - ${scrollbarWidth}px)`;
// Create a temporary clone
const measureClone = syntaxLayer.cloneNode(false);
measureClone.style.position = "absolute";
measureClone.style.visibility = "hidden";
measureClone.style.width = syntaxLayer.style.width;
measureClone.style.height = "auto";
syntaxLayer.parentElement.appendChild(measureClone);
// Build content with simple marker at start of each line
let html = "";
for (let i = 0; i < textLineCount; i++) {
html += `<span class="lm" data-ln="${i}"></span>`;
html += renderHighlight(rawLines[i] || " ");
if (i < textLineCount - 1) {
html += "\n";
}
}
measureClone.innerHTML = html;
// Simply read offsetTop of each marker
const totalHeight = measureClone.offsetHeight;
for (let i = 0; i < textLineCount; i++) {
const marker = measureClone.querySelector(`[data-ln="${i}"]`);
const nextMarker = i < textLineCount - 1 ? measureClone.querySelector(`[data-ln="${i + 1}"]`) : null;
const currentY = marker ? marker.offsetTop : i * lineHeightPx;
const nextY = nextMarker ? nextMarker.offsetTop : totalHeight;
const heightDiff = nextY - currentY;
const visualLines = Math.max(1, Math.round(heightDiff / lineHeightPx));
gutterLines.push(String(i + 1));
for (let j = 1; j < visualLines; j++) {
gutterLines.push(" ");
}
}
measureClone.remove();
gutterContent.textContent = gutterLines.join("\n");
gutter.style.height = `${inputEl.clientHeight}px`;
gutterContent.style.transform = `translateY(${-inputEl.scrollTop}px)`;
};
const updateHighlight = () => {
const sourceText = inputEl.value.endsWith("\n") ? `${inputEl.value} ` : inputEl.value;
const highlighted = renderHighlight(sourceText);
syntaxLayer.innerHTML = highlighted;
syntaxLayer.scrollTop = inputEl.scrollTop;
syntaxLayer.scrollLeft = inputEl.scrollLeft;
};
const handleBracketAutoClose = (e) => {
const key = e.key;
if (!OPENING_BRACKETS.has(key)) {
return;
}
const selectionStart = inputEl.selectionStart;
const selectionEnd = inputEl.selectionEnd;
if (selectionStart !== selectionEnd) {
return;
}
const text = inputEl.value;
const charAfter = text[selectionStart] || '';
// Only prevent auto-close if the MATCHING closing bracket is after cursor
const closingBracket = BRACKET_PAIRS[key];
const isMatchingClosingBracket = charAfter === closingBracket;
// Prevent auto-close if next char is alphanumeric or a matching closing bracket
if (/[a-zA-Z0-9_]/.test(charAfter) || isMatchingClosingBracket) {
return;
}
e.preventDefault();
const before = text.substring(0, selectionStart);
const after = text.substring(selectionStart);
inputEl.value = before + key + closingBracket + after;
inputEl.selectionStart = inputEl.selectionEnd = selectionStart + 1;
updateNumbers();
updateHighlight();
const event = new Event('input', { bubbles: true });
inputEl.dispatchEvent(event);
};
inputEl.addEventListener("keydown", handleBracketAutoClose);
inputEl.addEventListener("input", () => {
updateNumbers();
updateHighlight();
});
inputEl.addEventListener("scroll", () => {
gutterContent.style.transform = `translateY(${-inputEl.scrollTop}px)`;
syntaxLayer.scrollTop = inputEl.scrollTop;
syntaxLayer.scrollLeft = inputEl.scrollLeft;
});
if (window.ResizeObserver) {
const resizeObserver = new ResizeObserver(() => {
updateNumbers();
updateHighlight();
});
resizeObserver.observe(inputEl);
}
requestAnimationFrame(() => {
updateNumbers();
updateHighlight();
});
return true;
}
function attachLineNumbersWithRetry(node) {
const widget = node.widgets?.find((w) => w.name === "script");
if (attachLineNumbers(widget)) {
return;
}
requestAnimationFrame(() => {
const retryWidget = node.widgets?.find((w) => w.name === "script");
attachLineNumbers(retryWidget);
});
}
app.registerExtension({
name: "mrmth.ScriptTextInput.LineNumbers",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (!isTargetNode(nodeData)) {
return;
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated?.apply(this, arguments);
attachLineNumbersWithRetry(this);
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments);
attachLineNumbersWithRetry(this);
};
},
});
-187
View File
@@ -1,187 +0,0 @@
import { app } from "/scripts/app.js";
const STYLE_ID = "mrmth-script-line-numbers-v2";
const BRACKET_PAIRS = { "(": ")", "[": "]", "{": "}" };
const OPENING_BRACKETS = new Set(["(", "[", "{"]);
function ensureStyles() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
.mrmth-v2-editor {
display: flex;
width: 100%;
min-height: 160px;
border: 1px solid rgba(255,255,255,0.12);
border-radius: 8px;
overflow: hidden;
}
.mrmth-v2-gutter {
padding: 8px 10px;
min-width: 40px;
text-align: right;
user-select: none;
color: #9aa0a6;
background: rgba(0,0,0,0.35);
border-right: 1px solid rgba(255,255,255,0.12);
font-family: monospace;
font-size: 12px;
line-height: 1.45;
white-space: pre;
box-sizing: border-box;
}
.mrmth-v2-textarea {
flex: 1;
min-height: 160px !important;
border: 0 !important;
outline: none !important;
resize: vertical !important;
font-family: monospace !important;
font-size: 12px !important;
line-height: 1.45 !important;
padding: 8px 10px !important;
box-sizing: border-box !important;
}
`;
document.head.appendChild(style);
}
function hasNearbyScriptLabel(el) {
const container = el.closest("div, section, article");
if (!container) return false;
// text labels around the textarea
const candidates = container.querySelectorAll("label, .label, .input-label, .property_name, span, div");
for (const n of candidates) {
const t = (n.textContent || "").trim().toLowerCase();
if (t === "script") return true;
}
return false;
}
function isLikelyScriptTextarea(el) {
if (!(el instanceof HTMLTextAreaElement)) return false;
if (el.dataset.mrmthV2Script === "1") return false;
const attrs = [
el.name,
el.id,
el.getAttribute("data-name"),
el.getAttribute("aria-label"),
el.getAttribute("placeholder"),
]
.filter(Boolean)
.join(" ")
.toLowerCase();
// accept either direct attribute match OR nearby label
return attrs.includes("script") || hasNearbyScriptLabel(el);
}
function updateGutter(textarea, gutter) {
const lineCount = Math.max(1, textarea.value.split("\n").length);
const lines = [];
for (let i = 1; i <= lineCount; i++) lines.push(String(i));
gutter.textContent = lines.join("\n");
gutter.style.transform = `translateY(${-textarea.scrollTop}px)`;
}
function addBracketAutoClose(textarea) {
textarea.addEventListener("keydown", (e) => {
const key = e.key;
if (!OPENING_BRACKETS.has(key)) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
if (start !== end) return;
const after = textarea.value[start] ?? "";
const closing = BRACKET_PAIRS[key];
if (/[a-zA-Z0-9_]/.test(after) || after === closing) return;
e.preventDefault();
const before = textarea.value.slice(0, start);
const tail = textarea.value.slice(start);
textarea.value = `${before}${key}${closing}${tail}`;
textarea.selectionStart = textarea.selectionEnd = start + 1;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
});
}
function hideNearbyScriptLabels(textarea) {
const root = textarea.closest("div, section, article") ?? textarea.parentElement;
if (!root) return;
const labels = root.querySelectorAll("label, .label, .input-label, .property_name, .v-label, span, div");
for (const el of labels) {
if (el.dataset.mrmthHiddenLabel === "1") continue;
const t = (el.textContent || "").trim().toLowerCase();
if (t === "script") {
el.style.display = "none";
el.dataset.mrmthHiddenLabel = "1";
}
}
}
function attachToTextarea(textarea) {
if (!isLikelyScriptTextarea(textarea)) return;
const parent = textarea.parentElement;
if (!parent) return;
ensureStyles();
hideNearbyScriptLabels(textarea);
const wrapper = document.createElement("div");
wrapper.className = "mrmth-v2-editor";
const gutter = document.createElement("div");
gutter.className = "mrmth-v2-gutter";
parent.replaceChild(wrapper, textarea);
wrapper.appendChild(gutter);
wrapper.appendChild(textarea);
textarea.classList.add("mrmth-v2-textarea");
textarea.dataset.mrmthV2Script = "1";
// disable red spell-check underline for code
textarea.spellcheck = false;
textarea.autocapitalize = "off";
textarea.autocomplete = "off";
textarea.autocorrect = "off";
const refresh = () => updateGutter(textarea, gutter);
textarea.addEventListener("input", refresh);
textarea.addEventListener("scroll", refresh);
addBracketAutoClose(textarea);
refresh();
}
function scan(root = document) {
root.querySelectorAll("textarea").forEach(attachToTextarea);
}
app.registerExtension({
name: "mrmth.ScriptTextInput.V2",
async setup() {
scan(document);
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (!(node instanceof Element)) continue;
if (node.matches?.("textarea")) attachToTextarea(node);
scan(node);
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
},
});