"""Draw what the ball will trace, as a PNG (top view, looking down through the glass).""" import math from PIL import Image, ImageDraw COLORS = {'pattern': (60, 45, 30), 'erase': (205, 190, 165), 'travel': (200, 60, 60)} SAND = (236, 222, 196) def render(segments, rho_max_mm, path, size=900, invert_theta=False, transparent=False): """segments: [(kind, [(theta_deg, rho_mm), ...])] in machine units. transparent: no square background behind the sand disc (for the web page).""" im = Image.new('RGBA', (size, size), (0, 0, 0, 0)) if transparent else Image.new('RGB', (size, size), (250, 250, 248)) d = ImageDraw.Draw(im) c = size / 2 s = (size / 2 - 12) / rho_max_mm d.ellipse([c - rho_max_mm * s - 6, c - rho_max_mm * s - 6, c + rho_max_mm * s + 6, c + rho_max_mm * s + 6], fill=SAND, outline=(150, 135, 110), width=3) sign = -1 if invert_theta else 1 # show the pattern as it should look, not as the motor turns for kind, pts in segments: xy = [(c + r * s * math.cos(math.radians(sign * t)), c - r * s * math.sin(math.radians(sign * t))) for t, r in pts] if len(xy) > 1: d.line(xy, fill=COLORS.get(kind, (0, 0, 0)), width=max(1, size // 400) if kind == 'pattern' else 1) im.save(path) return path def sheet(jobs, rho_max_mm, path, tile=420, invert_theta=False, cols=3): """One tile per pattern (each drawn after its erase), with the name under it.""" import os import tempfile n = len(jobs) cols = min(cols, n) rows = (n + cols - 1) // cols out = Image.new('RGB', (cols * tile, rows * (tile + 28)), (250, 250, 248)) d = ImageDraw.Draw(out) with tempfile.TemporaryDirectory() as tmp: for i, (name, segs) in enumerate(jobs): f = os.path.join(tmp, f'{i}.png') render([s for s in segs if s[0] != 'erase'], rho_max_mm, f, size=tile, invert_theta=invert_theta) x, y = (i % cols) * tile, (i // cols) * (tile + 28) out.paste(Image.open(f), (x, y)) d.text((x + 12, y + tile + 6), name, fill=(40, 40, 40)) out.save(path) return path