from __future__ import annotations import json import os from typing import Iterable, List, Optional, Sequence, Tuple from alphageometry.modules import utils Point = Tuple[float, float] Triangle = Tuple[int, int, int] def _scale_and_translate(points: Iterable[Point], width: float = 400, height: float = 400, margin: float = 8.0): pts = list(points) if not pts: return [], width, height (minx, miny), (maxx, maxy) = utils.bounding_box(pts) w = maxx - minx h = maxy - miny if w == 0 and h == 0: # single point return [(width / 2.0, height / 2.0)], width, height sx = (width - 2 * margin) / (w if w != 0 else 1.0) sy = (height - 2 * margin) / (h if h != 0 else 1.0) s = min(sx, sy) out = [((p[0] - minx) * s + margin, (maxy - p[1]) * s + margin) for p in pts] return out, width, height def polygon_to_svg( poly: Iterable[Point], stroke: str = "black", fill: str = "none", width: int = 400, height: int = 400, label: Optional[str] = None, show_vertex_labels: bool = False, ) -> str: """Render a single polygon to an SVG string. Parameters: - label: optional text label for the polygon (renders at centroid) - show_vertex_labels: when True, each vertex is annotated with its index """ pts = list(poly) scaled, w, h = _scale_and_translate(pts, width=width, height=height) path = " ".join(f"{x:.2f},{y:.2f}" for x, y in scaled) svg = [f""] svg.append(f" ") if label is not None and pts: cx, cy = utils.polygon_centroid(pts) # scale centroid into svg coords scaled_centroid, _, _ = _scale_and_translate([(cx, cy)], width=w, height=h) if scaled_centroid: sx, sy = scaled_centroid[0] svg.append(f" {label}") if show_vertex_labels and pts: for i, (x, y) in enumerate(scaled): svg.append(f" {i}") svg.append("") return "\n".join(svg) def mesh_to_svg( vertices: Iterable[Point], triangles: Iterable[Triangle], stroke: str = "black", fill: str = "none", width: int = 600, height: int = 600, show_triangle_labels: bool = False, ) -> str: verts = list(vertices) tri = list(triangles) scaled, w, h = _scale_and_translate(verts, width=width, height=height) svg = [f""] svg.append("") for t_idx, (a, b, c) in enumerate(tri): if a < 0 or b < 0 or c < 0 or a >= len(scaled) or b >= len(scaled) or c >= len(scaled): continue xa, ya = scaled[a] xb, yb = scaled[b] xc, yc = scaled[c] svg.append(f" ") if show_triangle_labels: tx = (xa + xb + xc) / 3.0 ty = (ya + yb + yc) / 3.0 svg.append(f" {t_idx}") svg.append("") svg.append("") return "\n".join(svg) def render_scene( polygons: Optional[Sequence[Iterable[Point]]] = None, meshes: Optional[Sequence[Tuple[Iterable[Point], Iterable[Triangle]]]] = None, width: int = 800, height: int = 600, background: str = "white", ) -> str: """Render multiple polygons and meshes into a single SVG scene. This composes objects into one SVG canvas. Each polygon/mesh will be scaled independently to the full canvas; callers who need consistent coordinates should pre-scale externally. """ svg = [f""] svg.append(f" ") if polygons: for i, poly in enumerate(polygons): # small inset to avoid overlay issues svg.append(polygon_to_svg(poly, width=width, height=height, label=str(i))) if meshes: for i, (verts, tris) in enumerate(meshes): svg.append(mesh_to_svg(verts, tris, width=width, height=height, show_triangle_labels=False)) svg.append("") return "\n".join(svg) def write_svg(path: str, svg_text: str) -> None: """Write the SVG string to disk, creating parent directories if needed.""" os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding="utf8") as f: f.write(svg_text) def mesh_to_json(vertices: Iterable[Point], triangles: Iterable[Triangle]) -> str: payload = {"vertices": [list(v) for v in vertices], "triangles": [list(t) for t in triangles]} return json.dumps(payload)