From acdeac3ed52411acd6bc53ce2f8fbcfe93006f8e Mon Sep 17 00:00:00 2001 From: Carasibana <247009443+Carasibana@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:23:37 -0400 Subject: [PATCH] Add custom_color_hex_input connector: overrides picker when connected, grays out picker, sanitizes invalid hex --- README.md | 3 ++- js/anchor_widget.js | 48 +++++++++++++++++++++++++++++++++++++++++++-- nodes.py | 35 ++++++++++++++++++++++++++------- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d396c99..5958a31 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ A ComfyUI custom node that resizes an image to an exact target canvas size with | `scale_method` | Dropdown | How to scale the image before filling (see below) | | `fill_method` | Dropdown | `crop` or `stretch` | | `padding_fill` | Dropdown | How to fill any gaps (see below) | -| `custom_color_hex` | Colour picker | Only shown when `padding_fill` is `custom` — accepts `#RRGGBB` or `#RRGGBBAA`; invalid values fall back to black | +| `custom_color_hex` | Colour picker | Only shown when `padding_fill` is `custom` | +| `custom_color_hex_input` | STRING (optional connector) | Only visible when `padding_fill` is `custom`. Connect any node that outputs a `#RRGGBB` / `#RRGGBBAA` string to override the colour picker. Invalid values fall back to the picker value. | | `noise_seed` | INT | Seed for noise generation — only shown when `padding_fill` is `noise` | --- diff --git a/js/anchor_widget.js b/js/anchor_widget.js index ead0cbc..4550be0 100644 --- a/js/anchor_widget.js +++ b/js/anchor_widget.js @@ -187,7 +187,26 @@ app.registerExtension({ // ComfyUI may auto-add a control widget immediately after noise_seed. const noiseSeedIdx = node.widgets?.findIndex(w => w.name === "noise_seed") ?? -1; const maybeCtrlW = noiseSeedIdx >= 0 ? node.widgets[noiseSeedIdx + 1] : null; - const noiseCtrlW = (maybeCtrlW && maybeCtrlW.name !== "custom_color") ? maybeCtrlW : null; + const noiseCtrlW = (maybeCtrlW && maybeCtrlW.name !== "custom_color_hex") ? maybeCtrlW : null; + + // Remove the custom_color_hex_input slot initially; sync() will add it when needed. + const removeColorSlot = () => { + const idx = node.inputs?.findIndex(i => i.name === "custom_color_hex_input") ?? -1; + if (idx >= 0) node.removeInput(idx); + }; + removeColorSlot(); + + const isColorSlotConnected = () => { + const idx = node.inputs?.findIndex(i => i.name === "custom_color_hex_input") ?? -1; + return idx >= 0 && node.inputs[idx].link != null; + }; + + const syncPickerState = () => { + const connected = isColorSlotConnected(); + colorInput.disabled = connected; + colorInput.style.opacity = connected ? "0.35" : "1"; + colorInput.style.cursor = connected ? "not-allowed" : "pointer"; + }; const sync = () => { // Capture size BEFORE any changes so we never shrink a user-resized node. @@ -198,10 +217,18 @@ app.registerExtension({ const showColor = val === "custom"; const showNoise = val === "noise"; - // Color picker DOM widget + // Color picker DOM widget + connector slot colorWrapper.style.display = showColor ? "" : "none"; colorDomW.computeSize = showColor ? () => [200, 36] : () => [0, -4]; + const slotExists = node.inputs?.some(i => i.name === "custom_color_hex_input"); + if (showColor && !slotExists) { + node.addInput("custom_color_hex_input", "STRING"); + } else if (!showColor && slotExists) { + removeColorSlot(); + } + syncPickerState(); + // noise_seed (and optional auto-added control widget) setWidgetVisible(noiseSeedW, showNoise); if (noiseCtrlW) setWidgetVisible(noiseCtrlW, showNoise); @@ -215,6 +242,23 @@ app.registerExtension({ paddingFillW.callback = (...args) => { origCb?.apply(paddingFillW, args); sync(); }; } + // Re-sync when the color slot is connected or disconnected. + const origOnConnectionsChange = nodeType.prototype.onConnectionsChange; + nodeType.prototype.onConnectionsChange = function (slotType, slotIdx, connected, link, ioSlot) { + origOnConnectionsChange?.apply(this, arguments); + if (slotType === LiteGraph.INPUT) { + const slot = this.inputs?.[slotIdx]; + if (slot?.name === "custom_color_hex_input") syncPickerState(); + } + }; + + // After a saved workflow restores widget values, re-run sync. + const origOnConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function (info) { + origOnConfigure?.apply(this, arguments); + sync(); + }; + sync(); }; }, diff --git a/nodes.py b/nodes.py index d6fdf63..b80ee53 100644 --- a/nodes.py +++ b/nodes.py @@ -44,7 +44,12 @@ class ResizeToCanvasSize: {"default": "black"}), "custom_color_hex": ("STRING", {"default": "#000000"}), "noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - } + }, + "optional": { + # Connected value overrides the colour picker; must be #RRGGBB or #RRGGBBAA. + # Invalid values fall back to the picker value. + "custom_color_hex_input": ("STRING", {"forceInput": True}), + }, } RETURN_TYPES = ("IMAGE", "MASK") @@ -58,15 +63,22 @@ class ResizeToCanvasSize: # ------------------------------------------------------------------ def resize(self, image, width, height, anchor, scale_method, fill_method, - padding_fill, custom_color_hex, noise_seed): + padding_fill, custom_color_hex, noise_seed, custom_color_hex_input=None): """Process every image in the batch.""" + # If a node is connected to custom_color_hex_input, validate and use it; + # fall back to the picker value on any invalid input. + if custom_color_hex_input is not None: + resolved_color = self._sanitize_hex(custom_color_hex_input, custom_color_hex) + else: + resolved_color = custom_color_hex + results, masks = [], [] for i in range(image.shape[0]): img_t, mask_t = self._process_single( image[i], width, height, anchor, scale_method, fill_method, - padding_fill, custom_color_hex, noise_seed, + padding_fill, resolved_color, noise_seed, ) results.append(img_t) masks.append(mask_t) @@ -163,6 +175,17 @@ class ResizeToCanvasSize: y = {"top": 0, "middle": (tgt_h - src_h) // 2, "bottom": tgt_h - src_h}[v_key] return x, y + @staticmethod + def _sanitize_hex(value, fallback="#000000"): + """Validate a hex colour string; return value if valid, fallback otherwise.""" + try: + h = str(value).strip().lstrip("#").strip() + if len(h) in (6, 8) and all(c in "0123456789abcdefABCDEF" for c in h): + return "#" + h + except (AttributeError, TypeError): + pass + return fallback + @staticmethod def _parse_color(padding_fill, custom_color_hex): """Return (R, G, B, A) 0-255.""" @@ -175,9 +198,7 @@ class ResizeToCanvasSize: if padding_fill in presets: return presets[padding_fill] - # custom hex: sanitize, strip whitespace and leading #, accept #RRGGBB or #RRGGBBAA. - # If the value is invalid (e.g. passed from a connected node), fall back to the - # picker default (black). + # custom_color_hex has already been sanitized and resolved upstream. try: h = str(custom_color_hex).strip().lstrip("#").strip() if len(h) == 6: @@ -188,7 +209,7 @@ class ResizeToCanvasSize: return (r, g, b, a) except (ValueError, AttributeError, TypeError): pass - return (0, 0, 0, 255) # fallback — matches picker default + return (0, 0, 0, 255) @staticmethod def _tensor_to_pil(tensor):