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