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
This commit is contained in:
+16
@@ -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"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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);
|
||||
});
|
||||
},
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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 = ""
|
||||
Reference in New Issue
Block a user