diff --git a/comfy_api/input/__init__.py b/comfy_api/input/__init__.py index 0d24730b5..11977360e 100644 --- a/comfy_api/input/__init__.py +++ b/comfy_api/input/__init__.py @@ -7,6 +7,7 @@ from comfy_api.latest._input import ( VideoInput, CurveInput, MonotoneCubicCurve, + LinearCurve, ) __all__ = [ @@ -17,4 +18,5 @@ __all__ = [ "VideoInput", "CurveInput", "MonotoneCubicCurve", + "LinearCurve", ] diff --git a/comfy_api/latest/_input/__init__.py b/comfy_api/latest/_input/__init__.py index 05af2f0b7..029e5b192 100644 --- a/comfy_api/latest/_input/__init__.py +++ b/comfy_api/latest/_input/__init__.py @@ -1,4 +1,4 @@ -from .basic_types import ImageInput, AudioInput, MaskInput, LatentInput, CurveInput, MonotoneCubicCurve +from .basic_types import ImageInput, AudioInput, MaskInput, LatentInput, CurveInput, MonotoneCubicCurve, LinearCurve from .video_types import VideoInput __all__ = [ @@ -9,4 +9,5 @@ __all__ = [ "LatentInput", "CurveInput", "MonotoneCubicCurve", + "LinearCurve", ] diff --git a/comfy_api/latest/_input/basic_types.py b/comfy_api/latest/_input/basic_types.py index 3e571faad..aefb3fc29 100644 --- a/comfy_api/latest/_input/basic_types.py +++ b/comfy_api/latest/_input/basic_types.py @@ -170,9 +170,9 @@ class MonotoneCubicCurve(CurveInput): xs, ys, slopes = self._xs, self._ys, self._slopes n = len(xs) if n == 0: - return np.zeros_like(xs_in) + return np.zeros_like(xs_in, dtype=np.float64) if n == 1: - return np.full_like(xs_in, ys[0]) + return np.full_like(xs_in, ys[0], dtype=np.float64) hi = np.searchsorted(xs, xs_in, side='right').clip(1, n - 1) lo = hi - 1 @@ -195,3 +195,40 @@ class MonotoneCubicCurve(CurveInput): def __repr__(self) -> str: return f"MonotoneCubicCurve(points={self._points})" + + +class LinearCurve(CurveInput): + """Piecewise linear interpolation over control points. + + Mirrors the frontend ``createLinearInterpolator`` in + ``ComfyUI_frontend/src/components/curve/curveUtils.ts``. + """ + + def __init__(self, control_points: list[CurvePoint]): + sorted_pts = sorted(control_points, key=lambda p: p[0]) + self._points = [(float(x), float(y)) for x, y in sorted_pts] + self._xs = np.array([p[0] for p in self._points], dtype=np.float64) + self._ys = np.array([p[1] for p in self._points], dtype=np.float64) + + @property + def points(self) -> list[CurvePoint]: + return list(self._points) + + def interp(self, x: float) -> float: + xs, ys = self._xs, self._ys + n = len(xs) + if n == 0: + return 0.0 + if n == 1: + return float(ys[0]) + return float(np.interp(x, xs, ys)) + + def interp_array(self, xs_in: np.ndarray) -> np.ndarray: + if len(self._xs) == 0: + return np.zeros_like(xs_in, dtype=np.float64) + if len(self._xs) == 1: + return np.full_like(xs_in, self._ys[0], dtype=np.float64) + return np.interp(xs_in, self._xs, self._ys) + + def __repr__(self) -> str: + return f"LinearCurve(points={self._points})" diff --git a/nodes.py b/nodes.py index 2af2874f6..2ae986d2c 100644 --- a/nodes.py +++ b/nodes.py @@ -2039,7 +2039,7 @@ class CurveEditor: def INPUT_TYPES(s): return { "required": { - "curve": ("CURVE", {"default": [[0, 0], [1, 1]]}), + "curve": ("CURVE", {"default": {"points": [[0, 0], [1, 1]], "interpolation": "monotone_cubic"}}), } } @@ -2049,10 +2049,15 @@ class CurveEditor: CATEGORY = "utils" def execute(self, curve): - from comfy_api.input import CurveInput, MonotoneCubicCurve + from comfy_api.input import CurveInput, MonotoneCubicCurve, LinearCurve if isinstance(curve, CurveInput): return (curve,) - return (MonotoneCubicCurve([(float(x), float(y)) for x, y in curve]),) + raw_points = curve["points"] if isinstance(curve, dict) else curve + points = [(float(x), float(y)) for x, y in raw_points] + interpolation = curve.get("interpolation", "monotone_cubic") if isinstance(curve, dict) else "monotone_cubic" + if interpolation == "linear": + return (LinearCurve(points),) + return (MonotoneCubicCurve(points),) NODE_CLASS_MAPPINGS = {