|
|
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:
|
|
|
|
|
|
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 xmlns='http://www.w3.org/2000/svg' width='{w}' height='{h}'>"]
|
|
|
svg.append(f" <polygon points='{path}' stroke='{stroke}' fill='{fill}' stroke-width='1' />")
|
|
|
if label is not None and pts:
|
|
|
cx, cy = utils.polygon_centroid(pts)
|
|
|
|
|
|
scaled_centroid, _, _ = _scale_and_translate([(cx, cy)], width=w, height=h)
|
|
|
if scaled_centroid:
|
|
|
sx, sy = scaled_centroid[0]
|
|
|
svg.append(f" <text x='{sx:.2f}' y='{sy:.2f}' font-size='12' fill='black'>{label}</text>")
|
|
|
if show_vertex_labels and pts:
|
|
|
for i, (x, y) in enumerate(scaled):
|
|
|
svg.append(f" <text x='{x:.2f}' y='{y:.2f}' font-size='10' fill='red'>{i}</text>")
|
|
|
svg.append("</svg>")
|
|
|
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 xmlns='http://www.w3.org/2000/svg' width='{w}' height='{h}'>"]
|
|
|
svg.append("<g fill='none' stroke='black' stroke-width='1'>")
|
|
|
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" <path d='M {xa:.2f} {ya:.2f} L {xb:.2f} {yb:.2f} L {xc:.2f} {yc:.2f} Z' stroke='{stroke}' fill='{fill}' />")
|
|
|
if show_triangle_labels:
|
|
|
tx = (xa + xb + xc) / 3.0
|
|
|
ty = (ya + yb + yc) / 3.0
|
|
|
svg.append(f" <text x='{tx:.2f}' y='{ty:.2f}' font-size='10' fill='blue'>{t_idx}</text>")
|
|
|
svg.append("</g>")
|
|
|
svg.append("</svg>")
|
|
|
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 xmlns='http://www.w3.org/2000/svg' width='{width}' height='{height}'>"]
|
|
|
svg.append(f" <rect width='100%' height='100%' fill='{background}' />")
|
|
|
if polygons:
|
|
|
for i, poly in enumerate(polygons):
|
|
|
|
|
|
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("</svg>")
|
|
|
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)
|
|
|
|