From e06fe71f888a028b695cf9b8eb7a2865c476d28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Zs=C3=B6g=C3=B6n?= <10298071+andreszs@users.noreply.github.com> Date: Fri, 26 Dec 2025 17:50:00 -0300 Subject: [PATCH] Enable INT node inputs and fix live preview sync for MaskRectArea nodes ## 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. --- js/mask-rect-area-advanced.js | 258 ++++++++++++++++++++++++----- js/mask-rect-area.js | 302 ++++++++++++++++++++++++++++++---- modules/impact/impact_pack.py | 151 +++++++++++------ 3 files changed, 588 insertions(+), 123 deletions(-) diff --git a/js/mask-rect-area-advanced.js b/js/mask-rect-area-advanced.js index 7f7e638d..2bc7ed96 100644 --- a/js/mask-rect-area-advanced.js +++ b/js/mask-rect-area-advanced.js @@ -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; - - 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); + if (nodeData.name !== "MaskRectAreaAdvanced") { + return; + } - this.selected = false; - this.index = 3; - this.serialize_widgets = true; + 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; @@ -258,19 +324,19 @@ app.registerExtension({ }, {"min": 0, "max": 255, "step": 10} ); + } - showPreviewCanvas(this, app); - - this.onSelected = function () { - this.selected = true; - }; - this.onDeselected = function () { - this.selected = false; - }; + showPreviewCanvas(this, app); - 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; +} + diff --git a/js/mask-rect-area.js b/js/mask-rect-area.js index 8605f719..11d15e43 100644 --- a/js/mask-rect-area.js +++ b/js/mask-rect-area.js @@ -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; - - 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); + if (nodeData.name !== "MaskRectArea") { + return; + } - this.selected = false; - this.index = 3; - this.serialize_widgets = true; + 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))); // 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; +} diff --git a/modules/impact/impact_pack.py b/modules/impact/impact_pack.py index 0c667869..9e25c0ba 100644 --- a/modules/impact/impact_pack.py +++ b/modules/impact/impact_pack.py @@ -2157,6 +2157,12 @@ def __init__(self): 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 @@ def INPUT_TYPES(cls): 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 @@ def __init__(self): 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 @@ def INPUT_TYPES(cls): 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