Add custom_color_hex_input connector: overrides picker when connected, grays out picker, sanitizes invalid hex

This commit is contained in:
Carasibana
2026-03-18 16:23:37 -04:00
parent a912f2ca60
commit acdeac3ed5
3 changed files with 76 additions and 10 deletions
+2 -1
View File
@@ -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` |
---
+46 -2
View File
@@ -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();
};
},
+28 -7
View File
@@ -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):