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