Enable INT node inputs and fix live preview sync for MaskRectArea nodes (#1168)

## Overview

This PR improves both MaskRectArea and MaskRectAreaAdvanced nodes by allowing them to accept values from connected INT nodes, while preserving backward compatibility with existing workflows.

It also fixes several UI and preview issues that prevented the canvas from updating correctly when values were driven by links.

Fixes #1126

## Key changes

### 1. Typed INT inputs support (backend)

- Both MaskRectArea and MaskRectAreaAdvanced now declare proper INT inputs in INPUT_TYPES.
- This allows parameters such as x, y, width, height, and blur_radius to be driven directly by other nodes.
- Existing workflows that relied on node properties remain supported via fallback logic using extra_pnginfo.

### 2. Live preview correctly updates when inputs are linked (frontend)

- The canvas preview now updates immediately when values come from linked INT nodes.
- This is achieved by synchronizing linked input values into node.properties during canvas rendering, avoiding reliance on widget callbacks or execution timing.
- This directly resolves the behavior reported in issue #1126.

### 3. Node height and layout fix (frontend)

- The node height calculation was corrected to use the actual widget layout (last_y) instead of input/output counts.
- Nodes can now both grow and shrink correctly when resized or when widget content changes.
- This removes excessive empty space below the widgets and canvas.

### 4. Widget duplication prevention

- Frontend logic now detects when widgets are already created by Python and avoids recreating them in JavaScript.
- This prevents duplicated widgets and keeps the node UI consistent.

### 5. Comment cleanup

- Remaining comments in Spanish were translated to English for consistency and maintainability.

## Backward compatibility

- Existing workflows continue to work without modification.
- Default behavior and mask output remain unchanged unless inputs are explicitly linked.

## Motivation

These changes make the nodes composable with the rest of the graph (especially math and control nodes), improve the reliability of the preview, and fix UI inconsistencies without introducing breaking changes.
This commit is contained in:
Andrés Zsögön
2025-12-30 18:26:50 +09:00
committed by GitHub
parent 61bd8397a1
commit 4c864fafb0
3 changed files with 586 additions and 121 deletions
+216 -40
View File
@@ -23,9 +23,16 @@ function showPreviewCanvas(node, app) {
const margin = 12;
const border = 2;
const widgetHeight = node.canvasHeight;
const width = Math.round(node.properties["width"]);
const height = Math.round(node.properties["height"]);
const scale = Math.min((widgetWidth - margin * 3) / width, (widgetHeight - margin * 3) / height);
// 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;
@@ -120,11 +127,11 @@ function showPreviewCanvas(node, app) {
xOffset += (widgetWidth - backgroundWidth) / 2 - margin;
}
// Ajustar las coordenadas X e Y
// Adjust X and Y coordinates
const barHeight = 8;
let widgetYBar = widgetY + backgroundHeight + margin;
// Dibujar el borde negro alrededor de la barra
// Draw the border around the progress bar
ctx.fillStyle = globalThis.LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.fillRect(
widgetX - border,
@@ -133,8 +140,8 @@ function showPreviewCanvas(node, app) {
barHeight + border * 2
);
// Dibujar el área principal de la barra (fondo)
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR; // Mismo color de fondo que el canvas
// Draw the main bar area (background)
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR;
ctx.fillRect(
widgetX,
widgetYBar,
@@ -142,16 +149,15 @@ function showPreviewCanvas(node, app) {
barHeight
);
// Draw progress bar grid
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = "#66666650";
// Calcular el número de líneas en función del tamaño de la barra
// Calculate the number of grid lines based on the bar size
const numLines = Math.floor(backgroundWidth / 64);
// Dibujar líneas del grid
// 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);
@@ -159,7 +165,7 @@ function showPreviewCanvas(node, app) {
ctx.stroke();
ctx.closePath();
// Dibujar progreso (basado en blur_radius)
// Draw progress (based on blur_radius)
const progress = Math.min(blurRadius / 255, 1);
ctx.fillStyle = "rgba(0, 120, 255, 0.5)";
@@ -203,25 +209,85 @@ function showPreviewCanvas(node, app) {
}
app.registerExtension({
name: 'drltdata.MaskRectAreaAdvanced',
name: "drltdata.MaskRectAreaAdvanced",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MaskRectAreaAdvanced") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
if (nodeData.name !== "MaskRectAreaAdvanced") {
return;
}
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);
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
this.selected = false;
this.index = 3;
this.serialize_widgets = true;
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;
@@ -258,19 +324,19 @@ app.registerExtension({
},
{"min": 0, "max": 255, "step": 10}
);
}
showPreviewCanvas(this, app);
showPreviewCanvas(this, app);
this.onSelected = function () {
this.selected = true;
};
this.onDeselected = function () {
this.selected = false;
};
return r;
this.onSelected = function () {
this.selected = true;
};
}
this.onDeselected = function () {
this.selected = false;
};
return r;
};
}
});
@@ -320,7 +386,7 @@ function getDrawColor(percent, alpha) {
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
return Math.round(255 * color).toString(16).padStart(2, '0');
};
return `#${f(0)}${f(8)}${f(4)}${alpha}`;
}
@@ -333,7 +399,8 @@ function computeCanvasSize(node, size) {
const MIN_HEIGHT = 220;
const MIN_WIDTH = 240;
let y = LiteGraph.NODE_WIDGET_HEIGHT * Math.max(node.inputs.length, node.outputs.length) + 5;
// FIX: use real last_y from layout, not slot count
let y = node.widgets[0].last_y + 5;
let freeSpace = size[1] - y;
// Compute the height of all non-customCanvas widgets
@@ -352,10 +419,15 @@ function computeCanvasSize(node, size) {
// Ensure there is enough vertical space
freeSpace -= widgetHeight;
// Adjust the height of the node if needed
// Minimum canvas height clamp
if (freeSpace < MIN_HEIGHT) {
freeSpace = MIN_HEIGHT;
node.size[1] = y + widgetHeight + freeSpace;
}
// Allow the node height to grow and shrink
const targetHeight = y + widgetHeight + freeSpace;
if (node.size[1] !== targetHeight) {
node.size[1] = targetHeight;
node.graph.setDirtyCanvas(true);
}
@@ -379,3 +451,107 @@ function computeCanvasSize(node, size) {
node.canvasHeight = freeSpace;
}
// Reads a numeric value from a connected link by inspecting the origin node widget.
// This is more reliable than getInputData() in ComfyUI's frontend execution model.
function readLinkedNumber(node, inputName) {
try {
if (!node || !node.graph || !Array.isArray(node.inputs)) {
return null;
}
const inp = node.inputs.find(i => i && i.name === inputName);
if (!inp || inp.link == null) {
return null;
}
const link = node.graph.links && node.graph.links[inp.link];
if (!link) {
return null;
}
const originNode = node.graph.getNodeById ? node.graph.getNodeById(link.origin_id) : null;
if (!originNode || !Array.isArray(originNode.widgets) || originNode.widgets.length === 0) {
return null;
}
// Most "Int" nodes expose the value in the first widget named "value".
const w = originNode.widgets.find(ww => ww && ww.name === "value") || originNode.widgets[0];
const v = w ? w.value : null;
return (typeof v === "number") ? v : null;
} catch (e) {
return null;
}
}
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;
}
+269 -31
View File
@@ -64,6 +64,10 @@ function showPreviewCanvas(node, app) {
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR;
ctx.fillRect(widgetX, widgetY, backgroundWidth, backgroundHeight);
// Keep preview in sync when inputs are driven by links.
const DEBUG_PREVIEW_SYNC = false;
syncLinkedInputsToProperties(node, DEBUG_PREVIEW_SYNC);
// Draw the conditioning zone
let [x, y, w, h] = getDrawArea(node, backgroundWidth, backgroundHeight);
@@ -100,7 +104,6 @@ function showPreviewCanvas(node, app) {
ctx.strokeStyle = globalThis.LiteGraph.NODE_SELECTED_TITLE_COLOR;
ctx.lineWidth = 2;
ctx.strokeRect(widgetX + sx, widgetY + sy, sw, sh);
//ctx.strokeRect(finalSX, finalSY, finalSW, finalSH);
// Display
ctx.beginPath();
@@ -202,25 +205,82 @@ function showPreviewCanvas(node, app) {
app.registerExtension({
name: 'drltdata.MaskRectArea',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MaskRectArea") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
if (nodeData.name !== "MaskRectArea") {
return;
}
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);
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
this.selected = false;
this.index = 3;
this.serialize_widgets = true;
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))); // Limitar entre 0 y 100
this.value = Math.max(0, Math.min(100, Math.round(v)));
node.properties["x"] = this.value;
});
CUSTOM_INT(this, "y", 0, function (v, _, node) {
@@ -238,25 +298,104 @@ app.registerExtension({
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}
);
}, {"min": 0, "max": 255, "step": 10});
showPreviewCanvas(this, app);
// If Python widgets exist, they will be used instead; this is back-compat only.
}
this.onSelected = function () {
this.selected = true;
};
this.onDeselected = function () {
this.selected = false;
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;
}
};
return r;
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
@@ -305,7 +444,7 @@ function getDrawColor(percent, alpha) {
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
return Math.round(255 * color).toString(16).padStart(2, '0');
};
return `#${f(0)}${f(8)}${f(4)}${alpha}`;
}
@@ -318,7 +457,8 @@ function computeCanvasSize(node, size) {
const MIN_HEIGHT = 200;
const MIN_WIDTH = 200;
let y = LiteGraph.NODE_WIDGET_HEIGHT * Math.max(node.inputs.length, node.outputs.length) + 5;
// Use last_y from LiteGraph layout (fixes excessive node height)
let y = node.widgets[0].last_y + 5;
let freeSpace = size[1] - y;
// Compute the height of all non-customCanvas widgets
@@ -337,10 +477,15 @@ function computeCanvasSize(node, size) {
// Ensure there is enough vertical space
freeSpace -= widgetHeight;
// Adjust the height of the node if needed
// Clamp minimum canvas height
if (freeSpace < MIN_HEIGHT) {
freeSpace = MIN_HEIGHT;
node.size[1] = y + widgetHeight + freeSpace;
}
// Allow both grow and shrink to fit content
const targetHeight = y + widgetHeight + freeSpace;
if (node.size[1] !== targetHeight) {
node.size[1] = targetHeight;
node.graph.setDirtyCanvas(true);
}
@@ -364,3 +509,96 @@ function computeCanvasSize(node, size) {
node.canvasHeight = freeSpace;
}
// Reads a numeric value from a connected link by inspecting the origin node widget.
// This is more reliable than getInputData() in ComfyUI's frontend execution model.
function readLinkedNumber(node, inputName) {
try {
if (!node || !node.graph || !Array.isArray(node.inputs)) {
return null;
}
const inp = node.inputs.find(i => i && i.name === inputName);
if (!inp || inp.link == null) {
return null;
}
const link = node.graph.links && node.graph.links[inp.link];
if (!link) {
return null;
}
const originNode = node.graph.getNodeById ? node.graph.getNodeById(link.origin_id) : null;
if (!originNode || !Array.isArray(originNode.widgets) || originNode.widgets.length === 0) {
return null;
}
// Most "Int" nodes expose the value in the first widget named "value".
const w = originNode.widgets.find(ww => ww && ww.name === "value") || originNode.widgets[0];
const v = w ? w.value : null;
return (typeof v === "number") ? v : null;
} catch (e) {
return null;
}
}
function syncLinkedInputsToProperties(node, debug) {
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;
}
}
if (debug && changed) {
console.log("[MaskRectArea] preview sync from links", {
x: node.properties["x"],
y: node.properties["y"],
w: node.properties["w"],
h: node.properties["h"],
blur_radius: node.properties["blur_radius"]
});
}
return changed;
}
+101 -50
View File
@@ -2157,6 +2157,12 @@ class MaskRectArea:
def INPUT_TYPES(cls):
return {
"required": {
# Added typed INT inputs so this node can be driven by other INT nodes.
"x": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
"y": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
"width": ("INT", {"default": 50, "min": 0, "max": 100, "step": 1}),
"height": ("INT", {"default": 50, "min": 0, "max": 100, "step": 1}),
"blur_radius": ("INT", {"default": 0, "min": 0, "step": 1})
},
"hidden": {"extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"}
}
@@ -2166,34 +2172,68 @@ class MaskRectArea:
CATEGORY = "ImpactPack/Operation"
FUNCTION = "create_mask"
def create_mask(self, extra_pnginfo, unique_id, **kwargs):
# search for node
node_found = False
for node in extra_pnginfo["workflow"]["nodes"]:
if str(node["id"]) == unique_id:
min_x = node["properties"].get("x", 0) / 100
min_y = node["properties"].get("y", 0) / 100
width = node["properties"].get("w", 0) / 100
height = node["properties"].get("h", 0) / 100
blur_radius = node["properties"].get("blur_radius", 0)
node_found = True
break
def create_mask(self, x, y, width, height, blur_radius, extra_pnginfo, unique_id):
# Backward-compat: if node properties exist in workflow, prefer them.
try:
for node in extra_pnginfo["workflow"]["nodes"]:
if str(node["id"]) == str(unique_id):
props = node.get("properties", {})
x = int(props.get("x", x))
y = int(props.get("y", y))
width = int(props.get("w", width))
height = int(props.get("h", height))
blur_radius = int(props.get("blur_radius", blur_radius))
break
except Exception:
pass
if not node_found:
raise ValueError(f"No node found with unique_id {unique_id}.")
# Clamp percent inputs
if x < 0:
x = 0
if y < 0:
y = 0
if width < 0:
width = 0
if height < 0:
height = 0
if x > 100:
x = 100
if y > 100:
y = 100
if width > 100:
width = 100
if height > 100:
height = 100
# Convert percent to ratio
min_x = x / 100.0
min_y = y / 100.0
w_ratio = width / 100.0
h_ratio = height / 100.0
# Create a mask with standard resolution (e.g., 512x512)
resolution = 512
mask = torch.zeros((resolution, resolution))
mask = torch.zeros((resolution, resolution), dtype=torch.float32)
# Calculate pixel coordinates
min_x_px = int(min_x * resolution)
min_y_px = int(min_y * resolution)
max_x_px = int((min_x + width) * resolution)
max_y_px = int((min_y + height) * resolution)
max_x_px = int((min_x + w_ratio) * resolution)
max_y_px = int((min_y + h_ratio) * resolution)
# Clamp pixel bounds
if min_x_px < 0:
min_x_px = 0
if min_y_px < 0:
min_y_px = 0
if max_x_px > resolution:
max_x_px = resolution
if max_y_px > resolution:
max_y_px = resolution
# Draw the rectangle on the mask
mask[min_y_px:max_y_px, min_x_px:max_x_px] = 1
if max_x_px > min_x_px and max_y_px > min_y_px:
mask[min_y_px:max_y_px, min_x_px:max_x_px] = 1.0
# Apply blur if the radii are greater than 0
if blur_radius > 0:
@@ -2222,6 +2262,13 @@ class MaskRectAreaAdvanced:
def INPUT_TYPES(cls):
return {
"required": {
"x": ("INT", {"default": 0, "min": 0, "step": 1}),
"y": ("INT", {"default": 0, "min": 0, "step": 1}),
"width": ("INT", {"default": 256, "min": 0, "step": 1}),
"height": ("INT", {"default": 320, "min": 0, "step": 1}),
"image_width": ("INT", {"default": 512, "min": 1, "step": 1}),
"image_height": ("INT", {"default": 320, "min": 1, "step": 1}),
"blur_radius": ("INT", {"default": 0, "min": 0, "step": 1})
},
"hidden": {"extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"}
}
@@ -2231,46 +2278,50 @@ class MaskRectAreaAdvanced:
CATEGORY = "ImpactPack/Operation"
FUNCTION = "create_mask_advanced"
def create_mask_advanced(self, extra_pnginfo, unique_id, **kwargs):
# search for node
node_found = False
for node in extra_pnginfo["workflow"]["nodes"]:
if node["id"] == int(unique_id):
min_x = node["properties"]["x"]
min_y = node["properties"]["y"]
width = node["properties"]["w"]
height = node["properties"]["h"]
image_width = node["properties"]["width"]
image_height = node["properties"]["height"]
blur_radius = node["properties"]["blur_radius"]
node_found = True
break
def create_mask_advanced(self, x, y, width, height, image_width, image_height, blur_radius, extra_pnginfo, unique_id):
# Backward-compat fallback: if node properties exist in workflow, prefer them
try:
for node in extra_pnginfo["workflow"]["nodes"]:
if node["id"] == int(unique_id):
props = node.get("properties", {})
x = int(props.get("x", x))
y = int(props.get("y", y))
width = int(props.get("w", width))
height = int(props.get("h", height))
image_width = int(props.get("width", image_width))
image_height = int(props.get("height", image_height))
blur_radius = int(props.get("blur_radius", blur_radius))
break
except Exception:
pass
if not node_found:
raise ValueError(f"No node found with unique_id {unique_id}.")
# Clamp to safe bounds
if image_width < 1:
image_width = 1
if image_height < 1:
image_height = 1
if width < 0:
width = 0
if height < 0:
height = 0
if x < 0:
x = 0
if y < 0:
y = 0
# Calculate maximum coordinates
max_x = min_x + width
max_y = min_y + height
max_x = min(x + width, image_width)
max_y = min(y + height, image_height)
# Create a mask with the image dimensions
mask = torch.zeros((image_height, image_width))
mask = torch.zeros((image_height, image_width), dtype=torch.float32)
# Draw the rectangle on the mask
mask[int(min_y):int(max_y), int(min_x):int(max_x)] = 1
if max_x > x and max_y > y:
mask[y:max_y, x:max_x] = 1.0
# Apply blur if the radii are greater than 0
if blur_radius > 0:
dx = blur_radius * 2 + 1
dy = blur_radius * 2 + 1
# Convert the mask to a format compatible with OpenCV (numpy array)
k = blur_radius * 2 + 1
mask_np = mask.cpu().numpy().astype("float32")
# Apply Gaussian Blur
blurred_mask = cv2.GaussianBlur(mask_np, (dx, dy), 0)
# Convert back to tensor
blurred_mask = cv2.GaussianBlur(mask_np, (k, k), 0)
mask = torch.from_numpy(blurred_mask)
# Return the mask as a tensor with an additional channel