Sight groups, DM reveal areas (cones), client-side image shrink for map upload

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
Claude
2026-10-24 18:33:08 +00:00
parent 6aac03f112
commit ab3c35aab7
8 changed files with 243 additions and 29 deletions
+21 -7
View File
@@ -11,8 +11,9 @@ from PIL import Image, ImageDraw
EPS = 1e-5
def polygon(origin, segs, w, h, radius=0.0):
"""Points of the area visible from origin. segs: [(x1, y1, x2, y2)] that block sight; radius 0 means unlimited."""
def polygon(origin, segs, w, h, radius=0.0, sector=None):
"""Points of the area visible from origin. segs: [(x1, y1, x2, y2)] that block sight; radius 0 means unlimited.
sector = (direction, half angle) in radians limits the view to a cone."""
ox, oy = origin
border = [(0, 0, w, 0), (w, 0, w, h), (w, h, 0, h), (0, h, 0, 0)]
if radius:
@@ -34,7 +35,13 @@ def polygon(origin, segs, w, h, radius=0.0):
angles += (a - EPS, a, a + EPS)
if radius:
angles += [i * (2 * math.pi / 120) for i in range(120)]
angles.sort()
if sector:
d, half = sector
rel = [(a - d + math.pi) % (2 * math.pi) - math.pi for a in angles]
rel = [a for a in rel if abs(a) <= half] + [-half, half] + [-half + i * (2 * half / 48) for i in range(49)]
angles = [d + a for a in sorted(rel)]
else:
angles.sort()
pts = []
for a in angles:
dx, dy = math.cos(a), math.sin(a)
@@ -54,7 +61,7 @@ def polygon(origin, segs, w, h, radius=0.0):
if best == float("inf"):
continue
pts.append((ox + dx * best, oy + dy * best))
return pts
return [(ox, oy)] + pts if sector else pts
def inside(pt, poly) -> bool:
@@ -112,19 +119,26 @@ def source(path):
return im
def cut(path, poly, key):
def inside_any(pt, polys) -> bool:
return any(inside(pt, p) for p in polys)
def cut(path, polys, key):
"""The part of the image inside the polygon (everything else black), cropped to its bounding box.
Returns (webp bytes, (x, y, w, h))."""
hit = _cuts.fetch(key)
if hit:
return hit
im = source(path)
xs, ys = [p[0] for p in poly], [p[1] for p in poly]
xs, ys = [p[0] for poly in polys for p in poly], [p[1] for poly in polys for p in poly]
x0, y0 = max(0, int(math.floor(min(xs)))), max(0, int(math.floor(min(ys))))
x1, y1 = min(im.width, int(math.ceil(max(xs)))), min(im.height, int(math.ceil(max(ys))))
w, h = max(1, x1 - x0), max(1, y1 - y0)
mask = Image.new("L", (w, h), 0)
ImageDraw.Draw(mask).polygon([(x - x0, y - y0) for x, y in poly], fill=255)
draw = ImageDraw.Draw(mask)
for poly in polys:
if len(poly) > 2:
draw.polygon([(x - x0, y - y0) for x, y in poly], fill=255)
piece = Image.composite(im.crop((x0, y0, x0 + w, y0 + h)), Image.new("RGB", (w, h), (0, 0, 0)), mask)
out = io.BytesIO()
piece.save(out, "WEBP", quality=82, method=1)