feat: ComfyUI-SimpleLogics library with conversion, logic, comparison, and math nodes

This commit is contained in:
Daniele Piscaglia
2026-03-05 14:36:18 +01:00
parent ceec75f1d5
commit c4ab166329
6 changed files with 670 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
.Python
*.egg-info/
dist/
build/
# Virtual environments
.venv/
venv/
env/
# Test / coverage
.pytest_cache/
.coverage
htmlcov/
# IDE
.vscode/
.idea/
# OS
.DS_Store
Thumbs.db
+59
View File
@@ -0,0 +1,59 @@
# ComfyUI-SimpleLogics
A lightweight ComfyUI node library providing logic operators, type conversions, math utilities, and switch nodes.
No extra dependencies
## Installation
Clone into your `ComfyUI/custom_nodes/` folder:
```bash
git clone https://github.com/danipisca07/ComfyUI-SimpleLogics
```
## Nodes
### Convert (`SimpleLogics/Convert`)
| Node | Description |
|------|-------------|
| Int → Float | Cast integer to float |
| Float → Int | Truncate float to integer |
| Int / Float → String | Numeric to text |
| String → Int / Float | Parse text to number |
| Bool → Int | `True` → `1`, `False` → `0` |
| Int → Bool | `0` → `False`, anything else → `True` |
### Logic (`SimpleLogics/Logic`)
AND, OR, NOT, XOR, NAND, NOR — all take `BOOLEAN` inputs and return `BOOLEAN`.
### Compare (`SimpleLogics/Compare`)
**Compare Int / Compare Float** — pick an operator (`==` `!=` `>` `<` `>=` `<=`), returns `BOOLEAN`.
### Math (`SimpleLogics/Math`)
| Node | Description |
|------|-------------|
| Max / Min Int | Larger / smaller of two integers |
| Max / Min Float | Larger / smaller of two floats |
| Clamp Int / Float | Constrain a value between `min` and `max` |
### Switch (`SimpleLogics/Switch`)
All switch nodes take a `condition` (BOOLEAN) and return `if_true` or `if_false`.
| Node | Input types |
|------|-------------|
| Switch Int | INT |
| Switch Float | FLOAT |
| Switch String | STRING |
| Switch Any | Any type |
## Running Tests
```bash
pip install pytest
pytest tests/
```
## Contributions?
Welcome
+6
View File
@@ -0,0 +1,6 @@
try:
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
except ImportError:
from nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+470
View File
@@ -0,0 +1,470 @@
# SimpleLogics - ComfyUI node library for logic, math, and type conversion
# --- Type Conversion ---
class IntToFloat:
DESCRIPTION = "Converts an integer to a float."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"value": ("INT", {"default": 0})}}
RETURN_TYPES = ("FLOAT",)
FUNCTION = "convert"
CATEGORY = "SimpleLogics/Convert"
def convert(self, value):
return (float(value),)
class FloatToInt:
DESCRIPTION = "Converts a float to an integer by truncating the decimal part."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"value": ("FLOAT", {"default": 0.0})}}
RETURN_TYPES = ("INT",)
FUNCTION = "convert"
CATEGORY = "SimpleLogics/Convert"
def convert(self, value):
return (int(value),)
class IntToString:
DESCRIPTION = "Converts an integer to its string representation."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"value": ("INT", {"default": 0})}}
RETURN_TYPES = ("STRING",)
FUNCTION = "convert"
CATEGORY = "SimpleLogics/Convert"
def convert(self, value):
return (str(value),)
class FloatToString:
DESCRIPTION = "Converts a float to its string representation."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"value": ("FLOAT", {"default": 0.0})}}
RETURN_TYPES = ("STRING",)
FUNCTION = "convert"
CATEGORY = "SimpleLogics/Convert"
def convert(self, value):
return (str(value),)
class StringToInt:
DESCRIPTION = "Parses a string into an integer. Accepts decimal strings like '3.9' (truncated to 3)."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"value": ("STRING", {"default": "0"})}}
RETURN_TYPES = ("INT",)
FUNCTION = "convert"
CATEGORY = "SimpleLogics/Convert"
def convert(self, value):
return (int(float(value)),)
class StringToFloat:
DESCRIPTION = "Parses a string into a float."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"value": ("STRING", {"default": "0.0"})}}
RETURN_TYPES = ("FLOAT",)
FUNCTION = "convert"
CATEGORY = "SimpleLogics/Convert"
def convert(self, value):
return (float(value),)
class BoolToInt:
DESCRIPTION = "Converts a boolean to an integer: True → 1, False → 0."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"value": ("BOOLEAN", {"default": False})}}
RETURN_TYPES = ("INT",)
FUNCTION = "convert"
CATEGORY = "SimpleLogics/Convert"
def convert(self, value):
return (1 if value else 0,)
class IntToBool:
DESCRIPTION = "Converts an integer to a boolean: 0 → False, any other value → True."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"value": ("INT", {"default": 0})}}
RETURN_TYPES = ("BOOLEAN",)
FUNCTION = "convert"
CATEGORY = "SimpleLogics/Convert"
def convert(self, value):
return (value != 0,)
# --- Logic Operators ---
class LogicAND:
DESCRIPTION = "Returns True only if both inputs are True."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"a": ("BOOLEAN", {"default": False}), "b": ("BOOLEAN", {"default": False})}}
RETURN_TYPES = ("BOOLEAN",)
FUNCTION = "op"
CATEGORY = "SimpleLogics/Logic"
def op(self, a, b):
return (a and b,)
class LogicOR:
DESCRIPTION = "Returns True if at least one input is True."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"a": ("BOOLEAN", {"default": False}), "b": ("BOOLEAN", {"default": False})}}
RETURN_TYPES = ("BOOLEAN",)
FUNCTION = "op"
CATEGORY = "SimpleLogics/Logic"
def op(self, a, b):
return (a or b,)
class LogicNOT:
DESCRIPTION = "Returns the opposite boolean value of the input."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"a": ("BOOLEAN", {"default": False})}}
RETURN_TYPES = ("BOOLEAN",)
FUNCTION = "op"
CATEGORY = "SimpleLogics/Logic"
def op(self, a):
return (not a,)
class LogicXOR:
DESCRIPTION = "Returns True if exactly one of the two inputs is True."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"a": ("BOOLEAN", {"default": False}), "b": ("BOOLEAN", {"default": False})}}
RETURN_TYPES = ("BOOLEAN",)
FUNCTION = "op"
CATEGORY = "SimpleLogics/Logic"
def op(self, a, b):
return (a ^ b,)
class LogicNAND:
DESCRIPTION = "Returns False only if both inputs are True (NOT AND)."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"a": ("BOOLEAN", {"default": False}), "b": ("BOOLEAN", {"default": False})}}
RETURN_TYPES = ("BOOLEAN",)
FUNCTION = "op"
CATEGORY = "SimpleLogics/Logic"
def op(self, a, b):
return (not (a and b),)
class LogicNOR:
DESCRIPTION = "Returns True only if both inputs are False (NOT OR)."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"a": ("BOOLEAN", {"default": False}), "b": ("BOOLEAN", {"default": False})}}
RETURN_TYPES = ("BOOLEAN",)
FUNCTION = "op"
CATEGORY = "SimpleLogics/Logic"
def op(self, a, b):
return (not (a or b),)
# --- Comparison ---
class CompareInt:
DESCRIPTION = "Compares two integers using the selected operator and returns a boolean result."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("INT", {"default": 0}),
"b": ("INT", {"default": 0}),
"op": (["==", "!=", ">", "<", ">=", "<="],),
}
}
RETURN_TYPES = ("BOOLEAN",)
FUNCTION = "compare"
CATEGORY = "SimpleLogics/Compare"
def compare(self, a, b, op):
result = {"==": a == b, "!=": a != b, ">": a > b, "<": a < b, ">=": a >= b, "<=": a <= b}[op]
return (result,)
class CompareFloat:
DESCRIPTION = "Compares two floats using the selected operator and returns a boolean result."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("FLOAT", {"default": 0.0}),
"b": ("FLOAT", {"default": 0.0}),
"op": (["==", "!=", ">", "<", ">=", "<="],),
}
}
RETURN_TYPES = ("BOOLEAN",)
FUNCTION = "compare"
CATEGORY = "SimpleLogics/Compare"
def compare(self, a, b, op):
result = {"==": a == b, "!=": a != b, ">": a > b, "<": a < b, ">=": a >= b, "<=": a <= b}[op]
return (result,)
# --- Math ---
class MathMaxInt:
DESCRIPTION = "Returns the larger of two integers."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"a": ("INT", {"default": 0}), "b": ("INT", {"default": 0})}}
RETURN_TYPES = ("INT",)
FUNCTION = "op"
CATEGORY = "SimpleLogics/Math"
def op(self, a, b):
return (max(a, b),)
class MathMinInt:
DESCRIPTION = "Returns the smaller of two integers."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"a": ("INT", {"default": 0}), "b": ("INT", {"default": 0})}}
RETURN_TYPES = ("INT",)
FUNCTION = "op"
CATEGORY = "SimpleLogics/Math"
def op(self, a, b):
return (min(a, b),)
class MathMaxFloat:
DESCRIPTION = "Returns the larger of two floats."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"a": ("FLOAT", {"default": 0.0}), "b": ("FLOAT", {"default": 0.0})}}
RETURN_TYPES = ("FLOAT",)
FUNCTION = "op"
CATEGORY = "SimpleLogics/Math"
def op(self, a, b):
return (max(a, b),)
class MathMinFloat:
DESCRIPTION = "Returns the smaller of two floats."
@classmethod
def INPUT_TYPES(cls):
return {"required": {"a": ("FLOAT", {"default": 0.0}), "b": ("FLOAT", {"default": 0.0})}}
RETURN_TYPES = ("FLOAT",)
FUNCTION = "op"
CATEGORY = "SimpleLogics/Math"
def op(self, a, b):
return (min(a, b),)
class MathClampInt:
DESCRIPTION = "Clamps an integer between a minimum and maximum value."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("INT", {"default": 0}),
"min_val": ("INT", {"default": 0}),
"max_val": ("INT", {"default": 100}),
}
}
RETURN_TYPES = ("INT",)
FUNCTION = "op"
CATEGORY = "SimpleLogics/Math"
def op(self, value, min_val, max_val):
return (max(min_val, min(max_val, value)),)
class MathClampFloat:
DESCRIPTION = "Clamps a float between a minimum and maximum value."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("FLOAT", {"default": 0.0}),
"min_val": ("FLOAT", {"default": 0.0}),
"max_val": ("FLOAT", {"default": 1.0}),
}
}
RETURN_TYPES = ("FLOAT",)
FUNCTION = "op"
CATEGORY = "SimpleLogics/Math"
def op(self, value, min_val, max_val):
return (max(min_val, min(max_val, value)),)
# --- Switch Nodes ---
class SwitchInt:
DESCRIPTION = "Returns if_true when condition is True, otherwise returns if_false. Works with integers."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"condition": ("BOOLEAN", {"default": False}),
"if_true": ("INT", {"default": 0}),
"if_false": ("INT", {"default": 0}),
}
}
RETURN_TYPES = ("INT",)
FUNCTION = "switch"
CATEGORY = "SimpleLogics/Switch"
def switch(self, condition, if_true, if_false):
return (if_true if condition else if_false,)
class SwitchFloat:
DESCRIPTION = "Returns if_true when condition is True, otherwise returns if_false. Works with floats."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"condition": ("BOOLEAN", {"default": False}),
"if_true": ("FLOAT", {"default": 0.0}),
"if_false": ("FLOAT", {"default": 0.0}),
}
}
RETURN_TYPES = ("FLOAT",)
FUNCTION = "switch"
CATEGORY = "SimpleLogics/Switch"
def switch(self, condition, if_true, if_false):
return (if_true if condition else if_false,)
class SwitchString:
DESCRIPTION = "Returns if_true when condition is True, otherwise returns if_false. Works with strings."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"condition": ("BOOLEAN", {"default": False}),
"if_true": ("STRING", {"default": ""}),
"if_false": ("STRING", {"default": ""}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "switch"
CATEGORY = "SimpleLogics/Switch"
def switch(self, condition, if_true, if_false):
return (if_true if condition else if_false,)
class SwitchAny:
DESCRIPTION = "Returns if_true when condition is True, otherwise returns if_false. Works with any type."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"condition": ("BOOLEAN", {"default": False}),
"if_true": ("*",),
"if_false": ("*",),
}
}
RETURN_TYPES = ("*",)
FUNCTION = "switch"
CATEGORY = "SimpleLogics/Switch"
def switch(self, condition, if_true, if_false):
return (if_true if condition else if_false,)
# --- Mappings ---
NODE_CLASS_MAPPINGS = {
# Convert
"SL_IntToFloat": IntToFloat,
"SL_FloatToInt": FloatToInt,
"SL_IntToString": IntToString,
"SL_FloatToString": FloatToString,
"SL_StringToInt": StringToInt,
"SL_StringToFloat": StringToFloat,
"SL_BoolToInt": BoolToInt,
"SL_IntToBool": IntToBool,
# Logic
"SL_AND": LogicAND,
"SL_OR": LogicOR,
"SL_NOT": LogicNOT,
"SL_XOR": LogicXOR,
"SL_NAND": LogicNAND,
"SL_NOR": LogicNOR,
# Compare
"SL_CompareInt": CompareInt,
"SL_CompareFloat": CompareFloat,
# Math
"SL_MaxInt": MathMaxInt,
"SL_MinInt": MathMinInt,
"SL_MaxFloat": MathMaxFloat,
"SL_MinFloat": MathMinFloat,
"SL_ClampInt": MathClampInt,
"SL_ClampFloat": MathClampFloat,
# Switch
"SL_SwitchInt": SwitchInt,
"SL_SwitchFloat": SwitchFloat,
"SL_SwitchString": SwitchString,
"SL_SwitchAny": SwitchAny,
}
NODE_DISPLAY_NAME_MAPPINGS = {
# Convert
"SL_IntToFloat": "Int → Float",
"SL_FloatToInt": "Float → Int",
"SL_IntToString": "Int → String",
"SL_FloatToString": "Float → String",
"SL_StringToInt": "String → Int",
"SL_StringToFloat": "String → Float",
"SL_BoolToInt": "Bool → Int",
"SL_IntToBool": "Int → Bool",
# Logic
"SL_AND": "AND",
"SL_OR": "OR",
"SL_NOT": "NOT",
"SL_XOR": "XOR",
"SL_NAND": "NAND",
"SL_NOR": "NOR",
# Compare
"SL_CompareInt": "Compare Int",
"SL_CompareFloat": "Compare Float",
# Math
"SL_MaxInt": "Max Int",
"SL_MinInt": "Min Int",
"SL_MaxFloat": "Max Float",
"SL_MinFloat": "Min Float",
"SL_ClampInt": "Clamp Int",
"SL_ClampFloat": "Clamp Float",
# Switch
"SL_SwitchInt": "Switch Int",
"SL_SwitchFloat": "Switch Float",
"SL_SwitchString": "Switch String",
"SL_SwitchAny": "Switch Any",
}
+3
View File
@@ -0,0 +1,3 @@
[pytest]
testpaths = tests
addopts = --import-mode=importlib
+105
View File
@@ -0,0 +1,105 @@
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from nodes import (
IntToFloat, FloatToInt, IntToString, FloatToString,
StringToInt, StringToFloat, BoolToInt, IntToBool,
LogicAND, LogicOR, LogicNOT, LogicXOR, LogicNAND, LogicNOR,
CompareInt, CompareFloat,
MathMaxInt, MathMinInt, MathMaxFloat, MathMinFloat, MathClampInt, MathClampFloat,
SwitchInt, SwitchFloat, SwitchString, SwitchAny,
)
# --- Convert ---
def test_int_to_float():
assert IntToFloat().convert(3) == (3.0,)
def test_float_to_int():
assert FloatToInt().convert(3.9) == (3,)
def test_int_to_string():
assert IntToString().convert(42) == ("42",)
def test_float_to_string():
assert FloatToString().convert(1.5) == ("1.5",)
def test_string_to_int():
assert StringToInt().convert("7") == (7,)
def test_string_to_float():
assert StringToFloat().convert("2.5") == (2.5,)
def test_bool_to_int():
assert BoolToInt().convert(True) == (1,)
def test_int_to_bool():
assert IntToBool().convert(1) == (True,)
# --- Logic ---
def test_and():
assert LogicAND().op(True, True) == (True,)
def test_or():
assert LogicOR().op(False, True) == (True,)
def test_not():
assert LogicNOT().op(False) == (True,)
def test_xor():
assert LogicXOR().op(True, False) == (True,)
def test_nand():
assert LogicNAND().op(True, True) == (False,)
def test_nor():
assert LogicNOR().op(False, False) == (True,)
# --- Compare ---
def test_compare_int():
assert CompareInt().compare(5, 3, ">") == (True,)
def test_compare_float():
assert CompareFloat().compare(1.0, 1.0, "==") == (True,)
# --- Math ---
def test_max_int():
assert MathMaxInt().op(3, 7) == (7,)
def test_min_int():
assert MathMinInt().op(3, 7) == (3,)
def test_max_float():
assert MathMaxFloat().op(1.5, 2.5) == (2.5,)
def test_min_float():
assert MathMinFloat().op(1.5, 2.5) == (1.5,)
def test_clamp_int():
assert MathClampInt().op(150, 0, 100) == (100,)
def test_clamp_float():
assert MathClampFloat().op(-0.5, 0.0, 1.0) == (0.0,)
# --- Switch ---
def test_switch_int():
assert SwitchInt().switch(True, 10, 20) == (10,)
def test_switch_float():
assert SwitchFloat().switch(False, 1.0, 2.0) == (2.0,)
def test_switch_string():
assert SwitchString().switch(True, "yes", "no") == ("yes",)
def test_switch_any():
assert SwitchAny().switch(False, "a", "b") == ("b",)