Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Includes 30 custom nodes committed directly, 7 Civitai-exclusive loras stored via Git LFS, and a setup script that installs all dependencies and downloads HuggingFace-hosted models on vast.ai. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
460 lines
16 KiB
JavaScript
460 lines
16 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { readLinkedNumber, getDrawColor, computeCanvasSize } from "./common.js";
|
|
function showPreviewCanvas(node, app) {
|
|
|
|
const widget = {
|
|
type: "customCanvas",
|
|
name: "mask-rect-area-canvas",
|
|
get value() {
|
|
return this.canvas.value;
|
|
},
|
|
set value(x) {
|
|
this.canvas.value = x;
|
|
},
|
|
draw: function (ctx, node, widgetWidth, widgetY) {
|
|
|
|
// If we are initially offscreen when created we wont have received a resize event
|
|
// Calculate it here instead
|
|
if (!node.canvasHeight) {
|
|
computeCanvasSize(node, node.size, 220, 240);
|
|
}
|
|
|
|
const visible = true;
|
|
const t = ctx.getTransform();
|
|
const margin = 12;
|
|
const border = 2;
|
|
const widgetHeight = node.canvasHeight;
|
|
|
|
// Keep preview in sync when inputs are driven by links.
|
|
syncLinkedInputsToPropertiesAdvanced(node);
|
|
|
|
const width = Math.max(1, Math.round(node.properties["width"]));
|
|
const height = Math.max(1, Math.round(node.properties["height"]));
|
|
const scale = Math.min(
|
|
(widgetWidth - margin * 3) / width,
|
|
(widgetHeight - margin * 3) / height
|
|
);
|
|
const blurRadius = node.properties["blur_radius"] || 0;
|
|
const index = 0;
|
|
|
|
Object.assign(this.canvas.style, {
|
|
left: `${t.e}px`,
|
|
top: `${t.f + (widgetY * t.d)}px`,
|
|
width: `${widgetWidth * t.a}px`,
|
|
height: `${widgetHeight * t.d}px`,
|
|
position: "absolute",
|
|
zIndex: 1,
|
|
fontSize: `${t.d * 10.0}px`,
|
|
pointerEvents: "none"
|
|
});
|
|
|
|
this.canvas.hidden = !visible;
|
|
|
|
let backgroundWidth = width * scale;
|
|
let backgroundHeight = height * scale;
|
|
|
|
let xOffset = margin;
|
|
if (backgroundWidth < widgetWidth) {
|
|
xOffset += (widgetWidth - backgroundWidth) / 2 - margin;
|
|
}
|
|
let yOffset = (margin / 2);
|
|
if (backgroundHeight < widgetHeight) {
|
|
yOffset += (widgetHeight - backgroundHeight) / 2 - margin;
|
|
}
|
|
|
|
let widgetX = xOffset;
|
|
widgetY = widgetY + yOffset;
|
|
|
|
// Draw the background border
|
|
ctx.fillStyle = globalThis.LiteGraph.WIDGET_OUTLINE_COLOR;
|
|
ctx.fillRect(widgetX - border, widgetY - border, backgroundWidth + border * 2, backgroundHeight + border * 2)
|
|
|
|
// Draw the main background area
|
|
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR;
|
|
ctx.fillRect(widgetX, widgetY, backgroundWidth, backgroundHeight);
|
|
|
|
// Draw the conditioning zone
|
|
let [x, y, w, h] = getDrawArea(node, backgroundWidth, backgroundHeight);
|
|
|
|
ctx.fillStyle = getDrawColor(0, "80");
|
|
ctx.fillRect(widgetX + x, widgetY + y, w, h);
|
|
ctx.beginPath();
|
|
ctx.lineWidth = 1;
|
|
|
|
// Draw grid lines
|
|
for (let x = 0; x <= width / 64; x += 1) {
|
|
ctx.moveTo(widgetX + x * 64 * scale, widgetY);
|
|
ctx.lineTo(widgetX + x * 64 * scale, widgetY + backgroundHeight);
|
|
}
|
|
|
|
for (let y = 0; y <= height / 64; y += 1) {
|
|
ctx.moveTo(widgetX, widgetY + y * 64 * scale);
|
|
ctx.lineTo(widgetX + backgroundWidth, widgetY + y * 64 * scale);
|
|
}
|
|
|
|
ctx.strokeStyle = "#66666650";
|
|
ctx.stroke();
|
|
ctx.closePath();
|
|
|
|
// Draw current zone
|
|
let [sx, sy, sw, sh] = getDrawArea(node, backgroundWidth, backgroundHeight);
|
|
|
|
ctx.fillStyle = getDrawColor(0, "80");
|
|
ctx.fillRect(widgetX + sx, widgetY + sy, sw, sh);
|
|
|
|
ctx.fillStyle = getDrawColor(0, "40");
|
|
ctx.fillRect(widgetX + sx + border, widgetY + sy + border, sw - border * 2, sh - border * 2);
|
|
|
|
// Draw white border around the current zone
|
|
ctx.strokeStyle = globalThis.LiteGraph.NODE_SELECTED_TITLE_COLOR;
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeRect(widgetX + sx, widgetY + sy, sw, sh);
|
|
|
|
// Display
|
|
ctx.beginPath();
|
|
|
|
ctx.arc(LiteGraph.NODE_SLOT_HEIGHT * 0.5, LiteGraph.NODE_SLOT_HEIGHT * (index + 0.5) + 4, 4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeStyle = "white";
|
|
ctx.stroke();
|
|
|
|
ctx.lineWidth = 1;
|
|
ctx.closePath();
|
|
|
|
// Draw progress bar canvas
|
|
if (backgroundWidth < widgetWidth) {
|
|
xOffset += (widgetWidth - backgroundWidth) / 2 - margin;
|
|
}
|
|
|
|
// Adjust X and Y coordinates
|
|
const barHeight = 8;
|
|
let widgetYBar = widgetY + backgroundHeight + margin;
|
|
|
|
// Draw the border around the progress bar
|
|
ctx.fillStyle = globalThis.LiteGraph.WIDGET_OUTLINE_COLOR;
|
|
ctx.fillRect(
|
|
widgetX - border,
|
|
widgetYBar - border,
|
|
backgroundWidth + border * 2,
|
|
barHeight + border * 2
|
|
);
|
|
|
|
// Draw the main bar area (background)
|
|
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR;
|
|
ctx.fillRect(
|
|
widgetX,
|
|
widgetYBar,
|
|
backgroundWidth,
|
|
barHeight
|
|
);
|
|
|
|
// Draw progress bar grid
|
|
ctx.beginPath();
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeStyle = "#66666650";
|
|
|
|
// Calculate the number of grid lines based on the bar size
|
|
const numLines = Math.floor(backgroundWidth / 64);
|
|
|
|
// Draw grid lines
|
|
for (let x = 0; x <= width / 64; x += 1) {
|
|
ctx.moveTo(widgetX + x * 64 * scale, widgetYBar);
|
|
ctx.lineTo(widgetX + x * 64 * scale, widgetYBar + barHeight);
|
|
}
|
|
ctx.stroke();
|
|
ctx.closePath();
|
|
|
|
// Draw progress (based on blur_radius)
|
|
const progress = Math.min(blurRadius / 255, 1);
|
|
ctx.fillStyle = "rgba(0, 120, 255, 0.5)";
|
|
|
|
ctx.fillRect(
|
|
widgetX,
|
|
widgetYBar,
|
|
backgroundWidth * progress,
|
|
barHeight
|
|
);
|
|
}
|
|
};
|
|
|
|
widget.canvas = document.createElement("canvas");
|
|
widget.canvas.className = "mask-rect-area-canvas";
|
|
widget.parent = node;
|
|
|
|
widget.computeLayoutSize = function (node) {
|
|
return {
|
|
minHeight: 200,
|
|
maxHeight: 300
|
|
};
|
|
};
|
|
|
|
document.body.appendChild(widget.canvas);
|
|
node.addCustomWidget(widget);
|
|
|
|
app.canvas.onDrawBackground = function () {
|
|
// Draw node isnt fired once the node is off the screen
|
|
// if it goes off screen quickly, the input may not be removed
|
|
// this shifts it off screen so it can be moved back if the node is visible.
|
|
for (let n in app.graph._nodes) {
|
|
n = app.graph._nodes[n];
|
|
for (let w in n.widgets) {
|
|
let wid = n.widgets[w];
|
|
if (Object.hasOwn(wid, "canvas")) {
|
|
wid.canvas.style.left = -8000 + "px";
|
|
wid.canvas.style.position = "absolute";
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
node.onResize = function (size) {
|
|
computeCanvasSize(node, size, 220, 240);
|
|
};
|
|
|
|
return {minWidth: 200, minHeight: 200, widget};
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "drltdata.MaskRectAreaAdvanced",
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeData.name !== "MaskRectAreaAdvanced") {
|
|
return;
|
|
}
|
|
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
|
|
|
this.setProperty("width", 512);
|
|
this.setProperty("height", 512);
|
|
this.setProperty("x", 0);
|
|
this.setProperty("y", 0);
|
|
this.setProperty("w", 256);
|
|
this.setProperty("h", 256);
|
|
this.setProperty("blur_radius", 0);
|
|
|
|
this.selected = false;
|
|
this.index = 3;
|
|
this.serialize_widgets = true;
|
|
|
|
// If the node already provides widgets from Python/ComfyUI, do NOT recreate them
|
|
const hasExisting = Array.isArray(this.widgets) && this.widgets.some(w => w && w.name === "x");
|
|
|
|
// Helper: attach callbacks to existing widgets to keep node.properties in sync (canvas preview).
|
|
const hookWidget = (node, widgetName, propName, opts) => {
|
|
if (!Array.isArray(node.widgets)) {
|
|
return;
|
|
}
|
|
const w = node.widgets.find(ww => ww && ww.name === widgetName);
|
|
if (!w) {
|
|
return;
|
|
}
|
|
|
|
const min = (opts && typeof opts.min === "number") ? opts.min : undefined;
|
|
const max = (opts && typeof opts.max === "number") ? opts.max : undefined;
|
|
const step = (opts && typeof opts.step === "number") ? opts.step : undefined;
|
|
|
|
if (node.properties && Object.prototype.hasOwnProperty.call(node.properties, propName)) {
|
|
w.value = node.properties[propName];
|
|
} else {
|
|
node.properties[propName] = w.value;
|
|
}
|
|
|
|
const prevCb = w.callback;
|
|
w.callback = function (v, ...args) {
|
|
let val = v;
|
|
if (typeof val === "number") {
|
|
if (typeof step === "number" && step > 0) {
|
|
const s = step / 10;
|
|
val = Math.round(val / s) * s;
|
|
} else {
|
|
val = Math.round(val);
|
|
}
|
|
if (typeof min === "number") {
|
|
val = Math.max(min, val);
|
|
}
|
|
if (typeof max === "number") {
|
|
val = Math.min(max, val);
|
|
}
|
|
}
|
|
this.value = val;
|
|
node.properties[propName] = val;
|
|
if (prevCb) {
|
|
return prevCb.call(this, val, ...args);
|
|
}
|
|
};
|
|
};
|
|
|
|
if (hasExisting) {
|
|
hookWidget(this, "x", "x", {"step": 10});
|
|
hookWidget(this, "y", "y", {"step": 10});
|
|
hookWidget(this, "width", "w", {"step": 10});
|
|
hookWidget(this, "height", "h", {"step": 10});
|
|
hookWidget(this, "image_width", "width", {"step": 10});
|
|
hookWidget(this, "image_height", "height", {"step": 10});
|
|
hookWidget(this, "blur_radius", "blur_radius", {"min": 0, "max": 255, "step": 10});
|
|
} else {
|
|
CUSTOM_INT(this, "x", 0, function (v, _, node) {
|
|
const s = this.options.step / 10;
|
|
this.value = Math.round(v / s) * s;
|
|
node.properties["x"] = this.value;
|
|
});
|
|
CUSTOM_INT(this, "y", 0, function (v, _, node) {
|
|
const s = this.options.step / 10;
|
|
this.value = Math.round(v / s) * s;
|
|
node.properties["y"] = this.value;
|
|
});
|
|
CUSTOM_INT(this, "width", 256, function (v, _, node) {
|
|
const s = this.options.step / 10;
|
|
this.value = Math.round(v / s) * s;
|
|
node.properties["w"] = this.value;
|
|
});
|
|
CUSTOM_INT(this, "height", 256, function (v, _, node) {
|
|
const s = this.options.step / 10;
|
|
this.value = Math.round(v / s) * s;
|
|
node.properties["h"] = this.value;
|
|
});
|
|
CUSTOM_INT(this, "image_width", 512, function (v, _, node) {
|
|
const s = this.options.step / 10;
|
|
this.value = Math.round(v / s) * s;
|
|
node.properties["width"] = this.value;
|
|
});
|
|
CUSTOM_INT(this, "image_height", 512, function (v, _, node) {
|
|
const s = this.options.step / 10;
|
|
this.value = Math.round(v / s) * s;
|
|
node.properties["height"] = this.value;
|
|
});
|
|
CUSTOM_INT(this, "blur_radius", 0, function (v, _, node) {
|
|
this.value = Math.round(v) || 0;
|
|
node.properties["blur_radius"] = this.value;
|
|
},
|
|
{"min": 0, "max": 255, "step": 10}
|
|
);
|
|
}
|
|
|
|
showPreviewCanvas(this, app);
|
|
|
|
this.onSelected = function () {
|
|
this.selected = true;
|
|
};
|
|
this.onDeselected = function () {
|
|
this.selected = false;
|
|
};
|
|
|
|
return r;
|
|
};
|
|
}
|
|
});
|
|
|
|
// Calculate the drawing area using individual properties.
|
|
function getDrawArea(node, backgroundWidth, backgroundHeight) {
|
|
let x = node.properties["x"] * backgroundWidth / node.properties["width"];
|
|
let y = node.properties["y"] * backgroundHeight / node.properties["height"];
|
|
let w = node.properties["w"] * backgroundWidth / node.properties["width"];
|
|
let h = node.properties["h"] * backgroundHeight / node.properties["height"];
|
|
|
|
if (x > backgroundWidth) {
|
|
x = backgroundWidth;
|
|
}
|
|
if (y > backgroundHeight) {
|
|
y = backgroundHeight;
|
|
}
|
|
|
|
if (x + w > backgroundWidth) {
|
|
w = Math.max(0, backgroundWidth - x);
|
|
}
|
|
|
|
if (y + h > backgroundHeight) {
|
|
h = Math.max(0, backgroundHeight - y);
|
|
}
|
|
|
|
return [x, y, w, h];
|
|
}
|
|
|
|
function CUSTOM_INT(node, inputName, val, func, config = {}) {
|
|
return {
|
|
widget: node.addWidget(
|
|
"number",
|
|
inputName,
|
|
val,
|
|
func,
|
|
Object.assign({}, {min: 0, max: 4096, step: 640, precision: 0}, config)
|
|
)
|
|
};
|
|
}
|
|
|
|
function syncLinkedInputsToPropertiesAdvanced(node) {
|
|
let changed = false;
|
|
|
|
const vx = readLinkedNumber(node, "x");
|
|
if (vx != null) {
|
|
const nv = Math.max(0, Math.round(vx));
|
|
if (node.properties["x"] !== nv) {
|
|
node.properties["x"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
const vy = readLinkedNumber(node, "y");
|
|
if (vy != null) {
|
|
const nv = Math.max(0, Math.round(vy));
|
|
if (node.properties["y"] !== nv) {
|
|
node.properties["y"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
// Input "width" is the rectangle width in px -> property "w"
|
|
const vw = readLinkedNumber(node, "width");
|
|
if (vw != null) {
|
|
const nv = Math.max(0, Math.round(vw));
|
|
if (node.properties["w"] !== nv) {
|
|
node.properties["w"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
// Input "height" is the rectangle height in px -> property "h"
|
|
const vh = readLinkedNumber(node, "height");
|
|
if (vh != null) {
|
|
const nv = Math.max(0, Math.round(vh));
|
|
if (node.properties["h"] !== nv) {
|
|
node.properties["h"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
// Image size (must be >=1 to avoid division by zero in getDrawArea)
|
|
const viw = readLinkedNumber(node, "image_width");
|
|
if (viw != null) {
|
|
const nv = Math.max(1, Math.round(viw));
|
|
if (node.properties["width"] !== nv) {
|
|
node.properties["width"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
const vih = readLinkedNumber(node, "image_height");
|
|
if (vih != null) {
|
|
const nv = Math.max(1, Math.round(vih));
|
|
if (node.properties["height"] !== nv) {
|
|
node.properties["height"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
const vbr = readLinkedNumber(node, "blur_radius");
|
|
if (vbr != null) {
|
|
const nv = Math.max(0, Math.min(255, Math.round(vbr)));
|
|
if (node.properties["blur_radius"] !== nv) {
|
|
node.properties["blur_radius"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|