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)