Files

197 lines
6.2 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
class LineDrawNode:
"""
Draw a line from point A to point B.
Outputs image overlay and mask. Supersampled 2x for clean anti-aliased lines.
"""
PRESETS = [
"custom",
"vertical_center",
"horizontal_center",
"diagonal_left",
"diagonal_right",
"outline",
]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 1}),
"height": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 1}),
"preset": (cls.PRESETS, {"default": "diagonal_left"}),
"x1": ("INT", {"default": 0, "min": -8192, "max": 8192, "step": 1}),
"y1": ("INT", {"default": 0, "min": -8192, "max": 8192, "step": 1}),
"x2": ("INT", {"default": 1024, "min": -8192, "max": 8192, "step": 1}),
"y2": ("INT", {"default": 1024, "min": -8192, "max": 8192, "step": 1}),
"line_weight": ("INT", {"default": 2, "min": 1, "max": 20, "step": 1}),
"line_color": ("STRING", {"default": "#FFFFFF"}),
"extend": ("BOOLEAN", {"default": False}),
"invert": ("BOOLEAN", {"default": False}),
},
"optional": {
"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 _extend_line(x1, y1, x2, y2, w, h):
"""Extend line to canvas edges."""
dx = x2 - x1
dy = y2 - y1
if dx == 0 and dy == 0:
return x1, y1, x2, y2
# Find parameter t where line hits each edge
# line: (x1 + t*dx, y1 + t*dy)
t_values = []
if dx != 0:
# Left edge: x = 0
t = -x1 / dx
y_at = y1 + t * dy
if 0 <= y_at <= h:
t_values.append(t)
# Right edge: x = w
t = (w - x1) / dx
y_at = y1 + t * dy
if 0 <= y_at <= h:
t_values.append(t)
if dy != 0:
# Top edge: y = 0
t = -y1 / dy
x_at = x1 + t * dx
if 0 <= x_at <= w:
t_values.append(t)
# Bottom edge: y = h
t = (h - y1) / dy
x_at = x1 + t * dx
if 0 <= x_at <= w:
t_values.append(t)
if len(t_values) < 2:
return x1, y1, x2, y2
t_min = min(t_values)
t_max = max(t_values)
ex1 = int(x1 + t_min * dx)
ey1 = int(y1 + t_min * dy)
ex2 = int(x1 + t_max * dx)
ey2 = int(y1 + t_max * dy)
return ex1, ey1, ex2, ey2
def draw(self, width, height, preset, x1, y1, x2, y2, line_weight, line_color, extend, invert, trigger=None):
color = self._hex_to_rgb(line_color)
# Resolve preset into coordinates
lines = [] # list of (x1, y1, x2, y2) — outline draws 4 lines
if preset == "vertical_center":
mx = width // 2
lines = [(mx, 0, mx, height)]
elif preset == "horizontal_center":
my = height // 2
lines = [(0, my, width, my)]
elif preset == "diagonal_left":
lines = [(0, 0, width, height)]
elif preset == "diagonal_right":
lines = [(width, 0, 0, height)]
elif preset == "outline":
lines = [
(0, 0, width - 1, 0),
(width - 1, 0, width - 1, height - 1),
(width - 1, height - 1, 0, height - 1),
(0, height - 1, 0, 0),
]
else:
# custom — use raw x1/y1/x2/y2
lines = [(x1, y1, x2, y2)]
# 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)
drawn_lines = []
for lx1, ly1, lx2, ly2 in lines:
sx1, sy1, sx2, sy2 = lx1 * ss, ly1 * ss, lx2 * ss, ly2 * ss
if extend and preset == "custom":
sx1, sy1, sx2, sy2 = self._extend_line(sx1, sy1, sx2, sy2, sw, sh)
draw.line([(sx1, sy1), (sx2, sy2)], fill=color + (255,), width=slw)
# Track actual coords for JSON
ax1, ay1, ax2, ay2 = sx1 // ss, sy1 // ss, sx2 // ss, sy2 // ss
dx = ax2 - ax1
dy = ay2 - ay1
length = math.sqrt(dx * dx + dy * dy)
angle = math.degrees(math.atan2(dy, dx))
drawn_lines.append({
"a": {"x": ax1, "y": ay1},
"b": {"x": ax2, "y": ay2},
"midpoint": {"x": (ax1 + ax2) // 2, "y": (ay1 + ay2) // 2},
"length": round(length, 2),
"angle": round(angle, 2),
})
# Downsample
img = img.resize((width, height), Image.LANCZOS)
result = {
"preset": preset,
"canvas": [width, height],
"lines": drawn_lines,
}
if extend and preset == "custom":
ex1, ey1, ex2, ey2 = self._extend_line(x1, y1, x2, y2, width, height)
result["extended"] = {
"a": {"x": ex1, "y": ey1},
"b": {"x": ex2, "y": ey2},
}
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)