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

1499 lines
58 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 MultiIntNode {
constructor(node) {
this.node = node;
if (typeof node.properties.activeCount !== "number") {
node.properties.activeCount = 20;
}
if (!Array.isArray(node.properties.intSteps)) {
node.properties.intSteps = [];
}
if (!Array.isArray(node.properties.labels)) {
node.properties.labels = [];
}
if (!Array.isArray(node.properties.values)) {
node.properties.values = [];
}
if (!Array.isArray(node.properties.intMin)) {
node.properties.intMin = [];
}
if (!Array.isArray(node.properties.intMax)) {
node.properties.intMax = [];
}
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++) {
this.node.addOutput("", "INT");
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];
}
// CHANGED onDrawForeground + new drawGroupLabelRow
onDrawForeground(ctx) {
if (this.node.flags.collapsed) return;
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;
// Compute total needed height to factor in group labels as well
// A simple approach is to add groupLabels.length * groupLabelHeight, plus rowSpacing for each group label
// Then the float rows, plus bottom padding, etc. Or you can do it dynamically while drawing.
// Here, we do a rough pass to count how many group labels exist:
const neededHeight = margin
+ (count * (rowHeight + rowSpacing))
+ (groupLabels.length * (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;
}
// 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();
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 and group labels
for (let i = 0; i < count; i++) {
// Check if we have group labels that appear 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 the 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();
ctx.fillStyle = rowTextColor;
ctx.font = "12px Arial";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
const staticLabelY = y + rowHeight / 2;
let lineStartX = margin + 2;
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();
}
const valX = margin + rowBgWidth - 90;
const labelAreaStart = lineStartX + 6;
const labelAreaEnd = valX - 80;
let availableWidth = labelAreaEnd - labelAreaStart;
if (availableWidth < 0) {
availableWidth = 0;
}
const labelWidthRules = [
{ max: 10, set: 60 },
{ max: 15, set: 65 },
{ max: 30, set: 80 },
{ max: 50, set: 90 },
{ max: 70, set: 100 },
{ max: 90, set: 110 },
{ max: 150, multiplier: 1.2 },
{ max: 200, multiplier: 1.15 },
{ max: 400, multiplier: 0.98 },
{ max: 500, multiplier: 0.95 },
{ max: Infinity, multiplier: 0.93 }
];
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.font = "14px Arial";
ctx.fillStyle = rowTextColor;
ctx.fillText(displayLabel, labelAreaStart, staticLabelY);
const displayedLabelWidth = ctx.measureText(displayLabel).width;
this.labelRects[i] = { x: labelAreaStart, y, w: displayedLabelWidth + 8, h: rowHeight };
const val = this.node.properties.values[i] ?? 0;
const valStr = val.toString();
ctx.textAlign = "right";
ctx.font = "14px Arial";
const valMetrics = ctx.measureText(valStr);
ctx.fillText(valStr, valX, staticLabelY);
this.valueRects[i] = {
x: valX - valMetrics.width - 4,
y,
w: valMetrics.width + 8,
h: rowHeight
};
ctx.textAlign = "left";
ctx.font = "16px Arial";
const pencilIcon = "✎";
const pencilMetrics = ctx.measureText(pencilIcon);
const pencilX = valX + 6;
ctx.fillText(pencilIcon, pencilX, staticLabelY);
this.pencilRects[i] = {
x: pencilX,
y,
w: pencilMetrics.width + 4,
h: rowHeight
};
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;
}
// Finally, any 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
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 = parseInt(this.node.properties.bottomPadding, 10) || 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; // Adjust as needed
const radius = 8;
// Use the background color of the label, or default
const bg = labelObj.bgColor || "#242424";
ctx.fillStyle = bg;
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
const textColor = getTextColorForBackground(bg);
ctx.fillStyle = textColor;
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.intMin[index];
const maxVal = this.node.properties.intMax[index];
if (typeof minVal === "number" && val < minVal) {
val = minVal;
}
if (typeof maxVal === "number" && val > maxVal) {
val = maxVal;
}
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.intSteps && this.node.properties.intSteps[index]) || 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.showInlineIntEditor(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] || `INT ${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();
for (let i = 0; i < this.node.properties.activeCount; i++) {
const stepVal = this.node.properties.intSteps[i] || 1;
// If min is set, we respect the user's exact min.
// If max is set, we snap it from min (if min is a number).
if (typeof this.node.properties.intMin[i] === "number"
&& typeof this.node.properties.intMax[i] === "number") {
const minVal = this.node.properties.intMin[i];
const oldMax = this.node.properties.intMax[i];
const snappedMax = minVal + (stepVal * Math.round((oldMax - minVal) / stepVal));
this.node.properties.intMax[i] = snappedMax;
}
// If min is set but we do NOT forcibly snap it, we do nothing to the user’s intMin[i].
// If max is undefined or min is undefined, skip it altogether.
}
// Now open the gear popup UI
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);
}
// CHANGED populateGearPopup
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;");
// Add/Remove Ints
const addRemoveLabel = document.createElement("div");
addRemoveLabel.textContent = "Add/Remove Ints";
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);
// Steps / Min / Max label
const stepsLabel = document.createElement("div");
stepsLabel.textContent = "Steps / Min / Max";
stepsLabel.style.color = "#fff";
stepsLabel.style.fontSize = "12px";
stepsLabel.style.textAlign = "center";
content.appendChild(stepsLabel);
// Steps container
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";
const intLabel = document.createElement("span");
intLabel.textContent = `i${i + 1}`;
intLabel.style.color = "#fff";
intLabel.style.fontSize = "14px";
intLabel.style.marginRight = "5px";
rowDiv.appendChild(intLabel);
// STEP
const stepInput = document.createElement("input");
stepInput.type = "number";
stepInput.value = this.node.properties.intSteps[i] || 1;
stepInput.min = "1";
stepInput.style.width = "50px";
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 = "50px";
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.intMin[i] === "number") {
minInput.value = this.node.properties.intMin[i];
} else {
minInput.value = "";
}
rowDiv.appendChild(minInput);
// MAX
const maxInput = document.createElement("input");
maxInput.type = "number";
maxInput.placeholder = "Max";
maxInput.style.width = "50px";
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.intMax[i] === "number") {
maxInput.value = this.node.properties.intMax[i];
} else {
maxInput.value = "";
}
rowDiv.appendChild(maxInput);
stepsList.appendChild(rowDiv);
// Helper to set the .step attribute and also set the "min" attribute
// so that the stepping base is userMin, not 0
const syncMinMaxStep = () => {
const currentStep = this.node.properties.intSteps[i] || 1;
// Set .step for minInput & maxInput
minInput.step = currentStep.toString();
maxInput.step = currentStep.toString();
// If the user typed a numeric min, set that as the input's .min attribute
// so that the stepping base is from min, not 0
const userMin = parseInt(minInput.value, 10);
if (!isNaN(userMin)) {
// This ensures the arrow increments from userMin
minInput.min = userMin.toString();
} else {
// If min is cleared or invalid, remove the "min" attribute
minInput.removeAttribute("min");
}
};
// On creation, call syncMinMaxStep once
syncMinMaxStep();
// ================ EVENT HANDLERS ================
// STEP CHANGED
stepInput.addEventListener("change", () => {
const newStep = parseInt(stepInput.value, 10);
if (isNaN(newStep) || newStep < 1) {
stepInput.value = this.node.properties.intSteps[i] || 1;
return;
}
this.node.properties.intSteps[i] = newStep;
// Sync .step attribute on min/max and update stepping base
syncMinMaxStep();
// We do NOT snap the Min itself; we leave the user's min alone.
// But if there is a max, we snap it from min as the base offset.
const curMin = this.node.properties.intMin[i];
if (typeof curMin === "number" && typeof this.node.properties.intMax[i] === "number") {
const oldMax = this.node.properties.intMax[i];
const snappedMax = curMin + (newStep * Math.round((oldMax - curMin) / newStep));
if (snappedMax !== oldMax) {
this.showEphemeralPopup(
`Max snapped to: ${snappedMax} (step from min=${curMin})`,
stepInput.getBoundingClientRect().left,
stepInput.getBoundingClientRect().top - 25
);
this.node.properties.intMax[i] = snappedMax;
maxInput.value = snappedMax.toString();
}
}
});
// MIN CHANGED
minInput.addEventListener("change", () => {
const val = parseInt(minInput.value, 10);
if (!isNaN(val)) {
// Accept user’s min as typed
this.node.properties.intMin[i] = val;
// Update the .min attribute so the stepping base changes
minInput.min = val.toString();
// Then if there's a max, snap that from the new min
if (typeof this.node.properties.intMax[i] === "number") {
const oldMax = this.node.properties.intMax[i];
const stepVal = this.node.properties.intSteps[i] || 1;
const snappedMax = val + (stepVal * Math.round((oldMax - val) / stepVal));
if (snappedMax !== oldMax) {
this.showEphemeralPopup(
`Max snapped to: ${snappedMax} (step from min=${val})`,
minInput.getBoundingClientRect().left,
minInput.getBoundingClientRect().top - 25
);
this.node.properties.intMax[i] = snappedMax;
maxInput.value = snappedMax.toString();
}
}
} else {
// if user clears it, we set intMin[i] = undefined
this.node.properties.intMin[i] = undefined;
// remove stepping base
minInput.removeAttribute("min");
minInput.value = "";
}
});
// MAX CHANGED
maxInput.addEventListener("change", () => {
const val = parseInt(maxInput.value, 10);
if (!isNaN(val)) {
const stepVal = this.node.properties.intSteps[i] || 1;
const curMin = (typeof this.node.properties.intMin[i] === "number")
? this.node.properties.intMin[i]
: 0; // If min is not set, treat 0 as base
const snappedMax = curMin + (stepVal * Math.round((val - curMin) / stepVal));
if (snappedMax !== val) {
this.showEphemeralPopup(
`Max snapped to: ${snappedMax} (step from min=${curMin})`,
maxInput.getBoundingClientRect().left,
maxInput.getBoundingClientRect().top - 25
);
}
this.node.properties.intMax[i] = snappedMax;
maxInput.value = snappedMax.toString();
} else {
// Clear
this.node.properties.intMax[i] = undefined;
maxInput.value = "";
}
});
}
};
updateStepInputs();
// 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);
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
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
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
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);
});
// Spacing 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
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 = parseInt(bottomPaddingInput.value, 10);
if (!isNaN(newVal)) {
this.node.properties.bottomPadding = newVal;
this.setupOutputsAndWidgets();
this.node.setDirtyCanvas(true, true);
}
});
// Row spacing
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
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 || 0; i++) {
const gl = this.node.properties.groupLabels[i];
const rowDiv = document.createElement("div");
rowDiv.style.display = "flex";
rowDiv.style.gap = "5px";
const textInput = document.createElement("input");
textInput.type = "text";
textInput.value = gl.text;
textInput.style.width = "120px";
rowDiv.appendChild(textInput);
const colorInput = document.createElement("input");
colorInput.type = "color";
colorInput.value = gl.bgColor || "#242424";
colorInput.style.width = "40px";
rowDiv.appendChild(colorInput);
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);
const removeBtn = document.createElement("button");
removeBtn.textContent = "X";
removeBtn.style.background = "#a33";
removeBtn.style.color = "#fff";
rowDiv.appendChild(removeBtn);
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);
}
};
if (!Array.isArray(this.node.properties.groupLabels)) {
this.node.properties.groupLabels = [];
}
refreshGroupLabelsUI();
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);
}
});
}
createGearPopupFallback(x, y) {
const popup = new SettingsPopup(this.node);
popup.open(x, y);
this.populateGearPopup(popup);
}
showInlineIntEditor(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.spawnInlineEditorAtScreenPos(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.spawnInlineEditorAtScreenPos(index, screenX, screenY, rect);
}
spawnInlineEditorAtScreenPos(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 = parseInt(inputVal, 10);
}
} catch {
result = parseInt(inputVal, 10);
}
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.intSteps[index] || 1;
const steppedVal = step * Math.round(result / step);
// If step rounding changed the value
if (steppedVal !== oldVal) {
messages.push(`Set value not within step.`);
}
// Then clamp to min/max
const finalVal = this.clampValue(index, steppedVal);
// If clamp changed it again
if (finalVal !== steppedVal) {
const reason = finalVal === this.node.properties.intMin[index]
? `Min (${finalVal})`
: `Max (${finalVal})`;
messages.push(`Set value out of range.`);
}
// Now, if there were any adjustments, append a note of the final
if (messages.length > 0) {
messages.push(`Set value: ${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.intSteps && this.node.properties.intSteps[index]) || 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.intSteps && this.node.properties.intSteps[index]) || 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(`i${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);
}
this.node.properties.values.length = count;
while (this.node.properties.intSteps.length < count) {
this.node.properties.intSteps.push(1);
}
this.node.properties.intSteps.length = count;
while (this.node.properties.intMin.length < count) {
this.node.properties.intMin.push(undefined);
}
this.node.properties.intMin.length = count;
while (this.node.properties.intMax.length < count) {
this.node.properties.intMax.push(undefined);
}
this.node.properties.intMax.length = count;
}
}
app.registerExtension({
name: "AIDocsClinicalTools.MultiInt",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name === "MultiInt") {
Object.assign(nodeType.prototype, {
onNodeCreated() {
this.multiIntNode = new MultiIntNode(this);
},
onMouseDown(e, pos) {
return this.multiIntNode.onMouseDown(e, pos);
},
});
}
}
});