Files
BlueprintCoding-ComfyUI_AID…/js/MultiIntNode.js
T
Blueprint Coding c9fbee143e Enforced step value open min/max and edit pencil
When generating certain images or videos you need a certain divisible by number such as 16 or it throws an error. These changes enforce the step value upon the min and max values to the nearest step and round of the free enter from the pencil to the nearest step.
2025-02-21 14:13:48 -07:00

1170 lines
47 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 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();
for (let i = 0; i < this.node.properties.activeCount; i++) {
const stepVal = this.node.properties.intSteps[i] || 1;
if (typeof this.node.properties.intMin[i] === "number") {
const oldMin = this.node.properties.intMin[i];
const newMin = stepVal * Math.round(oldMin / stepVal);
this.node.properties.intMin[i] = newMin;
}
if (typeof this.node.properties.intMax[i] === "number") {
const oldMax = this.node.properties.intMax[i];
const newMax = stepVal * Math.round(oldMax / stepVal);
this.node.properties.intMax[i] = newMax;
}
}
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;
}
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${i+1}`;
}
this.node.size[0] = oldSize[0];
}
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 rowHeight = 30;
const rowSpacing = margin;
const count = this.node.properties.activeCount;
const neededHeight = margin + count * (rowHeight + rowSpacing) + (this.node.properties.bottomPadding || 0);
if (this.node.size[1] < neededHeight) {
this.node.size[1] = neededHeight;
} else if (this.node.size[1] > neededHeight) {
this.node.size[1] = neededHeight;
}
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;
}
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;
for (let i = 0; i < count; i++) {
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 staticLabel = `·i${i + 1}`;
const staticLabelMetrics = ctx.measureText(staticLabel);
const staticLabelX = margin + 2;
const staticLabelY = y + rowHeight / 2;
ctx.fillText(staticLabel, staticLabelX, staticLabelY);
const lineStartX = staticLabelX + staticLabelMetrics.width + 4;
ctx.strokeStyle = rowTextColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(lineStartX, staticLabelY - rowHeight / 2);
ctx.lineTo(lineStartX, staticLabelY + rowHeight / 2);
ctx.stroke();
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;
}
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.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
};
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, valY);
this.valueRects[i] = {
x: valX - valMetrics.width - 4,
y,
w: valMetrics.width + 8,
h: rowHeight
};
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
};
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;
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
};
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;
}
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
};
const bottomPad = parseInt(this.node.properties.bottomPadding, 10) || 0;
if (bottomPad > 0) {
ctx.fillStyle = "#181818";
ctx.fillRect(margin + radius, y, rowBgWidth, bottomPad);
y += bottomPad;
}
}
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();
// Before actually showing the popup, force min/max to resnap to step.
for (let i = 0; i < this.node.properties.activeCount; i++) {
const stepVal = this.node.properties.intSteps[i] || 1;
if (typeof this.node.properties.intMin[i] === "number") {
const oldMin = this.node.properties.intMin[i];
const snappedMin = stepVal * Math.round(oldMin / stepVal);
this.node.properties.intMin[i] = snappedMin;
}
if (typeof this.node.properties.intMax[i] === "number") {
const oldMax = this.node.properties.intMax[i];
const snappedMax = stepVal * Math.round(oldMax / stepVal);
this.node.properties.intMax[i] = snappedMax;
}
}
let 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);
}
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 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);
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);
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++) {
// A row for iN
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 input
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 input
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 input
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);
// Attach the rowDiv to stepsList
stepsList.appendChild(rowDiv);
// 1) STEP CHANGE EVENT:
stepInput.addEventListener("change", () => {
const newStep = parseInt(stepInput.value, 10);
if (isNaN(newStep) || newStep < 1) {
// Revert if invalid
stepInput.value = this.node.properties.intSteps[i] || 1;
return;
}
// Update step
this.node.properties.intSteps[i] = newStep;
// Now auto-resnap existing Min to the new step
if (typeof this.node.properties.intMin[i] === "number") {
const oldMin = this.node.properties.intMin[i];
const steppedMin = newStep * Math.round(oldMin / newStep);
if (steppedMin !== oldMin) {
this.showEphemeralPopup(
`Min not aligned with step. Using stepped value: ${steppedMin}`,
stepInput.getBoundingClientRect().left,
stepInput.getBoundingClientRect().top - 25
);
}
this.node.properties.intMin[i] = steppedMin;
minInput.value = steppedMin.toString();
}
// Auto-resnap existing Max to the new step
if (typeof this.node.properties.intMax[i] === "number") {
const oldMax = this.node.properties.intMax[i];
const steppedMax = newStep * Math.round(oldMax / newStep);
if (steppedMax !== oldMax) {
this.showEphemeralPopup(
`Max not aligned with step. Using stepped value: ${steppedMax}`,
stepInput.getBoundingClientRect().left,
stepInput.getBoundingClientRect().top - 25
);
}
this.node.properties.intMax[i] = steppedMax;
maxInput.value = steppedMax.toString();
}
});
// 2) MIN CHANGE EVENT:
minInput.addEventListener("change", () => {
const val = parseInt(minInput.value, 10);
if (!isNaN(val)) {
const stepVal = this.node.properties.intSteps[i] || 1;
const steppedVal = stepVal * Math.round(val / stepVal);
if (steppedVal !== val) {
this.showEphemeralPopup(
`Min not aligned with step. Using stepped value: ${steppedVal}`,
minInput.getBoundingClientRect().left,
minInput.getBoundingClientRect().top - 25
);
}
this.node.properties.intMin[i] = steppedVal;
minInput.value = steppedVal.toString();
} else {
this.node.properties.intMin[i] = undefined;
minInput.value = "";
}
});
// 3) MAX CHANGE EVENT:
maxInput.addEventListener("change", () => {
const val = parseInt(maxInput.value, 10);
if (!isNaN(val)) {
const stepVal = this.node.properties.intSteps[i] || 1;
const steppedVal = stepVal * Math.round(val / stepVal);
if (steppedVal !== val) {
this.showEphemeralPopup(
`Max not aligned with step. Using stepped value: ${steppedVal}`,
maxInput.getBoundingClientRect().left,
maxInput.getBoundingClientRect().top - 25
);
}
this.node.properties.intMax[i] = steppedVal;
maxInput.value = steppedVal.toString();
} else {
this.node.properties.intMax[i] = undefined;
maxInput.value = "";
}
});
}
};
updateStepInputs();
const bgColorLabel = document.createElement("div");
bgColorLabel.textContent = "Node Background Color";
bgColorLabel.style.color = "#fff";
bgColorLabel.style.fontSize = "12px";
bgColorLabel.style.textAlign = "center";
content.appendChild(bgColorLabel);
const colorPicker = document.createElement("input");
colorPicker.type = "color";
colorPicker.value = this.node.properties.bgColor || "#181818";
colorPicker.style.marginTop = "-5px";
colorPicker.style.minHeight = "25px";
colorPicker.style.width = "100%";
colorPicker.style.marginBottom = "0px";
content.appendChild(colorPicker);
colorPicker.addEventListener("input", (e) => {
this.node.properties.bgColor = e.target.value;
this.node.setDirtyCanvas(true, true);
});
const rowBgColorLabel = document.createElement("div");
rowBgColorLabel.textContent = "Row Background Color";
rowBgColorLabel.style.color = "#fff";
rowBgColorLabel.style.fontSize = "12px";
rowBgColorLabel.style.textAlign = "center";
content.appendChild(rowBgColorLabel);
const rowBgColorPicker = document.createElement("input");
rowBgColorPicker.type = "color";
rowBgColorPicker.value = this.node.properties.rowBgColor || "#121212";
rowBgColorPicker.style.marginTop = "-5px";
rowBgColorPicker.style.minHeight = "25px";
rowBgColorPicker.style.width = "100%";
rowBgColorPicker.style.marginBottom = "0px";
content.appendChild(rowBgColorPicker);
rowBgColorPicker.addEventListener("input", (e) => {
this.node.properties.rowBgColor = e.target.value;
this.node.setDirtyCanvas(true, true);
});
const buttonColorLabel = document.createElement("div");
buttonColorLabel.textContent = "Int +/- Button Color";
buttonColorLabel.style.color = "#fff";
buttonColorLabel.style.fontSize = "12px";
buttonColorLabel.style.textAlign = "center";
content.appendChild(buttonColorLabel);
const buttonColorPicker = document.createElement("input");
buttonColorPicker.type = "color";
buttonColorPicker.value = this.node.properties.buttonColor || "#4b3e72";
buttonColorPicker.style.marginTop = "-5px";
buttonColorPicker.style.minHeight = "25px";
buttonColorPicker.style.width = "100%";
buttonColorPicker.style.marginBottom = "0px";
content.appendChild(buttonColorPicker);
buttonColorPicker.addEventListener("input", (e) => {
this.node.properties.buttonColor = e.target.value;
this.node.setDirtyCanvas(true, true);
});
const bottomPaddingLabel = document.createElement("div");
bottomPaddingLabel.textContent = "Bottom Padding (px)";
bottomPaddingLabel.style.color = "#fff";
bottomPaddingLabel.style.fontSize = "12px";
bottomPaddingLabel.style.textAlign = "center";
content.appendChild(bottomPaddingLabel);
const bottomPaddingInput = document.createElement("input");
bottomPaddingInput.type = "number";
bottomPaddingInput.value = this.node.properties.bottomPadding || 0;
bottomPaddingInput.style.width = "60px";
bottomPaddingInput.style.fontSize = "12px";
bottomPaddingInput.style.background = "#181818";
bottomPaddingInput.style.color = "#fff";
bottomPaddingInput.style.textAlign = "center";
content.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);
}
});
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);
},
});
}
}
});