From 4d721bff593bb01f07c6cf212314c7db9affd494 Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Thu, 26 Feb 2026 08:22:46 -0500 Subject: [PATCH] Range Editor --- comfy_api/latest/_io.py | 35 ++++++++++++ nodes.py | 122 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index 988e34534..8cfa909b4 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -1252,6 +1252,41 @@ class Curve(ComfyTypeIO): return super().as_dict() +@comfytype(io_type="RANGE") +class Range(ComfyTypeIO): + Type = dict # {"min": float, "max": float, "midpoint"?: float} + + class Input(WidgetInput): + def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, + socketless: bool=True, default: dict=None, + display: str=None, + gradient_stops: list=None, + show_midpoint: bool=None, + midpoint_scale: str=None, + value_min: float=None, + value_max: float=None, + advanced: bool=None): + super().__init__(id, display_name, optional, tooltip, None, default, socketless, None, None, None, None, advanced) + if default is None: + self.default = {"min": 0.0, "max": 1.0} + self.display = display + self.gradient_stops = gradient_stops + self.show_midpoint = show_midpoint + self.midpoint_scale = midpoint_scale + self.value_min = value_min + self.value_max = value_max + + def as_dict(self): + return super().as_dict() | prune_dict({ + "display": self.display, + "gradient_stops": self.gradient_stops, + "show_midpoint": self.show_midpoint, + "midpoint_scale": self.midpoint_scale, + "value_min": self.value_min, + "value_max": self.value_max, + }) + + @comfytype(io_type="COLOR_CURVES") class ColorCurves(ComfyTypeIO): class ColorCurvesDict(TypedDict): diff --git a/nodes.py b/nodes.py index 7bd12e360..dfacdc129 100644 --- a/nodes.py +++ b/nodes.py @@ -2057,6 +2057,122 @@ class TestCurveWidget: return {"ui": {"text": [result]}, "result": (result,)} +class TestRangePlain: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "range": ("RANGE", {"default": {"min": 0.0, "max": 1.0}}), + "range_midpoint": ("RANGE", { + "default": {"min": 0.2, "max": 0.8, "midpoint": 0.5}, + "show_midpoint": True, + }), + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "execute" + OUTPUT_NODE = True + CATEGORY = "testing" + + def execute(self, **kwargs): + import json + result = json.dumps(kwargs, indent=2) + return {"ui": {"text": [result]}, "result": (result,)} + + +class TestRangeGradient: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "range": ("RANGE", { + "default": {"min": 0.0, "max": 1.0}, + "display": "gradient", + "gradient_stops": [ + {"offset": 0.0, "color": [0, 0, 0]}, + {"offset": 1.0, "color": [255, 255, 255]} + ], + }), + "range_midpoint": ("RANGE", { + "default": {"min": 0.0, "max": 1.0, "midpoint": 0.5}, + "display": "gradient", + "gradient_stops": [ + {"offset": 0.0, "color": [0, 0, 0]}, + {"offset": 1.0, "color": [255, 255, 255]} + ], + "show_midpoint": True, + "midpoint_scale": "gamma", + }), + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "execute" + OUTPUT_NODE = True + CATEGORY = "testing" + + def execute(self, **kwargs): + import json + result = json.dumps(kwargs, indent=2) + return {"ui": {"text": [result]}, "result": (result,)} + + +class TestRangeHistogram: + RANGE_OPTS = { + "display": "histogram", + "show_midpoint": True, + "midpoint_scale": "gamma", + "value_min": 0, + "value_max": 255, + } + + @classmethod + def INPUT_TYPES(s): + default = {"min": 0, "max": 255, "midpoint": 0.5} + return { + "required": { + "image": ("IMAGE",), + "rgb": ("RANGE", {"default": {**default}, **s.RANGE_OPTS}), + "red": ("RANGE", {"default": {**default}, **s.RANGE_OPTS}), + "green": ("RANGE", {"default": {**default}, **s.RANGE_OPTS}), + "blue": ("RANGE", {"default": {**default}, **s.RANGE_OPTS}), + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "execute" + OUTPUT_NODE = True + CATEGORY = "testing" + + def execute(self, image, rgb, red, green, blue): + import json + import numpy as np + + img = image[0].cpu().numpy() # (H, W, C) + + # Per-channel histograms + hist_r, _ = np.histogram(img[:, :, 0].flatten(), bins=256, range=(0.0, 1.0)) + hist_g, _ = np.histogram(img[:, :, 1].flatten(), bins=256, range=(0.0, 1.0)) + hist_b, _ = np.histogram(img[:, :, 2].flatten(), bins=256, range=(0.0, 1.0)) + + # Luminance histogram (BT.709) + luminance = 0.2126 * img[:, :, 0] + 0.7152 * img[:, :, 1] + 0.0722 * img[:, :, 2] + hist_rgb, _ = np.histogram(luminance.flatten(), bins=256, range=(0.0, 1.0)) + + result = json.dumps({"rgb": rgb, "red": red, "green": green, "blue": blue}, indent=2) + return { + "ui": { + "text": [result], + "range_histogram_rgb": hist_rgb.astype(np.uint32).tolist(), + "range_histogram_red": hist_r.astype(np.uint32).tolist(), + "range_histogram_green": hist_g.astype(np.uint32).tolist(), + "range_histogram_blue": hist_b.astype(np.uint32).tolist(), + }, + "result": (result,) + } + + NODE_CLASS_MAPPINGS = { "KSampler": KSampler, "CheckpointLoaderSimple": CheckpointLoaderSimple, @@ -2126,6 +2242,9 @@ NODE_CLASS_MAPPINGS = { "ConditioningSetTimestepRange": ConditioningSetTimestepRange, "LoraLoaderModelOnly": LoraLoaderModelOnly, "TestCurveWidget": TestCurveWidget, + "TestRangePlain": TestRangePlain, + "TestRangeGradient": TestRangeGradient, + "TestRangeHistogram": TestRangeHistogram, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -2195,6 +2314,9 @@ NODE_DISPLAY_NAME_MAPPINGS = { "VAEDecodeTiled": "VAE Decode (Tiled)", "VAEEncodeTiled": "VAE Encode (Tiled)", "TestCurveWidget": "Test Curve Widget", + "TestRangePlain": "Test Range (Plain)", + "TestRangeGradient": "Test Range (Gradient)", + "TestRangeHistogram": "Test Range (Histogram)", } EXTENSION_WEB_DIRS = {}