Files
Blueprint Coding 40a1476470 Fixed min/max snapping to be based off min if set not forced to zero
Issue:
If Step was set to 4, you could not set min to 1 to get a range like:
 [1 , 5 , 9 , 13 , 17 ...]

Fix was to set step to move off of min instead of zero as a fixed base.

Small bug but in interest of time was left for now. Min can not be incremented downwards by the down arrow, must be manually typed. (can move up and Max can move up or down)
2025-02-22 10:01:24 -07:00

1628 lines
66 KiB
JavaScript

import { app } from "../../../scripts/app.js";
import { SettingsPopup } from "./AIDocSharedClasses.js";
function calculateLuminance(hexColor) {
hexColor = hexColor.replace(/^#/, '');
const r = parseInt(hexColor.substring(0, 2), 16) / 255;
const g = parseInt(hexColor.substring(2, 4), 16) / 255;
const b = parseInt(hexColor.substring(4, 6), 16) / 255;
const a = [r, g, b].map(function (x) {
return (x <= 0.03928) ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
});
return (a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722);
}
function getTextColorForBackground(bgColor) {
const luminance = calculateLuminance(bgColor);
return luminance < 0.5 ? '#ffffff' : '#000000';
}
function truncateText(ctx, text, maxWidth) {
const ellipsis = "...";
const ellipsisWidth = ctx.measureText(ellipsis).width;
if (ctx.measureText(text).width <= maxWidth) {
return text;
}
let truncated = text;
while (truncated && ctx.measureText(truncated).width + ellipsisWidth > maxWidth) {
truncated = truncated.slice(0, -1);
}
return truncated + ellipsis;
}
class MultiFloatNodeAID {
constructor(node) {
this.node = node;
// Ensure basic properties
if (typeof node.properties.activeCount !== "number") {
node.properties.activeCount = 20;
}
if (!Array.isArray(node.properties.floatSteps)) {
node.properties.floatSteps = [];
}
if (!Array.isArray(node.properties.labels)) {
node.properties.labels = [];
}
if (!Array.isArray(node.properties.values)) {
node.properties.values = [];
}
if (!Array.isArray(node.properties.floatMin)) {
node.properties.floatMin = [];
}
if (!Array.isArray(node.properties.floatMax)) {
node.properties.floatMax = [];
}
if (!Array.isArray(node.properties.floatPrecision)) {
node.properties.floatPrecision = [];
}
this.adjustArrays();
if (typeof node.properties.bottomPadding !== "number") {
node.properties.bottomPadding = 0;
}
if (typeof node.properties.minWidth !== "number") {
node.properties.minWidth = 250;
}
if (typeof node.properties.maxWidth !== "number") {
node.properties.maxWidth = 1000;
}
// Add new boolean property for showing the static label & line
if (typeof node.properties.showStaticLabelAndLine !== "boolean") {
node.properties.showStaticLabelAndLine = true; // default: show
}
//group labels
if (!Array.isArray(node.properties.groupLabels)) {
node.properties.groupLabels = [];
}
this.labelRects = [];
this.valueRects = [];
this.pencilRects = [];
this.incrementRects = [];
this.decrementRects = [];
this.gearRect = null;
this.setupOutputsAndWidgets();
node.setDirtyCanvas(true, true);
node.onDrawForeground = this.onDrawForeground.bind(this);
}
setupOutputsAndWidgets() {
const oldSize = [...this.node.size];
this.node.outputs = [];
this.node.widgets = [];
for (let i = 0; i < this.node.properties.activeCount; i++) {
// Each output is a FLOAT now
this.node.addOutput("", "FLOAT");
const w = this.node.addWidget(
"number",
`value${i+1}`,
this.node.properties.values[i],
(val) => this.onValueChanged(i, val),
{ hidden: true }
);
w.computeSize = () => [0, -4];
w.hidden = true;
}
for (let i = 0; i < this.node.outputs.length; i++) {
this.node.outputs[i].name = `${i+1}`;
}
this.node.size[0] = oldSize[0];
}
onDrawForeground(ctx) {
if (this.node.flags.collapsed) return;
// Ensure outputs & widgets match activeCount
if (this.node.outputs.length !== this.node.properties.activeCount) {
this.node.outputs = [];
this.node.widgets = [];
this.setupOutputsAndWidgets();
if (this.node.onOutputsChange) {
this.node.onOutputsChange();
}
}
const margin = 8;
const rowSpacing = (typeof this.node.properties.rowSpacing === "number") ? this.node.properties.rowSpacing : margin;
const rowHeight = 30;
const count = this.node.properties.activeCount;
const groupLabels = Array.isArray(this.node.properties.groupLabels) ? this.node.properties.groupLabels : [];
const groupLabelHeight = 24;
const groupLabelCount = groupLabels.length;
// Calculate total needed height (floats + group labels + bottom padding)
const neededHeight = margin
+ (count * (rowHeight + rowSpacing))
+ (groupLabelCount * (groupLabelHeight + rowSpacing))
+ (this.node.properties.bottomPadding || 0);
this.node.size[1] = neededHeight;
// Clamp width
if (this.node.size[0] < this.node.properties.minWidth) {
this.node.size[0] = this.node.properties.minWidth;
}
if (this.node.size[0] > this.node.properties.maxWidth) {
this.node.size[0] = this.node.properties.maxWidth;
}
// Draw background
const bgColor = this.node.properties.bgColor || "#181818";
ctx.fillStyle = bgColor;
const radius = 8;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(this.node.size[0], 0);
ctx.lineTo(this.node.size[0], this.node.size[1] - radius);
ctx.arcTo(this.node.size[0], this.node.size[1], this.node.size[0] - radius, this.node.size[1], radius);
ctx.lineTo(radius, this.node.size[1]);
ctx.arcTo(0, this.node.size[1], 0, this.node.size[1] - radius, radius);
ctx.lineTo(0, radius);
ctx.closePath();
ctx.fill();
// Row style
const rowBgWidth = this.node.size[0] - margin - 38;
const rowBgColor = this.node.properties.rowBgColor || "#121212";
const rowTextColor = getTextColorForBackground(rowBgColor) || "#fff";
this.labelRects = [];
this.valueRects = [];
this.pencilRects = [];
this.incrementRects = [];
this.decrementRects = [];
let y = margin;
// Draw rows with group labels
for (let i = 0; i < count; i++) {
// Draw any group labels scheduled before row i
const labelsForThisRow = groupLabels.filter(lbl => lbl.beforeRow === i);
for (const gLabel of labelsForThisRow) {
y = this.drawGroupLabelRow(ctx, gLabel, margin, y, rowBgWidth);
}
// Draw float row background
ctx.fillStyle = rowBgColor;
ctx.beginPath();
ctx.moveTo(margin + radius, y);
ctx.arcTo(margin + rowBgWidth, y, margin + rowBgWidth, y + rowHeight, radius);
ctx.arcTo(margin + rowBgWidth, y + rowHeight, margin, y + rowHeight, radius);
ctx.arcTo(margin, y + rowHeight, margin, y, radius);
ctx.arcTo(margin, y, margin + rowBgWidth, y, radius);
ctx.closePath();
ctx.fill();
// Text style
ctx.fillStyle = rowTextColor;
ctx.font = "10px Arial";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
let staticLabelY = y + rowHeight / 2;
let lineStartX = margin + 2;
// Optional static label & divider line
if (this.node.properties.showStaticLabelAndLine) {
const staticLabel = `·${i + 1}`;
const staticLabelMetrics = ctx.measureText(staticLabel);
const staticLabelX = margin + 2;
ctx.fillText(staticLabel, staticLabelX, staticLabelY);
lineStartX = staticLabelX + staticLabelMetrics.width + 4;
ctx.beginPath();
ctx.moveTo(lineStartX, staticLabelY - rowHeight / 2);
ctx.lineTo(lineStartX, staticLabelY + rowHeight / 2);
ctx.stroke();
}
// Label area
const valX = margin + rowBgWidth - 90;
const valY = staticLabelY;
const labelAreaStart = lineStartX + 6;
const labelAreaEnd = valX - 80;
let availableWidth = labelAreaEnd - labelAreaStart;
if (availableWidth < 0) {
availableWidth = 0;
}
console.log(availableWidth);
const labelWidthRules = [
{ max: 10, set: 20 },
{ max: 15, set: 30 },
{ max: 30, set: 40 },
{ max: 50, set: 60 },
{ max: 70, set: 70 },
{ max: 90, set: 80 },
{ max: 150, multiplier: 0.85 },
{ max: 200, multiplier: 0.82 },
{ max: 400, multiplier: 0.75 },
{ max: 500, multiplier: 0.74 },
{ max: Infinity, multiplier: 0.72 }
];
function adjustLabelWidth(width) {
for (const rule of labelWidthRules) {
if (width <= rule.max) {
if (rule.set !== undefined) {
return rule.set;
}
if (rule.multiplier !== undefined) {
return width * rule.multiplier;
}
}
}
return width;
}
const customLabelRaw = this.node.properties.labels[i] || `INT ${i + 1}`;
const adjustedWidth = adjustLabelWidth(availableWidth);
const displayLabel = truncateText(ctx, customLabelRaw, adjustedWidth);
ctx.fillStyle = rowTextColor;
ctx.font = "14px Arial";
ctx.fillText(displayLabel, labelAreaStart, staticLabelY);
const displayedLabelWidth = ctx.measureText(displayLabel).width;
this.labelRects[i] = {
x: labelAreaStart,
y,
w: displayedLabelWidth + 8,
h: rowHeight
};
// Float value
let val = this.node.properties.values[i] ?? 0.0;
const decimals = this.node.properties.floatPrecision[i] || 1;
val = parseFloat(val.toFixed(decimals));
const valStr = val.toString();
ctx.textAlign = "right";
ctx.font = "14px Arial";
const valMetrics = ctx.measureText(valStr);
ctx.fillText(valStr, valX, valY);
this.valueRects[i] = {
x: valX - valMetrics.width - 4,
y,
w: valMetrics.width + 8,
h: rowHeight
};
// Pencil icon
ctx.textAlign = "left";
ctx.fillStyle = rowTextColor;
ctx.font = "16px Arial";
const pencilIcon = "✎";
const pencilMetrics = ctx.measureText(pencilIcon);
const pencilX = valX + 6;
ctx.fillText(pencilIcon, pencilX, valY);
this.pencilRects[i] = {
x: pencilX,
y,
w: pencilMetrics.width + 4,
h: rowHeight
};
// +/- buttons
const incDecColor = this.node.properties.buttonColor || "#4b3e72";
const incDecTextColor = getTextColorForBackground(incDecColor);
const decrementButtonBoxX = valX + 36;
const decrementButtonBoxY = y + (rowHeight / 4) - 2;
const buttonBoxWidth = 20;
const buttonBoxHeight = 20;
const incrementButtonBoxX = decrementButtonBoxX + 24;
const incrementButtonBoxY = decrementButtonBoxY;
// Decrement
ctx.fillStyle = incDecColor;
ctx.beginPath();
ctx.moveTo(decrementButtonBoxX + 5, decrementButtonBoxY);
ctx.arcTo(decrementButtonBoxX + buttonBoxWidth, decrementButtonBoxY, decrementButtonBoxX + buttonBoxWidth, decrementButtonBoxY + buttonBoxHeight, 5);
ctx.arcTo(decrementButtonBoxX + buttonBoxWidth, decrementButtonBoxY + buttonBoxHeight, decrementButtonBoxX, decrementButtonBoxY + buttonBoxHeight, 5);
ctx.arcTo(decrementButtonBoxX, decrementButtonBoxY + buttonBoxHeight, decrementButtonBoxX, decrementButtonBoxY, 5);
ctx.arcTo(decrementButtonBoxX, decrementButtonBoxY, decrementButtonBoxX + buttonBoxWidth, decrementButtonBoxY, 5);
ctx.closePath();
ctx.fill();
ctx.font = "12px Arial";
ctx.fillStyle = incDecTextColor;
ctx.fillText("-", decrementButtonBoxX + 8, decrementButtonBoxY + buttonBoxHeight / 2);
this.decrementRects[i] = {
x: decrementButtonBoxX,
y: decrementButtonBoxY,
w: buttonBoxWidth,
h: buttonBoxHeight
};
// Increment
ctx.fillStyle = incDecColor;
ctx.beginPath();
ctx.moveTo(incrementButtonBoxX + 5, incrementButtonBoxY);
ctx.arcTo(incrementButtonBoxX + buttonBoxWidth, incrementButtonBoxY, incrementButtonBoxX + buttonBoxWidth, incrementButtonBoxY + buttonBoxHeight, 5);
ctx.arcTo(incrementButtonBoxX + buttonBoxWidth, incrementButtonBoxY + buttonBoxHeight, incrementButtonBoxX, incrementButtonBoxY + buttonBoxHeight, 5);
ctx.arcTo(incrementButtonBoxX, incrementButtonBoxY + buttonBoxHeight, incrementButtonBoxX, incrementButtonBoxY, 5);
ctx.arcTo(incrementButtonBoxX, incrementButtonBoxY, incrementButtonBoxX + buttonBoxWidth, incrementButtonBoxY, 5);
ctx.closePath();
ctx.fill();
ctx.font = "12px Arial";
ctx.fillStyle = incDecTextColor;
ctx.fillText("+", incrementButtonBoxX + 6, incrementButtonBoxY + buttonBoxHeight / 2);
this.incrementRects[i] = {
x: incrementButtonBoxX,
y: incrementButtonBoxY,
w: buttonBoxWidth,
h: buttonBoxHeight
};
y += rowHeight + rowSpacing;
}
// Group labels after the last row
const trailingLabels = groupLabels.filter(lbl => lbl.beforeRow >= count);
for (const gLabel of trailingLabels) {
y = this.drawGroupLabelRow(ctx, gLabel, margin, y, rowBgWidth);
}
// Gear icon (with emoji)
const gearIcon = "⚙️";
ctx.font = "18px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#ccc";
const gearWidth = ctx.measureText(gearIcon).width;
const gearHeight = 18;
const gearX = this.node.size[0] - margin - (gearWidth / 2);
const gearY = this.node.size[1] - margin - (gearHeight / 2);
ctx.fillText(gearIcon, gearX, gearY);
this.gearRect = {
x: gearX - gearWidth / 2,
y: gearY - gearHeight / 2,
w: gearWidth,
h: gearHeight
};
// Bottom padding
const bottomPad = parseFloat(this.node.properties.bottomPadding) || 0;
if (bottomPad > 0) {
ctx.fillStyle = bgColor;
ctx.fillRect(margin + radius, y, rowBgWidth, bottomPad);
y += bottomPad;
}
}
drawGroupLabelRow(ctx, labelObj, margin, startY, rowBgWidth) {
const rowHeight = 24;
const radius = 8;
// Use group label's stored color or a default
const bgColor = labelObj.bgColor || "#242424";
ctx.fillStyle = bgColor;
ctx.beginPath();
ctx.moveTo(margin + radius, startY);
ctx.arcTo(margin + rowBgWidth, startY, margin + rowBgWidth, startY + rowHeight, radius);
ctx.arcTo(margin + rowBgWidth, startY + rowHeight, margin, startY + rowHeight, radius);
ctx.arcTo(margin, startY + rowHeight, margin, startY, radius);
ctx.arcTo(margin, startY, margin + rowBgWidth, startY, radius);
ctx.closePath();
ctx.fill();
// Text color
ctx.fillStyle = getTextColorForBackground(bgColor);
ctx.font = "16px Arial";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
const textX = margin + 10;
const textY = startY + rowHeight / 2;
ctx.fillText(labelObj.text || "Group Label", textX, textY);
const rowSpacing = (typeof this.node.properties.rowSpacing === "number") ? this.node.properties.rowSpacing : 8;
return startY + rowHeight + rowSpacing;
}
clampValue(index, val) {
const minVal = this.node.properties.floatMin[index];
const maxVal = this.node.properties.floatMax[index];
// --- ADDED: apply precision after clamp
// We'll clamp, then we'll do .toFixed(...) at the end
if (typeof minVal === "number" && val < minVal) {
val = minVal;
}
if (typeof maxVal === "number" && val > maxVal) {
val = maxVal;
}
// Final rounding to precision
const decimals = this.node.properties.floatPrecision[index] || 1;
val = parseFloat(val.toFixed(decimals));
return val;
}
startValueDragToggle(index, initialEvent) {
if (this.dragActive) {
this.stopValueDrag();
return;
}
this.dragActive = true;
this.dragIndex = index;
this.dragInitialX = initialEvent.clientX;
this.dragInitialValue = this.node.properties.values[index];
const step = (this.node.properties.floatSteps && this.node.properties.floatSteps[index]) || 0.1;
const sensitivity = 10;
const canvasEl = document.getElementById("graph-canvas");
if (!canvasEl) return;
this._onValuePointerMove = (e) => {
const dx = e.clientX - this.dragInitialX;
const steps = Math.floor(dx / sensitivity);
let newValue = this.dragInitialValue + steps * step;
newValue = this.clampValue(index, newValue);
if (newValue !== this.node.properties.values[this.dragIndex]) {
this.node.properties.values[this.dragIndex] = newValue;
if (this.node.widgets[this.dragIndex]) {
this.node.widgets[this.dragIndex].value = newValue;
}
this.node.setDirtyCanvas(true, true);
}
};
canvasEl.addEventListener("pointermove", this._onValuePointerMove);
this._onGlobalClickToStop = () => {
this.stopValueDrag();
};
document.addEventListener("click", this._onGlobalClickToStop, { capture: true });
this._onKeyDownToStop = (e) => {
if (e.key === "Enter") {
this.stopValueDrag();
}
};
document.addEventListener("keydown", this._onKeyDownToStop);
}
stopValueDrag() {
const canvasEl = document.getElementById("graph-canvas");
if (!canvasEl) return;
if (this._onValuePointerMove) {
canvasEl.removeEventListener("pointermove", this._onValuePointerMove);
this._onValuePointerMove = null;
}
if (this._onGlobalClickToStop) {
document.removeEventListener("click", this._onGlobalClickToStop, { capture: true });
this._onGlobalClickToStop = null;
}
if (this._onKeyDownToStop) {
document.removeEventListener("keydown", this._onKeyDownToStop);
this._onKeyDownToStop = null;
}
this.dragActive = false;
this.dragIndex = null;
this.node.setDirtyCanvas(true, true);
}
onMouseDown(e, pos) {
for (let i = 0; i < this.pencilRects.length; i++) {
const r = this.pencilRects[i];
if (!r) continue;
if (pos[0] >= r.x && pos[0] <= r.x + r.w &&
pos[1] >= r.y && pos[1] <= r.y + r.h) {
const centerX = r.x + (r.w / 2);
const centerY = r.y + (r.h / 2);
this.showInlineFloatEditor(i, { x: centerX, y: centerY, w: 80, h: 24 }, e);
return true;
}
}
for (let i = 0; i < this.labelRects.length; i++) {
const r = this.labelRects[i];
if (!r) continue;
if (pos[0] >= r.x && pos[0] <= r.x + r.w &&
pos[1] >= r.y && pos[1] <= r.y + r.h) {
const centerX = r.x + (r.w / 2);
const centerY = r.y + (r.h / 2);
this.showInlineLabelEditor(i, { x: centerX, y: centerY, w: 80, h: 24 }, e);
return true;
}
}
for (let i = 0; i < this.incrementRects.length; i++) {
const r = this.incrementRects[i];
if (pos[0] >= r.x && pos[0] <= r.x + r.w &&
pos[1] >= r.y && pos[1] <= r.y + r.h) {
this.incrementValue(i);
return true;
}
}
for (let i = 0; i < this.decrementRects.length; i++) {
const r = this.decrementRects[i];
if (pos[0] >= r.x && pos[0] <= r.x + r.w &&
pos[1] >= r.y && pos[1] <= r.y + r.h) {
this.decrementValue(i);
return true;
}
}
for (let i = 0; i < this.valueRects.length; i++) {
const r = this.valueRects[i];
if (r && pos[0] >= r.x && pos[0] <= r.x + r.w &&
pos[1] >= r.y && pos[1] <= r.y + r.h) {
this.startValueDragToggle(i, e);
return true;
}
}
if (this.gearRect) {
const r = this.gearRect;
if (pos[0] >= r.x && pos[0] <= r.x + r.w &&
pos[1] >= r.y && pos[1] <= r.y + r.h) {
this.openGearPopup(e);
return true;
}
}
return false;
}
showInlineLabelEditor(index, rect, e) {
e.preventDefault();
e.stopPropagation();
const graphCanvas = app.graphcanvas;
if (!graphCanvas) {
const fallbackX = e.clientX - (rect.w * 0.5);
const fallbackY = e.clientY - (rect.h * 0.5);
return this.spawnInlineLabelEditorAtScreenPos(index, fallbackX, fallbackY, rect);
}
const nodeX = this.node.pos[0];
const nodeY = this.node.pos[1];
const localX = rect.x + (rect.w * 0.5);
const localY = rect.y + (rect.h * 0.5);
const scale = graphCanvas.ds.scale;
const offset = graphCanvas.ds.offset;
const canvasX = (nodeX + localX) * scale + offset[0];
const canvasY = (nodeY + localY) * scale + offset[1];
const canvasRect = graphCanvas.canvas.getBoundingClientRect();
const screenX = canvasRect.left + canvasX;
const screenY = canvasRect.top + canvasY;
this.spawnInlineLabelEditorAtScreenPos(index, screenX, screenY, rect);
}
spawnInlineLabelEditorAtScreenPos(index, screenX, screenY, rect) {
const input = document.createElement("input");
input.type = "text";
input.value = this.node.properties.labels[index] || `FLOAT ${index + 1}`;
input.style.position = "fixed";
const offsetX = -(rect.w * 0.5);
const offsetY = -(rect.h * 0.5);
const FixedOffsetX = 35;
const FixedOffsetY = 10;
input.style.left = (screenX + offsetX + FixedOffsetX) + "px";
input.style.top = (screenY + offsetY + FixedOffsetY) + "px";
input.style.width = Math.max(rect.w, 250) + "px";
input.style.height = Math.max(rect.h, 30) + "px";
input.style.fontSize = "14px";
input.style.zIndex = "10000";
input.style.background = "#181818";
input.style.color = "#fff";
input.style.border = "2px solid #4b3e72";
input.style.borderRadius = "6px";
input.style.outline = "none";
input.style.padding = "0 6px";
document.body.appendChild(input);
setTimeout(() => {
input.focus();
input.select();
}, 10);
const commit = () => {
let val = input.value.trim();
if (val.length > 120) {
val = val.substring(0, 120);
}
if (val.length > 0) {
this.node.properties.labels[index] = val;
this.node.setDirtyCanvas(true, true);
}
remove();
};
const remove = () => {
document.removeEventListener("click", outsideClick, true);
if (input.parentNode) {
input.parentNode.removeChild(input);
}
};
const outsideClick = (evt) => {
if (evt.target !== input) {
commit();
}
};
setTimeout(() => {
document.addEventListener("click", outsideClick, true);
}, 100);
input.addEventListener("keydown", (evt) => {
if (evt.key === "Enter") {
evt.preventDefault();
commit();
} else if (evt.key === "Escape") {
evt.preventDefault();
remove();
}
});
}
openGearPopup(e) {
e.stopPropagation();
// We'll leave Min exactly as the user typed it, and only snap Max from that Min.
for (let i = 0; i < this.node.properties.activeCount; i++) {
const stepVal = this.node.properties.floatSteps[i] || 0.1;
const hasMin = (typeof this.node.properties.floatMin[i] === "number");
const hasMax = (typeof this.node.properties.floatMax[i] === "number");
// If both min and max are defined, we snap max from min as the offset:
// newMax = min + step * round( (oldMax - min) / step )
if (hasMin && hasMax) {
const oldMin = this.node.properties.floatMin[i];
const oldMax = this.node.properties.floatMax[i];
const offsetMax = oldMin + (stepVal * Math.round((oldMax - oldMin) / stepVal));
this.node.properties.floatMax[i] = offsetMax;
}
// Otherwise, do nothing to the user's min or max if one is missing
}
const nodeElem = this.node.htmlElement;
if (!nodeElem) {
this.createGearPopupFallback(e.clientX, e.clientY);
return;
}
const nodeRect = nodeElem.getBoundingClientRect();
const popupX = nodeRect.left + 10;
const popupY = nodeRect.top + 10;
const popup = new SettingsPopup(this.node);
popup.open(popupX, popupY);
this.populateGearPopup(popup);
}
createGearPopupFallback(x, y) {
const popup = new SettingsPopup(this.node);
popup.open(x, y);
this.populateGearPopup(popup);
}
populateGearPopup(popup) {
const content = popup.getContentContainer();
popup.clearContent();
content.style.maxHeight = "300px";
content.style.overflowY = "auto";
content.style.scrollbarWidth = "thin";
content.style.setProperty("::-webkit-scrollbar", "width: 6px; height: 6px;");
content.style.setProperty("::-webkit-scrollbar-thumb", "background-color: #4b3e72; border-radius: 10px;");
content.style.setProperty("::-webkit-scrollbar-track", "background-color: #181818; border-radius: 10px;");
const addRemoveLabel = document.createElement("div");
addRemoveLabel.textContent = "Add/Remove Floats";
addRemoveLabel.style.color = "#fff";
addRemoveLabel.style.fontSize = "12px";
addRemoveLabel.style.textAlign = "center";
content.appendChild(addRemoveLabel);
const buttonRow = document.createElement("div");
buttonRow.style.display = "flex";
buttonRow.style.justifyContent = "center";
buttonRow.style.alignItems = "center";
buttonRow.style.width = "100%";
buttonRow.style.gap = "10px";
buttonRow.style.marginTop = "-5px";
content.appendChild(buttonRow);
const minusBtn = document.createElement("button");
minusBtn.textContent = "-";
minusBtn.style.background = "#333";
minusBtn.style.color = "#fff";
minusBtn.style.padding = "0px 12px";
minusBtn.style.fontSize = "16px";
buttonRow.appendChild(minusBtn);
const plusBtn = document.createElement("button");
plusBtn.textContent = "+";
plusBtn.style.background = "#333";
plusBtn.style.color = "#fff";
plusBtn.style.padding = "0px 8px";
plusBtn.style.fontSize = "16px";
buttonRow.appendChild(plusBtn);
const stepsLabel = document.createElement("div");
stepsLabel.textContent = "Decimals / Steps / Min / Max";
stepsLabel.style.color = "#fff";
stepsLabel.style.fontSize = "12px";
stepsLabel.style.textAlign = "center";
content.appendChild(stepsLabel);
const stepsContainer = document.createElement("div");
stepsContainer.style.cssText = `
margin-top:-5px;
background: #131313;
padding-top: 5px;
padding-bottom: 5px;
min-height: 100px;
width: 95%;
overflow-y: auto;
scrollbar-width: thin;
`;
stepsContainer.style.setProperty("::-webkit-scrollbar", "width: 6px; height: 6px;");
stepsContainer.style.setProperty("::-webkit-scrollbar-thumb", "background-color: #4b3e72; border-radius: 10px;");
stepsContainer.style.setProperty("::-webkit-scrollbar-track", "background-color: #181818; border-radius: 10px;");
content.appendChild(stepsContainer);
const stepsList = document.createElement("div");
stepsContainer.appendChild(stepsList);
const updateStepInputs = () => {
stepsList.innerHTML = '';
for (let i = 0; i < this.node.properties.activeCount; i++) {
const rowDiv = document.createElement("div");
rowDiv.style.display = "flex";
rowDiv.style.justifyContent = "center";
rowDiv.style.alignItems = "center";
rowDiv.style.marginBottom = "5px";
rowDiv.style.paddingBottom = "2px";
rowDiv.style.borderBottom = "2px solid #262626";
// Label
const floatLabel = document.createElement("span");
floatLabel.textContent = `f${i + 1}`;
floatLabel.style.color = "#fff";
floatLabel.style.fontSize = "14px";
floatLabel.style.marginRight = "5px";
rowDiv.appendChild(floatLabel);
// Precision
const precInput = document.createElement("input");
precInput.type = "number";
precInput.min = "1";
precInput.max = "5";
precInput.value = this.node.properties.floatPrecision[i] || 1;
precInput.placeholder = "Prec";
precInput.style.width = "40px";
precInput.style.fontSize = "12px";
precInput.style.background = "#333";
precInput.style.color = "#fff";
precInput.style.border = "2px solid #4b3e72";
precInput.style.borderRadius = "6px";
precInput.style.textAlign = "center";
precInput.style.marginRight = "5px";
rowDiv.appendChild(precInput);
// Step
const stepInput = document.createElement("input");
stepInput.type = "number";
stepInput.value = this.node.properties.floatSteps[i] || 0.1;
stepInput.style.width = "60px";
stepInput.style.fontSize = "12px";
stepInput.style.background = "#333";
stepInput.style.color = "#fff";
stepInput.style.border = "2px solid #4b3e72";
stepInput.style.borderRadius = "6px";
stepInput.style.textAlign = "center";
stepInput.style.marginRight = "5px";
rowDiv.appendChild(stepInput);
// Min
const minInput = document.createElement("input");
minInput.type = "number";
minInput.placeholder = "Min";
minInput.style.width = "70px";
minInput.style.fontSize = "12px";
minInput.style.background = "#333";
minInput.style.color = "#fff";
minInput.style.border = "2px solid #4b3e72";
minInput.style.borderRadius = "6px";
minInput.style.textAlign = "center";
minInput.style.marginRight = "5px";
if (typeof this.node.properties.floatMin[i] === "number") {
minInput.value = this.node.properties.floatMin[i];
} else {
minInput.value = "";
}
rowDiv.appendChild(minInput);
// Max
const maxInput = document.createElement("input");
maxInput.type = "number";
maxInput.placeholder = "Max";
maxInput.style.width = "70px";
maxInput.style.fontSize = "12px";
maxInput.style.background = "#333";
maxInput.style.color = "#fff";
maxInput.style.border = "2px solid #4b3e72";
maxInput.style.borderRadius = "6px";
maxInput.style.textAlign = "center";
if (typeof this.node.properties.floatMax[i] === "number") {
maxInput.value = this.node.properties.floatMax[i];
} else {
maxInput.value = "";
}
rowDiv.appendChild(maxInput);
stepsList.appendChild(rowDiv);
// Helper to set .step on min/max, and also set minInput.min
// so that the browser uses that as the stepping base.
const syncMinMaxStep = () => {
let currentPrec = this.node.properties.floatPrecision[i] || 1;
let currentStep = parseFloat(
(this.node.properties.floatSteps[i] || 0.1).toFixed(currentPrec)
);
// 1) set step for min/max
minInput.step = currentStep.toString();
maxInput.step = currentStep.toString();
// 2) set min attribute on minInput if user typed a numeric min
const userMin = parseFloat(minInput.value);
if (!isNaN(userMin)) {
minInput.min = userMin.toString();
} else {
minInput.removeAttribute("min");
}
};
// On creation, call it once
syncMinMaxStep();
// ================= EVENT HANDLERS =================
// Precision changed
precInput.addEventListener("change", () => {
let newPrec = parseInt(precInput.value, 10);
if (isNaN(newPrec) || newPrec < 1) newPrec = 1;
if (newPrec > 5) newPrec = 5;
precInput.value = newPrec;
this.node.properties.floatPrecision[i] = newPrec;
// Snap step to new precision
let oldStep = parseFloat(stepInput.value);
if (isNaN(oldStep) || oldStep <= 0) oldStep = 0.1;
let stepVal = parseFloat(oldStep.toFixed(newPrec));
if (stepVal <= 0) stepVal = 0.1;
stepInput.value = stepVal;
this.node.properties.floatSteps[i] = stepVal;
// Resync step + min attribute
syncMinMaxStep();
// Snap max from min if both exist
if (typeof this.node.properties.floatMin[i] === "number"
&& typeof this.node.properties.floatMax[i] === "number") {
const oldMax = this.node.properties.floatMax[i];
const offsetMin = this.node.properties.floatMin[i];
let newMax = offsetMin + (stepVal * Math.round((oldMax - offsetMin) / stepVal));
newMax = parseFloat(newMax.toFixed(newPrec));
if (newMax !== oldMax) {
this.showEphemeralPopup(
`Max snapped to: ${newMax} (step from min=${offsetMin})`,
precInput.getBoundingClientRect().left,
precInput.getBoundingClientRect().top - 25
);
this.node.properties.floatMax[i] = newMax;
maxInput.value = newMax.toString();
}
}
// Also snap the current value
let oldVal = this.node.properties.values[i];
let newVal = stepVal * Math.round(oldVal / stepVal);
newVal = parseFloat(newVal.toFixed(newPrec));
newVal = this.clampValue(i, newVal);
this.node.properties.values[i] = newVal;
this.node.setDirtyCanvas(true, true);
});
// Step changed
stepInput.addEventListener("change", () => {
const newStepRaw = parseFloat(stepInput.value);
if (isNaN(newStepRaw) || newStepRaw <= 0) {
stepInput.value = this.node.properties.floatSteps[i] || 0.1;
return;
}
const decimals = this.node.properties.floatPrecision[i] || 1;
const stepVal = parseFloat(newStepRaw.toFixed(decimals));
stepInput.value = stepVal;
this.node.properties.floatSteps[i] = stepVal;
// Resync step + min attribute
syncMinMaxStep();
// If we have both min and max, snap the max from min
if (typeof this.node.properties.floatMin[i] === "number"
&& typeof this.node.properties.floatMax[i] === "number") {
const oldMax = this.node.properties.floatMax[i];
const offsetMin = this.node.properties.floatMin[i];
let newMax = offsetMin + (stepVal * Math.round((oldMax - offsetMin) / stepVal));
newMax = parseFloat(newMax.toFixed(decimals));
if (newMax !== oldMax) {
this.showEphemeralPopup(
`Max snapped to: ${newMax} (step from min=${offsetMin})`,
stepInput.getBoundingClientRect().left,
stepInput.getBoundingClientRect().top - 25
);
this.node.properties.floatMax[i] = newMax;
maxInput.value = newMax.toString();
}
}
// Snap the current value
let oldVal = this.node.properties.values[i];
let newVal = stepVal * Math.round(oldVal / stepVal);
newVal = parseFloat(newVal.toFixed(decimals));
newVal = this.clampValue(i, newVal);
this.node.properties.values[i] = newVal;
this.node.setDirtyCanvas(true, true);
});
// Min changed
minInput.addEventListener("change", () => {
const val = parseFloat(minInput.value);
const decimals = this.node.properties.floatPrecision[i] || 1;
const stepVal = this.node.properties.floatSteps[i] || 0.1;
if (!isNaN(val)) {
const newMin = parseFloat(val.toFixed(decimals));
this.node.properties.floatMin[i] = newMin;
minInput.value = newMin.toString();
// Also set minInput.min so the arrow stepping starts at newMin
minInput.min = newMin.toString();
// If max is defined, snap it from the new min
if (typeof this.node.properties.floatMax[i] === "number") {
const oldMax = this.node.properties.floatMax[i];
let newMax = newMin + (stepVal * Math.round((oldMax - newMin) / stepVal));
newMax = parseFloat(newMax.toFixed(decimals));
if (newMax !== oldMax) {
this.showEphemeralPopup(
`Max snapped to: ${newMax} (step from min=${newMin})`,
minInput.getBoundingClientRect().left,
minInput.getBoundingClientRect().top - 25
);
this.node.properties.floatMax[i] = newMax;
maxInput.value = newMax.toString();
}
}
} else {
// Clear min
this.node.properties.floatMin[i] = undefined;
minInput.removeAttribute("min");
minInput.value = "";
}
this.node.setDirtyCanvas(true, true);
});
// Max changed
maxInput.addEventListener("change", () => {
const val = parseFloat(maxInput.value);
const decimals = this.node.properties.floatPrecision[i] || 1;
const stepVal = this.node.properties.floatSteps[i] || 0.1;
const offsetMin = (typeof this.node.properties.floatMin[i] === "number")
? this.node.properties.floatMin[i]
: 0;
if (!isNaN(val)) {
let newMax = offsetMin + (stepVal * Math.round((val - offsetMin) / stepVal));
newMax = parseFloat(newMax.toFixed(decimals));
if (newMax !== val) {
this.showEphemeralPopup(
`Max snapped to: ${newMax} (step from min=${offsetMin})`,
maxInput.getBoundingClientRect().left,
maxInput.getBoundingClientRect().top - 25
);
}
this.node.properties.floatMax[i] = newMax;
maxInput.value = newMax.toString();
} else {
// Clear max
this.node.properties.floatMax[i] = undefined;
maxInput.value = "";
}
this.node.setDirtyCanvas(true, true);
});
}
};
updateStepInputs();
// Create a title for the color section
const colorSectionTitle = document.createElement("div");
colorSectionTitle.textContent = "Background Colors";
colorSectionTitle.style.color = "#fff";
colorSectionTitle.style.fontSize = "12px";
colorSectionTitle.style.textAlign = "center";
colorSectionTitle.style.marginTop = "5px";
content.appendChild(colorSectionTitle);
// Create a row container
const colorRow = document.createElement("div");
colorRow.style.display = "flex";
colorRow.style.flexWrap = "wrap";
colorRow.style.alignItems = "center";
colorRow.style.justifyContent = "space-between";
colorRow.style.marginBottom = "5px";
content.appendChild(colorRow);
// Node BG block
const nodeColorBlock = document.createElement("div");
nodeColorBlock.style.display = "flex";
nodeColorBlock.style.alignItems = "center";
nodeColorBlock.style.gap = "4px";
colorRow.appendChild(nodeColorBlock);
const nodeColorLabel = document.createElement("div");
nodeColorLabel.textContent = "Node";
nodeColorLabel.style.color = "#fff";
nodeColorLabel.style.fontSize = "12px";
nodeColorBlock.appendChild(nodeColorLabel);
const nodeColorPicker = document.createElement("input");
nodeColorPicker.type = "color";
nodeColorPicker.value = this.node.properties.bgColor || "#181818";
nodeColorPicker.style.width = "40px";
nodeColorPicker.style.height = "24px";
nodeColorBlock.appendChild(nodeColorPicker);
nodeColorPicker.addEventListener("input", (e) => {
this.node.properties.bgColor = e.target.value;
this.node.setDirtyCanvas(true, true);
});
// Row BG block
const rowBgColorBlock = document.createElement("div");
rowBgColorBlock.style.display = "flex";
rowBgColorBlock.style.alignItems = "center";
rowBgColorBlock.style.gap = "4px";
colorRow.appendChild(rowBgColorBlock);
const rowColorLabel = document.createElement("div");
rowColorLabel.textContent = "Row";
rowColorLabel.style.color = "#fff";
rowColorLabel.style.fontSize = "12px";
rowBgColorBlock.appendChild(rowColorLabel);
const rowBgColorPicker = document.createElement("input");
rowBgColorPicker.type = "color";
rowBgColorPicker.value = this.node.properties.rowBgColor || "#121212";
rowBgColorPicker.style.width = "40px";
rowBgColorPicker.style.height = "24px";
rowBgColorBlock.appendChild(rowBgColorPicker);
rowBgColorPicker.addEventListener("input", (e) => {
this.node.properties.rowBgColor = e.target.value;
this.node.setDirtyCanvas(true, true);
});
// Button BG block
const buttonColorBlock = document.createElement("div");
buttonColorBlock.style.display = "flex";
buttonColorBlock.style.alignItems = "center";
buttonColorBlock.style.gap = "4px";
colorRow.appendChild(buttonColorBlock);
const buttonColorLabel = document.createElement("div");
buttonColorLabel.textContent = "Button";
buttonColorLabel.style.color = "#fff";
buttonColorLabel.style.fontSize = "12px";
buttonColorBlock.appendChild(buttonColorLabel);
const buttonColorPicker = document.createElement("input");
buttonColorPicker.type = "color";
buttonColorPicker.value = this.node.properties.buttonColor || "#4b3e72";
buttonColorPicker.style.width = "40px";
buttonColorPicker.style.height = "24px";
buttonColorBlock.appendChild(buttonColorPicker);
buttonColorPicker.addEventListener("input", (e) => {
this.node.properties.buttonColor = e.target.value;
this.node.setDirtyCanvas(true, true);
});
// CHANGED SECTION to place bottom padding, row spacing, and the show label checkbox in one row
const spacingRow = document.createElement("div");
spacingRow.style.display = "flex";
spacingRow.style.flexWrap = "wrap";
spacingRow.style.justifyContent = "space-between";
spacingRow.style.gap = "15px";
spacingRow.style.marginTop = "8px";
content.appendChild(spacingRow);
// Bottom padding container
const bottomPadContainer = document.createElement("div");
bottomPadContainer.style.display = "flex";
bottomPadContainer.style.flexDirection = "column";
bottomPadContainer.style.alignItems = "flex-start";
spacingRow.appendChild(bottomPadContainer);
const bottomPaddingLabel = document.createElement("div");
bottomPaddingLabel.textContent = "Bottom Padding (px)";
bottomPaddingLabel.style.color = "#fff";
bottomPaddingLabel.style.fontSize = "12px";
bottomPadContainer.appendChild(bottomPaddingLabel);
const bottomPaddingInput = document.createElement("input");
bottomPaddingInput.type = "number";
bottomPaddingInput.value = this.node.properties.bottomPadding || 0;
bottomPaddingInput.style.width = "40px";
bottomPaddingInput.style.fontSize = "12px";
bottomPaddingInput.style.background = "#181818";
bottomPaddingInput.style.color = "#fff";
bottomPaddingInput.style.textAlign = "center";
bottomPadContainer.appendChild(bottomPaddingInput);
bottomPaddingInput.addEventListener("change", () => {
const newVal = parseFloat(bottomPaddingInput.value);
if (!isNaN(newVal)) {
this.node.properties.bottomPadding = newVal;
this.setupOutputsAndWidgets();
this.node.setDirtyCanvas(true, true);
}
});
// Row spacing container
const rowSpacingContainer = document.createElement("div");
rowSpacingContainer.style.display = "flex";
rowSpacingContainer.style.flexDirection = "column";
rowSpacingContainer.style.alignItems = "flex-start";
spacingRow.appendChild(rowSpacingContainer);
const rowSpacingLabel = document.createElement("div");
rowSpacingLabel.textContent = "Row Gap (px)";
rowSpacingLabel.style.color = "#fff";
rowSpacingLabel.style.fontSize = "12px";
rowSpacingContainer.appendChild(rowSpacingLabel);
const rowSpacingInput = document.createElement("input");
rowSpacingInput.type = "number";
rowSpacingInput.value = (typeof this.node.properties.rowSpacing === "number") ? this.node.properties.rowSpacing : "";
rowSpacingInput.style.width = "40px";
rowSpacingInput.style.fontSize = "12px";
rowSpacingInput.style.background = "#181818";
rowSpacingInput.style.color = "#fff";
rowSpacingInput.style.textAlign = "center";
rowSpacingContainer.appendChild(rowSpacingInput);
rowSpacingInput.addEventListener("change", () => {
const newVal = parseFloat(rowSpacingInput.value);
if (!isNaN(newVal)) {
this.node.properties.rowSpacing = newVal;
} else {
this.node.properties.rowSpacing = undefined;
}
this.node.setDirtyCanvas(true, true);
});
// Show label container
const showLabelContainer = document.createElement("div");
showLabelContainer.style.display = "flex";
showLabelContainer.style.flexDirection = "column";
showLabelContainer.style.alignItems = "flex-start";
spacingRow.appendChild(showLabelContainer);
const showLabelDiv = document.createElement("div");
showLabelDiv.textContent = "Show # Labels";
showLabelDiv.style.color = "#fff";
showLabelDiv.style.fontSize = "12px";
showLabelContainer.appendChild(showLabelDiv);
const showLabelCheckbox = document.createElement("input");
showLabelCheckbox.type = "checkbox";
showLabelCheckbox.style.marginTop = "2px";
showLabelCheckbox.checked = (this.node.properties.showStaticLabelAndLine !== false);
showLabelContainer.appendChild(showLabelCheckbox);
showLabelCheckbox.addEventListener("change", (e) => {
this.node.properties.showStaticLabelAndLine = e.target.checked;
this.node.setDirtyCanvas(true, true);
});
// Existing group labels section
const existingLabelsHeader = document.createElement("div");
existingLabelsHeader.textContent = "Existing Group Labels";
existingLabelsHeader.style.color = "#fff";
existingLabelsHeader.style.fontSize = "12px";
existingLabelsHeader.style.textAlign = "center";
content.appendChild(existingLabelsHeader);
const groupLabelsContainer = document.createElement("div");
groupLabelsContainer.style.display = "flex";
groupLabelsContainer.style.flexDirection = "column";
groupLabelsContainer.style.gap = "4px";
groupLabelsContainer.style.marginBottom = "10px";
content.appendChild(groupLabelsContainer);
const refreshGroupLabelsUI = () => {
groupLabelsContainer.innerHTML = "";
for (let i = 0; i < this.node.properties.groupLabels.length; i++) {
const gl = this.node.properties.groupLabels[i];
const rowDiv = document.createElement("div");
rowDiv.style.display = "flex";
rowDiv.style.gap = "5px";
// Text
const textInput = document.createElement("input");
textInput.type = "text";
textInput.value = gl.text;
textInput.style.width = "120px";
rowDiv.appendChild(textInput);
// Color picker for background
const colorInput = document.createElement("input");
colorInput.type = "color";
colorInput.value = gl.bgColor || "#242424";
colorInput.style.width = "40px";
rowDiv.appendChild(colorInput);
// Position (rowSelect)
const rowSelect = document.createElement("select");
for (let r = 0; r <= this.node.properties.activeCount; r++) {
const opt = document.createElement("option");
opt.value = r;
opt.text = (r === this.node.properties.activeCount)
? `After row ${r}`
: `Before row ${r + 1}`;
if (r === gl.beforeRow) {
opt.selected = true;
}
rowSelect.appendChild(opt);
}
rowDiv.appendChild(rowSelect);
// Remove button
const removeBtn = document.createElement("button");
removeBtn.textContent = "X";
removeBtn.style.background = "#a33";
removeBtn.style.color = "#fff";
rowDiv.appendChild(removeBtn);
// Event handlers
removeBtn.addEventListener("click", () => {
this.node.properties.groupLabels.splice(i, 1);
this.node.setDirtyCanvas(true, true);
refreshGroupLabelsUI();
});
textInput.addEventListener("change", () => {
gl.text = textInput.value;
this.node.setDirtyCanvas(true, true);
});
colorInput.addEventListener("input", () => {
gl.bgColor = colorInput.value;
this.node.setDirtyCanvas(true, true);
});
rowSelect.addEventListener("change", () => {
gl.beforeRow = parseInt(rowSelect.value, 10);
this.node.setDirtyCanvas(true, true);
});
groupLabelsContainer.appendChild(rowDiv);
}
};
refreshGroupLabelsUI();
// Add new group label
const addGroupLabelBtn = document.createElement("button");
addGroupLabelBtn.textContent = "+ Group Label";
addGroupLabelBtn.style.background = "#444";
addGroupLabelBtn.style.color = "#fff";
content.appendChild(addGroupLabelBtn);
addGroupLabelBtn.addEventListener("click", () => {
const newRow = document.createElement("div");
newRow.style.marginTop = "5px";
newRow.style.marginLeft = "5px";
newRow.style.marginRight = "5px";
newRow.style.display = "flex";
newRow.style.gap = "5px";
const lblInput = document.createElement("input");
lblInput.type = "text";
lblInput.placeholder = "Label text...";
lblInput.style.width = "120px";
newRow.appendChild(lblInput);
const colorInput = document.createElement("input");
colorInput.type = "color";
colorInput.value = "#242424";
colorInput.style.width = "40px";
newRow.appendChild(colorInput);
const rowSelect = document.createElement("select");
for (let i = 0; i <= this.node.properties.activeCount; i++) {
const opt = document.createElement("option");
opt.value = i;
opt.text = (i === this.node.properties.activeCount)
? `After row ${i}`
: `Before row ${i + 1}`;
rowSelect.appendChild(opt);
}
newRow.appendChild(rowSelect);
const saveBtn = document.createElement("button");
saveBtn.textContent = "Add";
saveBtn.style.background = "#666";
saveBtn.style.color = "#fff";
newRow.appendChild(saveBtn);
saveBtn.addEventListener("click", () => {
const textVal = lblInput.value.trim() || "Untitled Group";
const beforeRowVal = parseInt(rowSelect.value, 10) || 0;
this.node.properties.groupLabels.push({
text: textVal,
beforeRow: beforeRowVal,
bgColor: colorInput.value
});
this.node.setDirtyCanvas(true, true);
newRow.remove();
refreshGroupLabelsUI();
});
content.appendChild(newRow);
});
minusBtn.addEventListener("click", (evt) => {
evt.stopPropagation();
const currentWidth = this.node.size[0];
if (this.node.properties.activeCount > 1) {
this.node.properties.activeCount = Math.max(1, this.node.properties.activeCount - 1);
this.adjustArrays();
updateStepInputs();
this.setupOutputsAndWidgets();
if (this.node.onOutputsChange) {
this.node.onOutputsChange();
}
this.node.setDirtyCanvas(true, true);
this.node.size[0] = currentWidth;
this.node.setDirtyCanvas(true, true);
setTimeout(() => popup.reposition(this.node.htmlElement, 10, 10), 50);
}
});
plusBtn.addEventListener("click", (evt) => {
evt.stopPropagation();
const currentWidth = this.node.size[0];
if (this.node.properties.activeCount < 20) {
this.node.properties.activeCount = Math.min(20, this.node.properties.activeCount + 1);
this.adjustArrays();
updateStepInputs();
this.setupOutputsAndWidgets();
if (this.node.onOutputsChange) {
this.node.onOutputsChange();
}
this.node.setDirtyCanvas(true, true);
this.node.size[0] = currentWidth;
this.node.setDirtyCanvas(true, true);
setTimeout(() => popup.reposition(this.node.htmlElement, 10, 10), 50);
}
});
}
// FLoat version of the inline editor
showInlineFloatEditor(index, rect, e) {
e.preventDefault();
e.stopPropagation();
const graphCanvas = app.graphcanvas;
if (!graphCanvas) {
const fallbackX = e.clientX - (rect.w * 0.5);
const fallbackY = e.clientY - (rect.h * 0.5);
return this.spawnInlineFloatEditorAtScreenPos(index, fallbackX, fallbackY, rect);
}
const nodeX = this.node.pos[0];
const nodeY = this.node.pos[1];
const localX = rect.x + (rect.w * 0.5);
const localY = rect.y + (rect.h * 0.5);
const scale = graphCanvas.ds.scale;
const offset = graphCanvas.ds.offset;
const canvasX = (nodeX + localX) * scale + offset[0];
const canvasY = (nodeY + localY) * scale + offset[1];
const canvasRect = graphCanvas.canvas.getBoundingClientRect();
const screenX = canvasRect.left + canvasX;
const screenY = canvasRect.top + canvasY;
this.spawnInlineFloatEditorAtScreenPos(index, screenX, screenY, rect);
}
spawnInlineFloatEditorAtScreenPos(index, screenX, screenY, rect) {
const input = document.createElement("input");
input.type = "text";
input.value = this.node.properties.values[index].toString();
input.style.position = "fixed";
const offsetX = -(rect.w * 0.5);
const offsetY = -(rect.h * 0.5);
const FixedOffsetX = 25;
const FixedOffsetY = 10;
input.style.left = (screenX + offsetX + FixedOffsetX) + "px";
input.style.top = (screenY + offsetY + FixedOffsetY) + "px";
input.style.width = Math.max(rect.w, 60) + "px";
input.style.height = Math.max(rect.h, 24) + "px";
input.style.fontSize = "14px";
input.style.zIndex = "10000";
input.style.background = "#181818";
input.style.color = "#fff";
input.style.border = "2px solid #4b3e72";
input.style.borderRadius = "6px";
input.style.outline = "none";
input.style.padding = "0 6px";
document.body.appendChild(input);
setTimeout(() => {
input.focus();
input.select();
}, 10);
const commit = () => {
let inputVal = input.value.trim();
if (inputVal.length > 0) {
let result;
try {
if (/^[0-9+\-*/()..\s]+$/.test(inputVal)) {
result = eval(inputVal);
} else {
result = parseFloat(inputVal);
}
} catch {
result = parseFloat(inputVal);
}
if (!isNaN(result)) {
const messages = [];
const popupRect = input.getBoundingClientRect();
const popupX = popupRect.left;
const popupY = popupRect.top - 35;
const oldVal = result;
const step = this.node.properties.floatSteps[index] || 0.1;
// Snap to nearest multiple of step
const steppedVal = step * Math.round(result / step);
// Step changed?
if (steppedVal !== oldVal) {
messages.push(`Value not within step. Using step: ${steppedVal}`);
}
// Then clamp
const finalVal = this.clampValue(index, steppedVal);
if (finalVal !== steppedVal) {
const reason = finalVal === this.node.properties.floatMin[index]
? `Min (${finalVal})`
: `Max (${finalVal})`;
messages.push(`Value out of range. Using ${reason}`);
}
if (messages.length) {
messages.push(`Final: ${finalVal}`);
this.showEphemeralPopup(messages.join(" | "), popupX, popupY);
}
this.node.properties.values[index] = finalVal;
if (this.node.widgets[index]) {
this.node.widgets[index].value = finalVal;
}
this.node.setDirtyCanvas(true, true);
}
}
remove();
};
const remove = () => {
document.removeEventListener("click", outsideClick, true);
if (input.parentNode) {
input.parentNode.removeChild(input);
}
};
const outsideClick = (evt) => {
if (evt.target !== input) {
commit();
}
};
setTimeout(() => {
document.addEventListener("click", outsideClick, true);
}, 100);
input.addEventListener("keydown", (evt) => {
if (evt.key === "Enter") {
evt.preventDefault();
commit();
} else if (evt.key === "Escape") {
evt.preventDefault();
remove();
}
});
}
showEphemeralPopup(message, x, y) {
const popup = document.createElement("div");
popup.textContent = message;
popup.style.position = "fixed";
popup.style.left = x + "px";
popup.style.top = y + "px";
popup.style.padding = "6px 10px";
popup.style.fontSize = "12px";
popup.style.background = "#333";
popup.style.color = "#fff";
popup.style.border = "2px solid #4b3e72";
popup.style.borderRadius = "6px";
popup.style.zIndex = "999999";
document.body.appendChild(popup);
setTimeout(() => {
if (popup.parentNode) {
popup.parentNode.removeChild(popup);
}
}, 2000);
}
incrementValue(index) {
const step = (this.node.properties.floatSteps && this.node.properties.floatSteps[index]) || 0.1;
let v = this.node.properties.values[index] + step;
v = this.clampValue(index, v);
this.node.properties.values[index] = v;
if (this.node.widgets[index]) {
this.node.widgets[index].value = v;
}
this.node.setDirtyCanvas(true, true);
}
decrementValue(index) {
const step = (this.node.properties.floatSteps && this.node.properties.floatSteps[index]) || 0.1;
let v = this.node.properties.values[index] - step;
v = this.clampValue(index, v);
this.node.properties.values[index] = v;
if (this.node.widgets[index]) {
this.node.widgets[index].value = v;
}
this.node.setDirtyCanvas(true, true);
}
adjustArrays() {
const count = this.node.properties.activeCount;
while (this.node.properties.labels.length < count) {
this.node.properties.labels.push(`f${this.node.properties.labels.length + 1}`);
}
this.node.properties.labels.length = count;
while (this.node.properties.values.length < count) {
this.node.properties.values.push(0.0);
}
this.node.properties.values.length = count;
while (this.node.properties.floatSteps.length < count) {
this.node.properties.floatSteps.push(0.1);
}
this.node.properties.floatSteps.length = count;
while (this.node.properties.floatMin.length < count) {
this.node.properties.floatMin.push(undefined);
}
this.node.properties.floatMin.length = count;
while (this.node.properties.floatMax.length < count) {
this.node.properties.floatMax.push(undefined);
}
this.node.properties.floatMax.length = count;
while (this.node.properties.floatPrecision.length < count) {
this.node.properties.floatPrecision.push(1);
}
this.node.properties.floatPrecision.length = count;
if (!Array.isArray(this.node.properties.groupLabels)) {
this.node.properties.groupLabels = [];
}
}
}
app.registerExtension({
name: "AIDocsClinicalTools.MultiFloatNodeAID",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name === "MultiFloatNodeAID") {
Object.assign(nodeType.prototype, {
onNodeCreated() {
this.multiFloatNode = new MultiFloatNodeAID(this);
},
onMouseDown(e, pos) {
return this.multiFloatNode.onMouseDown(e, pos);
},
});
}
}
});