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>
495 lines
17 KiB
JavaScript
495 lines
17 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, 200, 200);
|
|
}
|
|
|
|
const visible = true;
|
|
const t = ctx.getTransform();
|
|
const margin = 12;
|
|
const border = 2;
|
|
const widgetHeight = node.canvasHeight;
|
|
const width = 512;
|
|
const height = 512;
|
|
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);
|
|
|
|
// Keep preview in sync when inputs are driven by links.
|
|
syncLinkedInputsToProperties(node);
|
|
|
|
// 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;
|
|
}
|
|
|
|
const barHeight = 8;
|
|
let widgetYBar = widgetY + backgroundHeight + margin;
|
|
|
|
// Draw progress bar border
|
|
ctx.fillStyle = globalThis.LiteGraph.WIDGET_OUTLINE_COLOR;
|
|
ctx.fillRect(
|
|
widgetX - border,
|
|
widgetYBar - border,
|
|
backgroundWidth + border * 2,
|
|
barHeight + border * 2
|
|
);
|
|
|
|
// Draw progress bar area
|
|
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR; // Mismo color de fondo que el canvas
|
|
ctx.fillRect(
|
|
widgetX,
|
|
widgetYBar,
|
|
backgroundWidth,
|
|
barHeight
|
|
);
|
|
|
|
// Draw progress bar grid
|
|
ctx.beginPath();
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeStyle = "#66666650";
|
|
|
|
// Determine max lines
|
|
const numLines = Math.floor(backgroundWidth / 64);
|
|
|
|
// Draw progress bar grid
|
|
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 bar
|
|
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, 200, 200);
|
|
};
|
|
|
|
return {minWidth: 200, minHeight: 200, widget};
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: 'drltdata.MaskRectArea',
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeData.name !== "MaskRectArea") {
|
|
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", 50);
|
|
this.setProperty("h", 50);
|
|
this.setProperty("blur_radius", 0);
|
|
|
|
this.selected = false;
|
|
this.index = 3;
|
|
this.serialize_widgets = true;
|
|
|
|
// If Python/ComfyUI already created typed widgets, do not recreate them (avoid duplicates).
|
|
const hasExisting = Array.isArray(this.widgets) && this.widgets.some(w => w && w.name === "x");
|
|
|
|
// Hook existing widgets to keep node.properties in sync (canvas uses properties).
|
|
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;
|
|
|
|
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") {
|
|
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) {
|
|
// Note: "width"/"height" widgets map to "w"/"h" properties (percent-based).
|
|
hookWidget(this, "x", "x", {"min": 0, "max": 100});
|
|
hookWidget(this, "y", "y", {"min": 0, "max": 100});
|
|
hookWidget(this, "width", "w", {"min": 0, "max": 100});
|
|
hookWidget(this, "height", "h", {"min": 0, "max": 100});
|
|
hookWidget(this, "blur_radius", "blur_radius", {"min": 0, "max": 255});
|
|
} else {
|
|
CUSTOM_INT(this, "x", 0, function (v, _, node) {
|
|
this.value = Math.max(0, Math.min(100, Math.round(v)));
|
|
node.properties["x"] = this.value;
|
|
});
|
|
CUSTOM_INT(this, "y", 0, function (v, _, node) {
|
|
this.value = Math.max(0, Math.min(100, Math.round(v)));
|
|
node.properties["y"] = this.value;
|
|
});
|
|
CUSTOM_INT(this, "w", 50, function (v, _, node) {
|
|
this.value = Math.max(0, Math.min(100, Math.round(v)));
|
|
node.properties["w"] = this.value;
|
|
});
|
|
CUSTOM_INT(this, "h", 50, function (v, _, node) {
|
|
this.value = Math.max(0, Math.min(100, Math.round(v)));
|
|
node.properties["h"] = 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});
|
|
|
|
// If Python widgets exist, they will be used instead; this is back-compat only.
|
|
}
|
|
|
|
showPreviewCanvas(this, app);
|
|
|
|
// Sync linked input values -> node.properties so the preview updates when driven by connections.
|
|
const prevOnExecute = this.onExecute;
|
|
this.onExecute = function () {
|
|
const rr = prevOnExecute ? prevOnExecute.apply(this, arguments) : undefined;
|
|
|
|
const readLinkedInt = (inputName) => {
|
|
if (!Array.isArray(this.inputs)) {
|
|
return null;
|
|
}
|
|
const inp = this.inputs.find(i => i && i.name === inputName);
|
|
if (!inp || !inp.link) {
|
|
return null;
|
|
}
|
|
try {
|
|
const v = this.getInputData(inputName);
|
|
return (typeof v === "number") ? v : null;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
let changed = false;
|
|
|
|
const vx = readLinkedInt("x");
|
|
if (vx != null) {
|
|
const nv = Math.max(0, Math.min(100, Math.round(vx)));
|
|
if (this.properties["x"] !== nv) {
|
|
this.properties["x"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
const vy = readLinkedInt("y");
|
|
if (vy != null) {
|
|
const nv = Math.max(0, Math.min(100, Math.round(vy)));
|
|
if (this.properties["y"] !== nv) {
|
|
this.properties["y"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
const vw = readLinkedInt("width");
|
|
if (vw != null) {
|
|
const nv = Math.max(0, Math.min(100, Math.round(vw)));
|
|
if (this.properties["w"] !== nv) {
|
|
this.properties["w"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
const vh = readLinkedInt("height");
|
|
if (vh != null) {
|
|
const nv = Math.max(0, Math.min(100, Math.round(vh)));
|
|
if (this.properties["h"] !== nv) {
|
|
this.properties["h"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
const vbr = readLinkedInt("blur_radius");
|
|
if (vbr != null) {
|
|
const nv = Math.max(0, Math.min(255, Math.round(vbr)));
|
|
if (this.properties["blur_radius"] !== nv) {
|
|
this.properties["blur_radius"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
this.setDirtyCanvas(true, true);
|
|
if (this.graph) {
|
|
this.graph.setDirtyCanvas(true, true);
|
|
}
|
|
}
|
|
|
|
return rr;
|
|
};
|
|
|
|
this.onSelected = function () {
|
|
this.selected = true;
|
|
};
|
|
this.onDeselected = function () {
|
|
this.selected = false;
|
|
};
|
|
|
|
return r;
|
|
};
|
|
}
|
|
});
|
|
|
|
|
|
// Calculate the drawing area using percentage-based properties.
|
|
function getDrawArea(node, backgroundWidth, backgroundHeight) {
|
|
// Convert percentages to actual pixel values based on the background dimensions
|
|
let x = (node.properties["x"] / 100) * backgroundWidth;
|
|
let y = (node.properties["y"] / 100) * backgroundHeight;
|
|
let w = (node.properties["w"] / 100) * backgroundWidth;
|
|
let h = (node.properties["h"] / 100) * backgroundHeight;
|
|
|
|
// Ensure the values do not exceed the background boundaries
|
|
if (x > backgroundWidth) {
|
|
x = backgroundWidth;
|
|
}
|
|
if (y > backgroundHeight) {
|
|
y = backgroundHeight;
|
|
}
|
|
|
|
// Adjust width and height to fit within the background dimensions
|
|
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: 100, step: 10, precision: 0}, config)
|
|
)
|
|
};
|
|
}
|
|
|
|
function syncLinkedInputsToProperties(node) {
|
|
let changed = false;
|
|
|
|
const vx = readLinkedNumber(node, "x");
|
|
if (vx != null) {
|
|
const nv = Math.max(0, Math.min(100, 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.min(100, Math.round(vy)));
|
|
if (node.properties["y"] !== nv) {
|
|
node.properties["y"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
const vw = readLinkedNumber(node, "width");
|
|
if (vw != null) {
|
|
const nv = Math.max(0, Math.min(100, Math.round(vw)));
|
|
if (node.properties["w"] !== nv) {
|
|
node.properties["w"] = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
const vh = readLinkedNumber(node, "height");
|
|
if (vh != null) {
|
|
const nv = Math.max(0, Math.min(100, Math.round(vh)));
|
|
if (node.properties["h"] !== nv) {
|
|
node.properties["h"] = 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;
|
|
}
|