Add custom_color_hex_input connector: overrides picker when connected, grays out picker, sanitizes invalid hex
This commit is contained in:
@@ -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
@@ -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();
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user