File size: 5,109 Bytes
39d1e9f |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
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 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)
# 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" <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):
# 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("</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)
|