197 lines
6.6 KiB
Python
197 lines
6.6 KiB
Python
# "Design-focused node suite for ComfyUI."
|
|
# Copyright 2026 Augment Studio
|
|
# Augmentstudio.app
|
|
|
|
import torch
|
|
import numpy as np
|
|
from PIL import Image, ImageDraw
|
|
import json
|
|
import math
|
|
|
|
PHI = 1.6180339887
|
|
|
|
|
|
class CircleDrawNode:
|
|
"""
|
|
Draw construction circles. Three modes:
|
|
- fibonacci: reads spiral JSON and draws circles at each subdivision step
|
|
- single: one circle from manual center_x/center_y/radius
|
|
- concentric: multiple circles from center, scaled by golden ratio
|
|
"""
|
|
|
|
MODES = ["fibonacci", "single", "concentric"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"width": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 1}),
|
|
"height": ("INT", {"default": 632, "min": 64, "max": 8192, "step": 1}),
|
|
"mode": (cls.MODES, {"default": "fibonacci"}),
|
|
"center_x": ("INT", {"default": 512, "min": -8192, "max": 8192, "step": 1}),
|
|
"center_y": ("INT", {"default": 316, "min": -8192, "max": 8192, "step": 1}),
|
|
"radius": ("INT", {"default": 200, "min": 1, "max": 8192, "step": 1}),
|
|
"rings": ("INT", {"default": 5, "min": 1, "max": 12, "step": 1}),
|
|
"line_weight": ("INT", {"default": 1, "min": 1, "max": 20, "step": 1}),
|
|
"line_color": ("STRING", {"default": "#FFFFFF"}),
|
|
"dotted": ("BOOLEAN", {"default": False}),
|
|
"invert": ("BOOLEAN", {"default": False}),
|
|
},
|
|
"optional": {
|
|
"spiral_json": ("AUGMENT_JSON",),
|
|
"trigger": ("TRIGGER",),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "MASK", "AUGMENT_JSON", "TRIGGER")
|
|
RETURN_NAMES = ("image", "mask", "json_result", "trigger")
|
|
FUNCTION = "draw"
|
|
CATEGORY = "Augment/Grid"
|
|
|
|
@staticmethod
|
|
def _hex_to_rgb(hex_str):
|
|
hex_str = hex_str.lstrip("#")
|
|
if len(hex_str) == 3:
|
|
hex_str = "".join(c * 2 for c in hex_str)
|
|
return tuple(int(hex_str[i : i + 2], 16) for i in (0, 2, 4))
|
|
|
|
@staticmethod
|
|
def _draw_circle(draw, cx, cy, r, color, weight, dotted=False):
|
|
"""Draw a single circle, optionally dotted."""
|
|
if r < 1:
|
|
return
|
|
|
|
bbox = [cx - r, cy - r, cx + r, cy + r]
|
|
|
|
if not dotted:
|
|
draw.ellipse(bbox, outline=color, width=weight)
|
|
else:
|
|
# Draw dotted circle using small arcs
|
|
steps = max(36, int(r * 0.5))
|
|
gap = 360 / steps
|
|
for i in range(0, steps, 2):
|
|
start = i * gap
|
|
end = start + gap
|
|
draw.arc(bbox, start=start, end=end, fill=color, width=weight)
|
|
|
|
def draw(
|
|
self,
|
|
width,
|
|
height,
|
|
mode,
|
|
center_x,
|
|
center_y,
|
|
radius,
|
|
rings,
|
|
line_weight,
|
|
line_color,
|
|
dotted,
|
|
invert,
|
|
spiral_json=None,
|
|
trigger=None,
|
|
):
|
|
color = self._hex_to_rgb(line_color)
|
|
|
|
# Supersampling
|
|
ss = 2
|
|
sw, sh = width * ss, height * ss
|
|
slw = line_weight * ss
|
|
|
|
img = Image.new("RGBA", (sw, sh), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
circles_data = []
|
|
|
|
if mode == "fibonacci" and spiral_json:
|
|
# Parse spiral JSON and draw circles at each step
|
|
if isinstance(spiral_json, str):
|
|
spiral = json.loads(spiral_json)
|
|
else:
|
|
spiral = spiral_json
|
|
|
|
steps = spiral.get("steps", [])
|
|
for step in steps:
|
|
cx = step["arc_center"]["x"] * ss
|
|
cy = step["arc_center"]["y"] * ss
|
|
r = step["radius"] * ss
|
|
|
|
self._draw_circle(draw, cx, cy, r, color + (255,), slw, dotted)
|
|
|
|
# Also draw small crosshair at center
|
|
ch_size = max(4, slw * 2)
|
|
draw.line([(cx - ch_size, cy), (cx + ch_size, cy)], fill=color + (120,), width=max(1, slw // 2))
|
|
draw.line([(cx, cy - ch_size), (cx, cy + ch_size)], fill=color + (120,), width=max(1, slw // 2))
|
|
|
|
circles_data.append({
|
|
"center": {"x": step["arc_center"]["x"], "y": step["arc_center"]["y"]},
|
|
"radius": step["radius"],
|
|
"iteration": step["iteration"],
|
|
})
|
|
|
|
elif mode == "concentric":
|
|
# Draw rings scaling by golden ratio from center
|
|
cx = center_x * ss
|
|
cy = center_y * ss
|
|
r = radius * ss
|
|
|
|
for i in range(rings):
|
|
current_r = int(r)
|
|
self._draw_circle(draw, cx, cy, current_r, color + (255,), slw, dotted)
|
|
|
|
# Crosshair at center (only on first)
|
|
if i == 0:
|
|
ch_size = max(6, slw * 3)
|
|
draw.line([(cx - ch_size, cy), (cx + ch_size, cy)], fill=color + (120,), width=max(1, slw // 2))
|
|
draw.line([(cx, cy - ch_size), (cx, cy + ch_size)], fill=color + (120,), width=max(1, slw // 2))
|
|
|
|
circles_data.append({
|
|
"center": {"x": center_x, "y": center_y},
|
|
"radius": int(current_r // ss),
|
|
"ring": i,
|
|
"scale": round(1 / (PHI ** i), 4),
|
|
})
|
|
|
|
r = r / PHI
|
|
|
|
else:
|
|
# Single circle
|
|
cx = center_x * ss
|
|
cy = center_y * ss
|
|
r = radius * ss
|
|
|
|
self._draw_circle(draw, cx, cy, int(r), color + (255,), slw, dotted)
|
|
|
|
# Crosshair
|
|
ch_size = max(6, slw * 3)
|
|
draw.line([(cx - ch_size, cy), (cx + ch_size, cy)], fill=color + (120,), width=max(1, slw // 2))
|
|
draw.line([(cx, cy - ch_size), (cx, cy + ch_size)], fill=color + (120,), width=max(1, slw // 2))
|
|
|
|
circles_data.append({
|
|
"center": {"x": center_x, "y": center_y},
|
|
"radius": radius,
|
|
})
|
|
|
|
# Downsample
|
|
img = img.resize((width, height), Image.LANCZOS)
|
|
|
|
# Build output
|
|
result = {
|
|
"mode": mode,
|
|
"canvas": [width, height],
|
|
"circles": circles_data,
|
|
}
|
|
json_result = json.dumps(result, indent=2)
|
|
|
|
# Convert
|
|
img_np = np.array(img)
|
|
alpha = img_np[:, :, 3].astype(np.float32) / 255.0
|
|
if invert:
|
|
alpha = 1.0 - alpha
|
|
|
|
rgb = img_np[:, :, :3].astype(np.float32) / 255.0
|
|
image_tensor = torch.from_numpy(rgb).unsqueeze(0)
|
|
mask_tensor = torch.from_numpy(alpha).unsqueeze(0)
|
|
|
|
return (image_tensor, mask_tensor, json_result, trigger)
|
|
|