commit e41eccf1a99d54bc75411f506c09f482a8871f3a Author: LyAhn Date: Mon Jun 1 02:18:04 2026 +0100 Initial commit: Image Aspect Size and Multi Switch nodes - ImageAspectSize: reads input image dimensions and outputs width/height scaled to a target longest-side, snapped to multiples of 8, with a flip toggle for portrait/landscape rotation - MultiSwitch: any-type switch node with dynamic slot pairs (JS-driven add/remove), colour-coded active/inactive sides, and clean labelling diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0e79223 --- /dev/null +++ b/__init__.py @@ -0,0 +1,16 @@ +from .nodes.image_aspect_size import ImageAspectSize +from .nodes.multi_switch import MultiSwitch + +NODE_CLASS_MAPPINGS = { + "ImageAspectSize": ImageAspectSize, + "MultiSwitch": MultiSwitch, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "ImageAspectSize": "Image Aspect Size", + "MultiSwitch": "Multi Switch", +} + +WEB_DIRECTORY = "./js" + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e5966c9 Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/__pycache__/__init__.cpython-313.pyc b/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..0adfa08 Binary files /dev/null and b/__pycache__/__init__.cpython-313.pyc differ diff --git a/__pycache__/install.cpython-313.pyc b/__pycache__/install.cpython-313.pyc new file mode 100644 index 0000000..58fcb4f Binary files /dev/null and b/__pycache__/install.cpython-313.pyc differ diff --git a/js/multi_switch.js b/js/multi_switch.js new file mode 100644 index 0000000..870fa0a --- /dev/null +++ b/js/multi_switch.js @@ -0,0 +1,126 @@ +import { app } from "../../scripts/app.js"; + +const MAX_SLOTS = 6; +const MIN_SLOTS = 1; + +// Slot dot colours — false side blue, true side orange, inactive grey +const COLOR_FALSE = "#7ab4f5"; +const COLOR_TRUE = "#f5a742"; +const COLOR_DIM = "#4a4a4a"; + +app.registerExtension({ + name: "JezzWTF.MultiSwitch", + + async nodeCreated(node) { + if (node.comfyClass !== "MultiSwitch") return; + + function getNumSlots() { + return node.widgets?.find(w => w.name === "num_slots")?.value ?? 2; + } + function setNumSlots(n) { + const w = node.widgets?.find(w => w.name === "num_slots"); + if (w) w.value = n; + } + function getSwitchValue() { + return node.widgets?.find(w => w.name === "switch")?.value ?? false; + } + + // Refresh slot dot colours based on current switch state + function updateColors() { + const isTrue = getSwitchValue(); + for (let i = 1; i < (node.inputs?.length ?? 0); i++) { + const inp = node.inputs[i]; + const isFalseSlot = inp.name?.startsWith("false"); + const active = isFalseSlot ? !isTrue : isTrue; + const col = active ? (isFalseSlot ? COLOR_FALSE : COLOR_TRUE) : COLOR_DIM; + inp.color_on = col; + inp.color_off = col; + } + node.setDirtyCanvas(true, false); + } + + // Add/remove input pairs and output slots together + function syncSlots(count) { + // Outputs + const curOut = node.outputs?.length ?? 0; + for (let i = curOut - 1; i >= count; i--) { + for (const id of [...(node.outputs[i]?.links ?? [])]) node.graph.removeLink(id); + node.removeOutput(i); + } + for (let i = curOut; i < count; i++) { + node.addOutput(`output_${i + 1}`, "*"); + } + + // Inputs (index 0 = switch widget area; slots start at 1) + const target = 1 + count * 2; + const curIn = node.inputs?.length ?? 0; + for (let i = curIn - 1; i >= target; i--) { + if (node.inputs[i]?.link != null) node.graph.removeLink(node.inputs[i].link); + node.removeInput(i); + } + for (let i = curIn; i < target; i++) { + const pair = Math.ceil(i / 2); + const isFalse = (i % 2 === 1); + node.addInput(`${isFalse ? "false" : "true"}_${pair}`, "*"); + } + + // Set clean display labels (strip _N suffix) + for (let i = 1; i < (node.inputs?.length ?? 0); i++) { + node.inputs[i].label = node.inputs[i].name?.startsWith("false") ? "false" : "true"; + } + + updateColors(); + node.setSize(node.computeSize()); + } + + // Watch the switch toggle and refresh colours immediately + const switchWidget = node.widgets?.find(w => w.name === "switch"); + if (switchWidget) { + const orig = switchWidget.callback; + switchWidget.callback = function (...args) { + orig?.call(this, ...args); + updateColors(); + }; + } + + // Draw subtle dashed dividers between slot pairs + const origFg = node.onDrawForeground?.bind(node); + node.onDrawForeground = function (ctx) { + origFg?.(ctx); + const count = getNumSlots(); + if (count <= 1) return; + + const slotH = LiteGraph.NODE_SLOT_HEIGHT ?? 20; + const titleH = LiteGraph.NODE_TITLE_HEIGHT ?? 30; + + ctx.save(); + ctx.strokeStyle = "rgba(255,255,255,0.10)"; + ctx.lineWidth = 1; + ctx.setLineDash([3, 5]); + + for (let i = 1; i < count; i++) { + const y = titleH + i * 2 * slotH; + ctx.beginPath(); + ctx.moveTo(6, y); + ctx.lineTo(this.size[0] - 6, y); + ctx.stroke(); + } + ctx.restore(); + }; + + // Initialise + syncSlots(getNumSlots()); + + node.addWidget("button", "+ Add Slot", null, () => { + const next = Math.min(getNumSlots() + 1, MAX_SLOTS); + setNumSlots(next); + syncSlots(next); + }); + + node.addWidget("button", "− Remove Slot", null, () => { + const next = Math.max(getNumSlots() - 1, MIN_SLOTS); + setNumSlots(next); + syncSlots(next); + }); + }, +}); diff --git a/nodes/__init__.py b/nodes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nodes/__pycache__/__init__.cpython-312.pyc b/nodes/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..6075604 Binary files /dev/null and b/nodes/__pycache__/__init__.cpython-312.pyc differ diff --git a/nodes/__pycache__/__init__.cpython-313.pyc b/nodes/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..dd4678f Binary files /dev/null and b/nodes/__pycache__/__init__.cpython-313.pyc differ diff --git a/nodes/__pycache__/image_aspect_size.cpython-312.pyc b/nodes/__pycache__/image_aspect_size.cpython-312.pyc new file mode 100644 index 0000000..1a30203 Binary files /dev/null and b/nodes/__pycache__/image_aspect_size.cpython-312.pyc differ diff --git a/nodes/__pycache__/image_aspect_size.cpython-313.pyc b/nodes/__pycache__/image_aspect_size.cpython-313.pyc new file mode 100644 index 0000000..a29a028 Binary files /dev/null and b/nodes/__pycache__/image_aspect_size.cpython-313.pyc differ diff --git a/nodes/__pycache__/multi_switch.cpython-312.pyc b/nodes/__pycache__/multi_switch.cpython-312.pyc new file mode 100644 index 0000000..6c43891 Binary files /dev/null and b/nodes/__pycache__/multi_switch.cpython-312.pyc differ diff --git a/nodes/__pycache__/multi_switch.cpython-313.pyc b/nodes/__pycache__/multi_switch.cpython-313.pyc new file mode 100644 index 0000000..c5fc91b Binary files /dev/null and b/nodes/__pycache__/multi_switch.cpython-313.pyc differ diff --git a/nodes/__pycache__/pixal3d.cpython-313.pyc b/nodes/__pycache__/pixal3d.cpython-313.pyc new file mode 100644 index 0000000..f7222b5 Binary files /dev/null and b/nodes/__pycache__/pixal3d.cpython-313.pyc differ diff --git a/nodes/image_aspect_size.py b/nodes/image_aspect_size.py new file mode 100644 index 0000000..998860c --- /dev/null +++ b/nodes/image_aspect_size.py @@ -0,0 +1,43 @@ +import torch + + +class ImageAspectSize: + TITLE = "Image Aspect Size" + CATEGORY = "JezzWTF/image" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "target_size": ("INT", { + "default": 1024, + "min": 64, + "max": 8192, + "step": 8, + "tooltip": "Longest side in pixels. The other dimension is calculated to preserve aspect ratio, snapped to multiples of 8.", + }), + "flip": ("BOOLEAN", { + "default": False, + "label_on": "Flipped (portrait↔landscape)", + "label_off": "Normal", + "tooltip": "Swap width and height before scaling — useful for rotating orientation without changing the image.", + }), + }, + } + + RETURN_TYPES = ("IMAGE", "INT", "INT") + RETURN_NAMES = ("IMAGE", "WIDTH", "HEIGHT") + FUNCTION = "calculate" + + def calculate(self, image: torch.Tensor, target_size: int, flip: bool) -> tuple[torch.Tensor, int, int]: + _, H, W, _ = image.shape + + if flip: + W, H = H, W + + scale = target_size / max(W, H) + width = round(W * scale / 8) * 8 + height = round(H * scale / 8) * 8 + + return (image, width, height) diff --git a/nodes/multi_switch.py b/nodes/multi_switch.py new file mode 100644 index 0000000..c9ed276 --- /dev/null +++ b/nodes/multi_switch.py @@ -0,0 +1,58 @@ +MAX_SLOTS = 6 + + +class AnyType(str): + """Compares equal to every type so ComfyUI accepts any connection.""" + + def __ne__(self, other: object) -> bool: + return False + + +ANY = AnyType("*") + + +class ContainsAnyDict(dict): + """An empty dict that reports containing any key. + Empty → ComfyUI renders no input slots from the Python definition. + __contains__ always True → any JS-added input name passes validation. + dict subclass → JSON serialisable (serialises as {}).""" + + def __contains__(self, key: object) -> bool: + return True + + def __getitem__(self, key: str) -> tuple: + return (ANY, {}) + + +class MultiSwitch: + TITLE = "Multi Switch" + CATEGORY = "JezzWTF/utils" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "switch": ("BOOLEAN", { + "default": False, + "label_on": "true", + "label_off": "false", + "tooltip": "Option A → false_N inputs pass through. Option B → true_N inputs pass through.", + }), + }, + "optional": ContainsAnyDict(), + "hidden": { + "num_slots": ("INT", {"default": 2}), + }, + } + + @classmethod + def VALIDATE_INPUTS(cls, input_types): + return True + + RETURN_TYPES = (ANY,) * MAX_SLOTS + RETURN_NAMES = tuple(f"output_{i}" for i in range(1, MAX_SLOTS + 1)) + FUNCTION = "execute" + + def execute(self, switch: bool, num_slots: int = 2, **kwargs) -> tuple: + side = "true" if switch else "false" + return tuple(kwargs.get(f"{side}_{i}", None) for i in range(1, MAX_SLOTS + 1)) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cbe508c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "comfyui-jezzwtf-nodes" +description = "JezzWTF custom nodes for ComfyUI — utility nodes." +version = "1.0.0" +license = { file = "LICENSE" } +dependencies = [] + +[project.urls] +Repository = "https://github.com/JezzWTF/comfyui-jezzwtf-nodes" + +[tool.comfy] +PublisherId = "jezzwtf" +DisplayName = "JezzWTF Nodes" +Icon = ""