197 lines
6.2 KiB
Python
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) |