Files
SKB 1e13687c2c feat: Major node updates, features, and compatibility fixes
Addressed compatibility issues from a recent ComfyUI update and introduced several new features and stability improvements across the entire node suite.

- **NodeDesign**: Implemented 'Place Adjacent' tools ('Place Right' & 'Place Below') for rapid, connection-aware alignment and removed outdated keyboard shortcuts.

- **PaintPro**: Added a new fullscreen editing mode and a refresh button for an improved workflow.

- **TitlePlus**: Refactored the animation engine to use transform offsets, fixing position corruption bugs and increasing stability.

- **Core**: Introduced a `dragHelper.js` utility to centralize and standardize mouse drag prevention logic.

- **Fixes & Polish**: Fixed a separator rendering bug in MultiText, patched a NullGraphError in LensFlare, and applied general compatibility fixes to other nodes.
2025-08-14 17:52:58 +03:00

757 lines
29 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { preventNodeDrag } from "./dragHelper.js";
class MultiFloatNode {
constructor(node) {
this.node = node;
this.currentSlider = undefined;
this.isPressed = false;
this.currentHoverSlider = undefined;
this.colors = {
sliderBackground: "rgba(240, 240, 245, 0.1)",
sliderProgress: ["#000000", "#2980b9"],
handle: "#ecf0f1",
tooltip: "rgba(52, 152, 219, 0.9)",
text: "#2c3e50"
};
this.fonts = {
primary: "'Inter', sans-serif",
size: {
medium: "12px"
}
};
this.handleRadius = {
normal: 3,
hover: 4,
active: 5
};
this.topMargin = 12;
this.bottomMargin = 50;
this.margin = 30;
this.sliderHeight = 4;
this.sliderSpacing = 16;
this.setupNode();
this.updateColors(node.properties.startColor || "#423438", node.properties.slidercolor || "#69414e");
this.updateSliderCount(node.properties.sliderCount || 3);
this.calculateConstants();
// Instance-level onMouseDown to handle slider interactions cleanly
this.node.onMouseDown = function(e, pos) {
if (this.flags.collapsed) return false;
if (this.multiFloat.checkSliderInteraction(pos)) return true;
return this.multiFloat.checkSliderCountButtons(pos);
}.bind(this.node);
}
calculateConstants() {
this.sliderWidth = this.node.size[0] - (this.margin * 2);
}
setupNode() {
this.node.outputs = [];
const sliderCount = 3;
const minHeight = this.topMargin + (sliderCount * (this.sliderHeight + this.sliderSpacing)) + this.bottomMargin;
this.node.size = [240, minHeight];
this.node.properties = {
values: [0, 0, 0],
min: -100,
max: 100,
step: 0.1,
decimals: 1,
snap: false,
startColor: "#4A90E2",
slidercolor: "#D0021B",
sliderCount: 3,
};
this.node.widgets = [];
this.node.onDrawForeground = this.onDrawForeground.bind(this);
this.node.serialize_widgets = true;
}
updateValue(index, value) {
const decimals = this.node.properties.decimals;
const roundedValue = Number(value.toFixed(decimals));
this.node.properties.values[index] = roundedValue;
if (this.node.widgets[index]) {
const widget = this.node.widgets[index];
widget.value = roundedValue;
}
this.node.setDirtyCanvas(true);
this.updateOutputs();
}
updateOutputs() {
const activeCount = this.node.properties.sliderCount || 0;
this.node.properties.values.forEach((value, i) => {
if (i < activeCount && this.node.outputs[i]) {
const decimals = this.node.properties.decimals;
const roundedValue = parseFloat(value.toFixed(decimals));
this.node.outputs[i].value = roundedValue;
this.node.triggerSlot(i, roundedValue);
if (this.node.widgets[i]) {
this.node.widgets[i].value = roundedValue;
}
}
});
for (let i = activeCount; i < (this.node.outputs?.length || 0); i++) {
if (this.node.outputs[i]) {
this.node.outputs[i].value = undefined;
}
}
}
onDrawForeground(ctx) {
if (this.node.flags.collapsed) return;
this.calculateConstants();
const { properties } = this.node;
const activeCount = properties.sliderCount || 0;
properties.values.forEach((value, i) => {
if (i < activeCount) {
this.drawSlider(ctx, value, i);
}
});
this.drawSliderCountControls(ctx);
}
drawSlider(ctx, value, index) {
const y = this.topMargin + index * (this.sliderHeight + this.sliderSpacing);
ctx.fillStyle = this.colors.sliderBackground;
this.drawRoundRect(ctx, this.margin, y, this.sliderWidth, this.sliderHeight, 3);
let progress = (value - this.node.properties.min) / (this.node.properties.max - this.node.properties.min);
progress = Math.min(Math.max(progress, 0), 1);
const progressWidth = Math.max(0, Math.min(this.sliderWidth * progress, this.sliderWidth));
const progressGradient = ctx.createLinearGradient(this.margin, y, this.margin + this.sliderWidth, y);
progressGradient.addColorStop(0, this.colors.sliderProgress[0]);
progressGradient.addColorStop(1, this.colors.sliderProgress[1]);
ctx.fillStyle = progressGradient;
this.drawRoundRect(ctx, this.margin, y, progressWidth, this.sliderHeight, 3);
const handleX = this.margin + progressWidth;
const handleY = y + this.sliderHeight / 2;
const isHovering = this.currentHoverSlider === index;
const isActive = this.isPressed && this.currentSlider === index;
if (isActive || isHovering) {
this.drawTooltip(ctx, handleX, handleY, value);
}
this.drawSliderHandle(ctx, progress, y, index, value);
}
drawSliderHandle(ctx, progress, y, index, value) {
const handleX = this.margin + this.sliderWidth * progress;
const handleY = y + this.sliderHeight / 2;
const isHovering = this.isHoveringHandle(handleX, handleY, index);
const isActive = this.isPressed && this.currentSlider === index;
let handleRadius = this.handleRadius.normal;
if (isActive) {
handleRadius = this.handleRadius.active;
} else if (isHovering) {
handleRadius = this.handleRadius.hover;
}
ctx.fillStyle = this.colors.handle;
ctx.beginPath();
ctx.arc(handleX, handleY, handleRadius, 0, 2 * Math.PI);
ctx.fill();
}
drawTooltip(ctx, x, y, value) {
const text = value.toFixed(this.node.properties.decimals);
const padding = 6;
ctx.font = `10px ${this.fonts.primary}`;
const textWidth = ctx.measureText(text).width;
const tooltipWidth = textWidth + (padding * 2);
const tooltipHeight = 20;
let tooltipX = x - tooltipWidth / 2;
const tooltipY = y - 30;
tooltipX = Math.max(0, Math.min(tooltipX, this.node.size[0] - tooltipWidth));
ctx.fillStyle = this.colors.tooltip;
this.drawRoundRect(ctx, tooltipX, tooltipY, tooltipWidth, tooltipHeight, 4);
ctx.fillStyle = "#FFFFFF";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, tooltipX + tooltipWidth / 2, tooltipY + tooltipHeight / 2);
}
drawSliderCountControls(ctx) {
const buttonY = this.node.size[1] - this.bottomMargin/2;
const buttonWidth = 10;
const buttonHeight = 10;
const buttonSpacing = 2;
const canDecrease = this.node.properties.sliderCount > 1;
ctx.fillStyle = canDecrease ? "rgba(60, 60, 60, 0.6)" : "rgba(60, 60, 60, 0.2)";
this.drawRoundRect(ctx, this.margin, buttonY, buttonWidth, buttonHeight, 3);
ctx.fillStyle = canDecrease ? "white" : "rgba(255, 255, 255, 0.4)";
ctx.font = "bold 8px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("-", this.margin + buttonWidth / 2, buttonY + buttonHeight / 2);
const canIncrease = this.node.properties.sliderCount < 10;
ctx.fillStyle = canIncrease ? "rgba(60, 60, 60, 0.6)" : "rgba(60, 60, 60, 0.2)";
this.drawRoundRect(ctx, this.margin + buttonWidth + buttonSpacing, buttonY, buttonWidth, buttonHeight, 3);
ctx.fillStyle = canIncrease ? "white" : "rgba(255, 255, 255, 0.4)";
ctx.fillText("+", this.margin + buttonWidth + buttonSpacing + buttonWidth / 2, buttonY + buttonHeight / 2);
const precisionX = this.margin + this.sliderWidth - 30;
ctx.fillStyle = "rgba(60, 60, 60, 0.6)";
this.drawRoundRect(ctx, precisionX, buttonY, 30, buttonHeight, 3);
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.font = "7px Arial";
const precisionText = "1." + "0".repeat(this.node.properties.decimals);
ctx.fillText(precisionText, precisionX + 15, buttonY + buttonHeight / 2);
}
onMouseDown(e, pos) {
if (this.node.flags.collapsed) return false;
if (this.checkSliderInteraction(pos)) return true;
return this.checkSliderCountButtons(pos);
}
checkSliderInteraction(pos) {
const { properties } = this.node;
const hitAreaHeight = 20;
for (let i = 0; i < properties.values.length; i++) {
const y = this.topMargin + i * (this.sliderHeight + this.sliderSpacing);
if (pos[1] >= y - hitAreaHeight / 2 && pos[1] <= y + hitAreaHeight / 2) {
if (pos[0] >= this.margin && pos[0] <= this.margin + this.sliderWidth) {
this.currentSlider = i;
this.isPressed = true;
this.updateSliderValue(pos);
return true;
}
}
}
return false;
}
checkSliderCountButtons(pos) {
const buttonY = this.node.size[1] - this.bottomMargin/2;
const buttonWidth = 10;
const buttonHeight = 10;
const buttonSpacing = 2;
if (pos[1] >= buttonY && pos[1] <= buttonY + buttonHeight) {
if (pos[0] >= this.margin && pos[0] <= this.margin + buttonWidth) {
if (this.node.properties.sliderCount > 1) {
this.updateSliderCount(this.node.properties.sliderCount - 1);
return true;
}
}
else if (pos[0] >= this.margin + buttonWidth + buttonSpacing &&
pos[0] <= this.margin + buttonWidth * 2 + buttonSpacing) {
if (this.node.properties.sliderCount < 10) {
this.updateSliderCount(this.node.properties.sliderCount + 1);
return true;
}
}
else {
const precisionX = this.margin + this.sliderWidth - 30;
if (pos[0] >= precisionX && pos[0] <= precisionX + 30) {
let newDecimals = (this.node.properties.decimals % 3) + 1;
this.node.properties.decimals = newDecimals;
this._syncPrecisionStateAndVisuals(newDecimals);
return true;
}
}
}
return false;
}
_syncPrecisionStateAndVisuals(newDecimals) {
this.node.widgets.forEach(w => {
if (w?.options) {
w.options.precision = newDecimals;
}
});
this.updateAllValues();
this.node.setDirtyCanvas(true, true);
}
onMouseMove(e, pos) {
if (e.buttons === 0 && this.isPressed) {
this.currentSlider = undefined;
this.isPressed = false;
}
if (!this.isPressed) {
const { properties } = this.node;
const hitAreaHeight = 20;
let found = false;
for (let i = 0; i < properties.values.length; i++) {
const y = this.topMargin + i * (this.sliderHeight + this.sliderSpacing);
if (pos[1] >= y - hitAreaHeight / 2 && pos[1] <= y + hitAreaHeight / 2) {
this.currentHoverSlider = i;
found = true;
this.node.setDirtyCanvas(true);
break;
}
}
if (!found) {
this.currentHoverSlider = undefined;
this.node.setDirtyCanvas(true);
}
return false;
}
if (this.currentSlider !== undefined) {
this.updateSliderValue(pos);
return true;
}
return false;
}
onMouseLeave() {
this.currentSlider = undefined;
this.isPressed = false;
this.currentHoverSlider = undefined;
this.node.setDirtyCanvas(true);
}
onMouseUp() {
this.currentSlider = undefined;
this.isPressed = false;
this.currentHoverSlider = undefined;
this.node.setDirtyCanvas(true);
return true;
}
updateSliderValue(pos) {
if (this.currentSlider === undefined) return;
const { properties } = this.node;
const relativeX = Math.min(Math.max(pos[0] - this.margin, 0), this.sliderWidth);
const t = relativeX / this.sliderWidth;
let value = properties.min + (properties.max - properties.min) * t;
value = Math.min(Math.max(value, properties.min), properties.max);
if (properties.snap) {
value = Math.round(value / properties.step) * properties.step;
}
this.updateValue(this.currentSlider, value);
this.node.setDirtyCanvas(true);
}
drawRoundRect(ctx, x, y, width, height, radius, stroke = false) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.arcTo(x + width, y, x + width, y + height, radius);
ctx.arcTo(x + width, y + height, x, y + height, radius);
ctx.arcTo(x, y + height, x, y, radius);
ctx.arcTo(x, y, x + width, y, radius);
ctx.closePath();
if (stroke) {
ctx.strokeStyle = "#555";
ctx.lineWidth = 1;
ctx.stroke();
} else {
ctx.fillStyle = ctx.fillStyle || this.colors.sliderBackground;
ctx.fill();
}
}
isHoveringHandle(handleX, handleY, sliderIndex) {
if (!this.node.mouse) return false;
const distance = Math.sqrt(
Math.pow(this.node.mouse[0] - handleX, 2) +
Math.pow(this.node.mouse[1] - handleY, 2)
);
return distance < this.handleRadius.hover && this.currentHoverSlider === sliderIndex;
}
updateColors(startColor, endColor) {
this.colors.sliderProgress = [startColor, endColor];
if (endColor) {
const rgbResult = this.hexToRgb(endColor);
if (rgbResult) {
this.colors.tooltip = `rgba(${rgbResult}, 0.9)`;
}
}
this.node.setDirtyCanvas(true);
}
hexToRgb(hex) {
if (!hex) return null;
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}` : null;
}
updateSliderCount(count) {
count = Math.round(count);
count = Math.max(1, Math.min(10, count));
if (this.node.properties.sliderCount === count) {
return;
}
this.rebuildStructure(count, this.node.properties.values);
}
onResize() {
this.calculateConstants();
this.node.setDirtyCanvas(true, true);
}
updateAllValues() {
const activeCount = this.node.properties.sliderCount || 0;
this.node.properties.values.forEach((value, index) => {
if (index < activeCount) {
const roundedValue = Number(value.toFixed(this.node.properties.decimals));
this.updateValue(index, roundedValue);
}
});
this.node.setDirtyCanvas(true, true);
}
rebuildStructure(count, existingValues = []) {
count = Math.round(count);
count = Math.max(1, Math.min(10, count));
if (this.node.widgets) {
this.node.widgets.length = 0;
}
this.node.widgets = this.node.widgets || [];
const newValues = existingValues.slice(0, count);
while (newValues.length < count) {
let defaultValue = 0;
const min = this.node.properties.min ?? -100;
const max = this.node.properties.max ?? 100;
if (min > 0 || max < 0) {
defaultValue = min + (max - min) / 2;
}
newValues.push(defaultValue);
}
this.node.properties.values = newValues;
while (this.node.outputs && this.node.outputs.length > count) {
this.node.removeOutput(this.node.outputs.length - 1);
}
while (!this.node.outputs || this.node.outputs.length < count) {
const outputIndex = this.node.outputs ? this.node.outputs.length : 0;
this.node.addOutput("", "FLOAT");
}
for (let i = 0; i < count; i++) {
const widgetName = `value${i + 1}`;
const widget = this.node.addWidget(
"number",
widgetName,
this.node.properties.values[i],
(v) => {
const currentIndex = this.node.widgets.findIndex(w => w.name === widgetName);
if (currentIndex !== -1 && currentIndex < this.node.properties.sliderCount) {
this.updateValue(currentIndex, v);
}
},
{
precision: this.node.properties.decimals,
min: this.node.properties.min,
max: this.node.properties.max,
step: this.node.properties.step,
hidden: true
}
);
widget.type = "number";
widget.computeSize = () => [0, -4];
widget.hidden = true;
Object.defineProperty(widget, "height", { get: () => 0, set: () => {} });
}
this.node.properties.sliderCount = count;
this.updateAllValues();
this.calculateConstants();
this.node.setDirtyCanvas(true, true);
}
updateWidgetOptions() {
if (!this.node.widgets) return;
const options = {
precision: this.node.properties.decimals,
min: this.node.properties.min,
max: this.node.properties.max,
step: this.node.properties.step
};
this.node.widgets.forEach(w => {
if (w.type === 'number' && w.options) {
Object.assign(w.options, options);
}
});
}
}
app.registerExtension({
name: "MultiFloat",
nodeType: "MultiFloat",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MultiFloat") {
nodeType.properties = {
startColor: { type: "color", default: "#4A90E2" },
slidercolor: { type: "color", default: "#D0021B" },
sliderCount: { type: "number", default: 3, min: 1, max: 10, step: 1 },
decimals: { type: "number", default: 1, min: 1, max: 3, step: 1 },
values: { type: "array", default: [0, 0, 0] },
min: { type: "number", default: -100 },
max: { type: "number", default: 100 },
step: { type: "number", default: 0.1 },
snap: { type: "boolean", default: false },
};
Object.assign(nodeType.prototype, {
computeSize() {
const count = this.properties?.sliderCount || 3;
const topMargin = 12;
const bottomMargin = 50;
const sliderHeight = 4;
const sliderSpacing = 16;
const height = topMargin + (count * (sliderHeight + sliderSpacing)) + bottomMargin;
const minHeight = 50;
let width = 240;
if (this.size && Array.isArray(this.size) && this.size.length >= 2) {
width = this.size[0];
}
return [width, Math.max(minHeight, height)];
},
onSerialize(o) {
if (this.multiFloat && this.properties) {
o.multiFloatState = {
values: this.properties.values || [],
sliderCount: this.properties.sliderCount || 3,
startColor: this.properties.startColor,
slidercolor: this.properties.slidercolor,
decimals: this.properties.decimals,
min: this.properties.min,
max: this.properties.max,
step: this.properties.step,
snap: this.properties.snap
};
}
},
onConfigure(config) {
if (!this.multiFloat) {
this.multiFloat = new MultiFloatNode(this);
} else {
}
const state = config.multiFloatState;
let loadedCount = 3;
let loadedValues = [0,0,0];
if (state) {
this.properties = this.properties || {};
loadedValues = state.values || [0, 0, 0];
loadedCount = state.sliderCount || 3;
Object.assign(this.properties, {
values: loadedValues,
sliderCount: loadedCount,
startColor: state.startColor || nodeType.properties.startColor.default,
slidercolor: state.slidercolor || nodeType.properties.slidercolor.default,
decimals: state.decimals ?? nodeType.properties.decimals.default,
min: state.min ?? nodeType.properties.min.default,
max: state.max ?? nodeType.properties.max.default,
step: state.step ?? nodeType.properties.step.default,
snap: state.snap ?? nodeType.properties.snap.default,
});
} else {
loadedCount = nodeType.properties.sliderCount.default;
loadedValues = Array(loadedCount).fill(0);
this.properties = this.properties || {};
Object.assign(this.properties, {
values: loadedValues,
sliderCount: loadedCount,
startColor: nodeType.properties.startColor.default,
slidercolor: nodeType.properties.slidercolor.default,
decimals: nodeType.properties.decimals.default,
min: nodeType.properties.min.default,
max: nodeType.properties.max.default,
step: nodeType.properties.step.default,
snap: nodeType.properties.snap.default,
});
}
this.multiFloat.rebuildStructure(loadedCount, loadedValues);
this.multiFloat.updateColors(this.properties.startColor, this.properties.slidercolor);
if (config.size && Array.isArray(config.size) && config.size.length >= 2) {
this.size = [config.size[0], config.size[1]];
} else {
const [width, height] = this.computeSize();
this.size = [width, height];
}
this.setDirtyCanvas(true, true);
},
onNodeCreated() {
if (!this.multiFloat) {
this.multiFloat = new MultiFloatNode(this);
this.outputs = this.outputs || [];
if (!this.graph || !this.graph._configuring) {
const initialCount = this.properties?.sliderCount || nodeType.properties.sliderCount.default;
const initialValues = this.properties?.values || Array(initialCount).fill(0);
this.properties = this.properties || {};
Object.assign(this.properties, {
values: initialValues,
sliderCount: initialCount,
startColor: nodeType.properties.startColor.default,
slidercolor: nodeType.properties.slidercolor.default,
decimals: nodeType.properties.decimals.default,
min: nodeType.properties.min.default,
max: nodeType.properties.max.default,
step: nodeType.properties.step.default,
snap: nodeType.properties.snap.default,
});
this.multiFloat.rebuildStructure(initialCount, initialValues);
const [width, height] = this.computeSize();
this.size = [width, height];
}
}
if (this.multiFloat) {
this.onDrawForeground = this.multiFloat.onDrawForeground.bind(this.multiFloat);
}
},
onPropertyChanged(property, value, prevValue) {
try {
if (!this.multiFloat || !this.properties) return;
if (property === "startColor" || property === "slidercolor") {
this.multiFloat.updateColors(this.properties.startColor, this.properties.slidercolor);
} else if (property === "sliderCount") {
const newCount = Math.round(value);
if (newCount !== this.properties.sliderCount) {
this.properties.sliderCount = newCount;
this.multiFloat.rebuildStructure(newCount, this.properties.values);
const [width, height] = this.computeSize();
this.size = [width, height];
this.setDirtyCanvas(true, true);
}
} else if (property === "decimals") {
const newDecimals = Math.round(value);
if (newDecimals !== this.properties.decimals) {
this.properties.decimals = newDecimals;
this.multiFloat._syncPrecisionStateAndVisuals(newDecimals);
this.multiFloat.updateAllValues();
}
} else if (["min", "max", "step"].includes(property)) {
this.properties[property] = value;
this.multiFloat.updateWidgetOptions();
this.multiFloat.updateAllValues();
} else if (property === "snap") {
this.properties.snap = value;
this.multiFloat.updateAllValues();
}
this.setDirtyCanvas(true, true);
} catch (error) {
console.warn(`MultiFloat property change error (${property}):`, error);
}
},
onMouseDown(e, pos) {
try {
return this.multiFloat?.onMouseDown(e, pos);
} catch (error) {
console.warn("MultiFloat mouse down error:", error);
return false;
}
},
onMouseMove(e, pos) {
try {
return this.multiFloat?.onMouseMove(e, pos);
} catch (error) {
console.warn("MultiFloat mouse move error:", error);
return false;
}
},
onMouseUp(e, pos) {
try {
return this.multiFloat?.onMouseUp(e, pos);
} catch (error) {
console.warn("MultiFloat mouse up error:", error);
return true;
}
},
onMouseLeave(e) {
try {
return this.multiFloat?.onMouseLeave(e);
} catch (error) {
console.warn("MultiFloat mouse leave error:", error);
}
},
onResize(size) {
if (this.multiFloat) {
this.multiFloat.calculateConstants();
this.multiFloat.node.setDirtyCanvas(true, true);
}
}
});
}
}
});