From c29afc9e38297de80438014aff2fbbc398a37abd Mon Sep 17 00:00:00 2001 From: StableLlama Date: Thu, 8 May 2025 00:13:39 +0200 Subject: [PATCH] Add math operations and min/max for values and lists --- README.md | 13 +- src/basic_data_handling/__init__.py | 4 +- src/basic_data_handling/control_flow.py | 6 +- src/basic_data_handling/data_list_nodes.py | 88 ++++ src/basic_data_handling/list_nodes.py | 68 +++ src/basic_data_handling/math_nodes.py | 578 +++++++++++++++++++++ 6 files changed, 750 insertions(+), 7 deletions(-) create mode 100644 src/basic_data_handling/math_nodes.py diff --git a/README.md b/README.md index 7f400a0..bb2d05c 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ These nodes are very lightweight and require no additional dependencies. - Modification: append, extend, insert, remove, pop, clear, set_item - Access: get_item, slice, index, contains - Information: length, count - - Operations: sort, reverse + - Operations: sort, reverse, min, max - **SET**: Python set manipulation nodes (as a single variable) - Conversion: convert to data list, to LIST @@ -44,7 +44,7 @@ These nodes are very lightweight and require no additional dependencies. - Modification: append, filter, extend, insert, remove, pop, clear, set_item - Access: get_item, slice, index, contains - Information: length, count - - Operations: sort, reverse, copy, zip + - Operations: sort, reverse, copy, zip, min, max - **DICT**: Dictionary manipulation nodes - Creation: create, from_items, from_lists, fromkeys, any_to_DICT @@ -76,6 +76,15 @@ These nodes are very lightweight and require no additional dependencies. - Conditional: if/else for branching logic based on conditions - Selection: switch/case for selecting from multiple options based on an index +- **Math**: Mathematical operations + - Trigonometric: sin, cos, tan, asin, acos, atan, atan2 + - Logarithmic/Exponential: log, log10, exp, sqrt + - Numerical: min, max + - Constants: pi, e + - Conversion: degrees/radians + - Rounding: floor, ceil + - Other: abs (absolute value) + ## Understanding LIST vs. data list vs. SET ComfyUI has different data types that serve different purposes: diff --git a/src/basic_data_handling/__init__.py b/src/basic_data_handling/__init__.py index a509ec8..6cdeaf9 100644 --- a/src/basic_data_handling/__init__.py +++ b/src/basic_data_handling/__init__.py @@ -1,5 +1,5 @@ from . import (boolean_nodes, comparison_nodes, control_flow, data_list_nodes, dict_nodes, - float_nodes, int_nodes, list_nodes, set_nodes, string_nodes) + float_nodes, int_nodes, list_nodes, math_nodes, set_nodes, string_nodes) NODE_CLASS_MAPPINGS = {} NODE_CLASS_MAPPINGS.update(boolean_nodes.NODE_CLASS_MAPPINGS) @@ -11,6 +11,7 @@ NODE_CLASS_MAPPINGS.update(float_nodes.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(int_nodes.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(list_nodes.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(set_nodes.NODE_CLASS_MAPPINGS) +NODE_CLASS_MAPPINGS.update(math_nodes.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(string_nodes.NODE_CLASS_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS = {} @@ -23,4 +24,5 @@ NODE_DISPLAY_NAME_MAPPINGS.update(float_nodes.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(int_nodes.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(list_nodes.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(set_nodes.NODE_DISPLAY_NAME_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(math_nodes.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(string_nodes.NODE_DISPLAY_NAME_MAPPINGS) diff --git a/src/basic_data_handling/control_flow.py b/src/basic_data_handling/control_flow.py index 85bf8d9..419b283 100644 --- a/src/basic_data_handling/control_flow.py +++ b/src/basic_data_handling/control_flow.py @@ -21,7 +21,7 @@ class IfElse: RETURN_TYPES = ("*",) RETURN_NAMES = ("result",) - CATEGORY = "Basic/ ⎇ Flow Control" + CATEGORY = "Basic/Flow Control" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "execute" @@ -40,7 +40,6 @@ class IfElse: return needed def execute(self, condition: bool, if_true: Any, if_false: Any) -> tuple[Any]: - """Execute the if/else statement and return the appropriate value.""" return (if_true if condition else if_false,) @@ -73,7 +72,7 @@ class SwitchCase: RETURN_TYPES = ("*",) RETURN_NAMES = ("result",) - CATEGORY = "Basic/ ⎇ Flow Control" + CATEGORY = "Basic/Flow Control" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "execute" @@ -103,7 +102,6 @@ class SwitchCase: def execute(self, selector: int, case_0: Any, case_1: Any, case_2: Any = None, case_3: Any = None, case_4: Any = None, default: Any = None) -> tuple[Any]: - """Execute the switch/case statement and return the appropriate value.""" cases = [case_0, case_1, case_2, case_3, case_4] if 0 <= selector < len(cases) and cases[selector] is not None: diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index a8bcee4..a9f5f24 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -721,6 +721,90 @@ class DataListToList: return (kwargs.get('list', []).copy(),) +class DataListMin: + """ + Finds the minimum value in a list of numbers. + + This node takes a list of numbers (either FLOAT or INT) and returns + the minimum value. Returns None if the list is empty or if it contains + non-numeric values. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": (["FLOAT", "INT"], {}), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("min_value",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "find_min" + INPUT_IS_LIST = True + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str: + if input_types[0]["list"] not in ("FLOAT", "INT"): + return "list must contain FLOAT or INT types" + return True + + def find_min(self, **kwargs: dict[str, list]) -> tuple[Any]: + values = kwargs.get('list', []) + if not values: + return (None,) + + try: + # Return the same type as found in the minimum value + return (min(values),) + except (TypeError, ValueError): + # Handle case where list contains non-comparable elements + return (None,) + + +class DataListMax: + """ + Finds the maximum value in a list of numbers. + + This node takes a list of numbers (either FLOAT or INT) and returns + the maximum value. Returns None if the list is empty or if it contains + non-numeric values. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": (["FLOAT", "INT"], {}), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("max_value",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "find_max" + INPUT_IS_LIST = True + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str: + if input_types[0]["list"] not in ("FLOAT", "INT"): + return "list must contain FLOAT or INT types" + return True + + def find_max(self, **kwargs: dict[str, list]) -> tuple[Any]: + values = kwargs.get('list', []) + if not values: + return (None,) + + try: + # Return the same type as found in the maximum value + return (max(values),) + except (TypeError, ValueError): + # Handle case where list contains non-comparable elements + return (None,) + + NODE_CLASS_MAPPINGS = { "DataListAppend": DataListAppend, "DataListExtend": DataListExtend, @@ -742,6 +826,8 @@ NODE_CLASS_MAPPINGS = { "DataListZip": DataListZip, "DataListFilter": DataListFilter, "DataListToList": DataListToList, + "DataListMin": DataListMin, + "DataListMax": DataListMax, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -765,4 +851,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "DataListZip": "zip", "DataListFilter": "filter", "DataListToList": "convert to LIST", + "DataListMin": "min", + "DataListMax": "max", } diff --git a/src/basic_data_handling/list_nodes.py b/src/basic_data_handling/list_nodes.py index 47ef9cb..c186227 100644 --- a/src/basic_data_handling/list_nodes.py +++ b/src/basic_data_handling/list_nodes.py @@ -515,6 +515,70 @@ class AnyToList: return ([input],) +class ListMin: + """ + Returns the minimum value in a LIST. + + This node takes a LIST of comparable items and returns the minimum value. + Returns None if the LIST is empty or if items are not comparable. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("min_value",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "find_min" + + def find_min(self, list: list[Any]) -> tuple[Any]: + if not list: + return (None,) + + try: + return (min(list),) + except (TypeError, ValueError): + # Handle case where list contains non-comparable elements + return (None,) + + +class ListMax: + """ + Returns the maximum value in a LIST. + + This node takes a LIST of comparable items and returns the maximum value. + Returns None if the LIST is empty or if items are not comparable. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("max_value",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "find_max" + + def find_max(self, list: list[Any]) -> tuple[Any]: + if not list: + return (None,) + + try: + return (max(list),) + except (TypeError, ValueError): + # Handle case where list contains non-comparable elements + return (None,) + + NODE_CLASS_MAPPINGS = { "ListAppend": ListAppend, "ListExtend": ListExtend, @@ -534,6 +598,8 @@ NODE_CLASS_MAPPINGS = { "ListToDataList": ListToDataList, "ListToSet": ListToSet, "AnyToList": AnyToList, + "ListMin": ListMin, + "ListMax": ListMax, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -555,4 +621,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "ListToDataList": "convert to data list", "ListToSet": "convert to SET", "AnyToList": "any to LIST", + "ListMin": "min", + "ListMax": "max", } diff --git a/src/basic_data_handling/math_nodes.py b/src/basic_data_handling/math_nodes.py new file mode 100644 index 0000000..81fbe4f --- /dev/null +++ b/src/basic_data_handling/math_nodes.py @@ -0,0 +1,578 @@ +import math +from inspect import cleandoc +from typing import Literal, Union + + +class MathSin: + """ + Calculates the sine of an angle in radians or degrees. + + This node takes an angle value and unit type, and returns the sine of that angle. + The sine of an angle is the ratio of the length of the opposite side to the length + of the hypotenuse in a right-angled triangle. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "angle": ("FLOAT", {"default": 0.0}), + "unit": (["radians", "degrees"], {"default": "radians"}), + } + } + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + if unit == "degrees": + # Convert degrees to radians + angle = math.radians(angle) + return (math.sin(angle),) + + +class MathCos: + """ + Calculates the cosine of an angle in radians or degrees. + + This node takes an angle value and unit type, and returns the cosine of that angle. + The cosine of an angle is the ratio of the length of the adjacent side to the length + of the hypotenuse in a right-angled triangle. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "angle": ("FLOAT", {"default": 0.0}), + "unit": (["radians", "degrees"], {"default": "radians"}), + } + } + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + if unit == "degrees": + # Convert degrees to radians + angle = math.radians(angle) + return (math.cos(angle),) + + +class MathTan: + """ + Calculates the tangent of an angle in radians or degrees. + + This node takes an angle value and unit type, and returns the tangent of that angle. + The tangent of an angle is the ratio of the length of the opposite side to the length + of the adjacent side in a right-angled triangle. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "angle": ("FLOAT", {"default": 0.0}), + "unit": (["radians", "degrees"], {"default": "radians"}), + } + } + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + if unit == "degrees": + # Convert degrees to radians + angle = math.radians(angle) + + # Handle specific angles that would result in division by zero + if abs(math.cos(angle)) < 1e-10: + raise ValueError("Tangent is undefined at this angle (division by zero)") + + return (math.tan(angle),) + + +class MathAsin: + """ + Calculates the arc sine (inverse sine) of a value. + + This node takes a value between -1 and 1, and returns the arc sine in radians or degrees. + The arc sine is the inverse operation of sine, returning the angle whose sine is the input value. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0}), + "unit": (["radians", "degrees"], {"default": "radians"}), + } + } + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, value: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + result = math.asin(value) + if unit == "degrees": + result = math.degrees(result) + return (result,) + + +class MathAcos: + """ + Calculates the arc cosine (inverse cosine) of a value. + + This node takes a value between -1 and 1, and returns the arc cosine in radians or degrees. + The arc cosine is the inverse operation of cosine, returning the angle whose cosine is the input value. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 1.0, "min": -1.0, "max": 1.0}), + "unit": (["radians", "degrees"], {"default": "radians"}), + } + } + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, value: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + result = math.acos(value) + if unit == "degrees": + result = math.degrees(result) + return (result,) + + +class MathAtan: + """ + Calculates the arc tangent (inverse tangent) of a value. + + This node takes a value and returns the arc tangent in radians or degrees. + The arc tangent is the inverse operation of tangent, returning the angle whose tangent is the input value. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 0.0}), + "unit": (["radians", "degrees"], {"default": "radians"}), + } + } + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, value: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + result = math.atan(value) + if unit == "degrees": + result = math.degrees(result) + return (result,) + + +class MathAtan2: + """ + Calculates the arc tangent of y/x, considering the quadrant. + + This node takes y and x coordinates and returns the arc tangent in radians or degrees. + Unlike atan(y/x), this function handles the case where x is zero and correctly determines + the quadrant of the resulting angle. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "y": ("FLOAT", {"default": 0.0}), + "x": ("FLOAT", {"default": 1.0}), + "unit": (["radians", "degrees"], {"default": "radians"}), + } + } + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, y: float, x: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + result = math.atan2(y, x) + if unit == "degrees": + result = math.degrees(result) + return (result,) + + +class MathSqrt: + """ + Calculates the square root of a non-negative number. + + This node takes a non-negative number and returns its square root. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 0.0, "min": 0.0}), + } + } + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, value: float) -> tuple[float]: + return (math.sqrt(value),) + + +class MathExp: + """ + Calculates the exponential of a number (e^x). + + This node takes a number and returns e (Euler's number) raised to the power of that number. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 0.0}), + } + } + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, value: float) -> tuple[float]: + return (math.exp(value),) + + +class MathLog: + """ + Calculates the natural logarithm (base e) of a number. + + This node takes a positive number and returns its natural logarithm. + If the specified base is not 'e', calculates the logarithm with the specified base. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 1.0, "min": 0.0000001}), + }, + "optional": { + "base": ("FLOAT", {"default": math.e, "min": 0.0000001}), + } + } + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, value: float, base: float = math.e) -> tuple[float]: + if base == math.e: + return (math.log(value),) + else: + return (math.log(value, base),) + + +class MathLog10: + """ + Calculates the base-10 logarithm of a number. + + This node takes a positive number and returns its base-10 logarithm. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 1.0, "min": 0.0000001}), + } + } + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, value: float) -> tuple[float]: + return (math.log10(value),) + + +class MathDegrees: + """ + Converts an angle from radians to degrees. + + This node takes an angle in radians and returns the equivalent angle in degrees. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "radians": ("FLOAT", {"default": 0.0}), + } + } + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, radians: float) -> tuple[float]: + return (math.degrees(radians),) + + +class MathRadians: + """ + Converts an angle from degrees to radians. + + This node takes an angle in degrees and returns the equivalent angle in radians. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "degrees": ("FLOAT", {"default": 0.0}), + } + } + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, degrees: float) -> tuple[float]: + return (math.radians(degrees),) + + +class MathFloor: + """ + Returns the floor of a number. + + This node takes a number and returns the largest integer less than or equal to the input value. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 0.0}), + } + } + + RETURN_TYPES = ("INT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, value: float) -> tuple[int]: + return (math.floor(value),) + + +class MathCeil: + """ + Returns the ceiling of a number. + + This node takes a number and returns the smallest integer greater than or equal to the input value. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 0.0}), + } + } + + RETURN_TYPES = ("INT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, value: float) -> tuple[int]: + return (math.ceil(value),) + + +class MathAbs: + """ + Returns the absolute value of a number. + + This node takes a number and returns its absolute value (magnitude without sign). + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (["FLOAT", "INT"], {}), + } + } + + RETURN_TYPES = ("*",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str: + if input_types["value"] not in ("FLOAT", "INT"): + return "value must be a FLOAT or INT type" + return True + + def calculate(self, value: Union[float, int]) -> tuple[Union[float, int]]: + return (abs(value),) + + +class MathPi: + """ + Returns the mathematical constant π (pi). + + This node returns the value of π, which is approximately 3.14159. + """ + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self) -> tuple[float]: + return (math.pi,) + + +class MathE: + """ + Returns the mathematical constant e. + + This node returns the value of e (Euler's number), which is approximately 2.71828. + """ + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = ("FLOAT",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self) -> tuple[float]: + return (math.e,) + + +class MathMin: + """ + Returns the minimum value among the inputs. + + This node takes two or more values and returns the minimum value among them. + The function works with both FLOAT and INT types. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value1": (["FLOAT", "INT"], {"default": 0}), + "value2": (["FLOAT", "INT"], {"default": 0}), + } + } + + RETURN_TYPES = ("*",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str: + for key, value in input_types.items(): + if key.startswith("value") and value not in ("FLOAT", "INT"): + return f"{key} must be a FLOAT or INT type" + return True + + def calculate(self, value1: Union[float, int], value2: Union[float, int]) -> tuple[Union[float, int]]: + return (min(value1, value2),) + + +class MathMax: + """ + Returns the maximum value among the inputs. + + This node takes two or more values and returns the maximum value among them. + The function works with both FLOAT and INT types. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value1": (["FLOAT", "INT"], {"default": 0}), + "value2": (["FLOAT", "INT"], {"default": 0}), + } + } + + RETURN_TYPES = ("*",) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str: + for key, value in input_types.items(): + if key.startswith("value") and value not in ("FLOAT", "INT"): + return f"{key} must be a FLOAT or INT type" + return True + + def calculate(self, value1: Union[float, int], value2: Union[float, int]) -> tuple[Union[float, int]]: + return (max(value1, value2),) + + +NODE_CLASS_MAPPINGS = { + "MathSin": MathSin, + "MathCos": MathCos, + "MathTan": MathTan, + "MathAsin": MathAsin, + "MathAcos": MathAcos, + "MathAtan": MathAtan, + "MathAtan2": MathAtan2, + "MathSqrt": MathSqrt, + "MathExp": MathExp, + "MathLog": MathLog, + "MathLog10": MathLog10, + "MathDegrees": MathDegrees, + "MathRadians": MathRadians, + "MathFloor": MathFloor, + "MathCeil": MathCeil, + "MathAbs": MathAbs, + "MathPi": MathPi, + "MathE": MathE, + "MathMin": MathMin, + "MathMax": MathMax, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "MathSin": "sin", + "MathCos": "cos", + "MathTan": "tan", + "MathAsin": "asin", + "MathAcos": "acos", + "MathAtan": "atan", + "MathAtan2": "atan2", + "MathSqrt": "sqrt", + "MathExp": "exp", + "MathLog": "log", + "MathLog10": "log10", + "MathDegrees": "degrees", + "MathRadians": "radians", + "MathFloor": "floor", + "MathCeil": "ceil", + "MathAbs": "abs", + "MathPi": "pi", + "MathE": "e", + "MathMin": "min", + "MathMax": "max", +}