File size: 4,386 Bytes
eab734a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
131
132
133
134
135
136
137
"""Schematic layout fallback when the exact solver fails."""

from __future__ import annotations

import math

import numpy as np

from statement_to_tikz.ir import (
    Collinear,
    GeometryIR,
    Intersection,
    Midpoint,
    OnCircle,
    OnLine,
    SolveMode,
    SolvedScene,
)
from statement_to_tikz.solve import (
    _circle_geometry,
    evaluate_residuals,
    scene_from_coordinates,
)


def _project_to_line(
    p: np.ndarray, a: np.ndarray, b: np.ndarray
) -> np.ndarray:
    ab = b - a
    L2 = float(np.dot(ab, ab))
    if L2 < 1e-16:
        return a.copy()
    t = float(np.dot(p - a, ab) / L2)
    return a + t * ab


def _initial_placement(ir: GeometryIR) -> dict[str, tuple[float, float]]:
    coords: dict[str, tuple[float, float]] = {}
    n = len(ir.points)
    for i, p in enumerate(ir.points):
        if p.hint is not None:
            coords[p.name] = (float(p.hint[0]), float(p.hint[1]))
        else:
            ang = 2 * math.pi * i / max(n, 1) - math.pi / 2
            r = 1.0 + 0.05 * i
            coords[p.name] = (r * math.cos(ang), r * math.sin(ang))
    return coords


def _repair_once(
    ir: GeometryIR, coords: dict[str, tuple[float, float]]
) -> dict[str, tuple[float, float]]:
    out = {k: (float(v[0]), float(v[1])) for k, v in coords.items()}

    def setp(name: str, xy: np.ndarray) -> None:
        out[name] = (float(xy[0]), float(xy[1]))

    for c in ir.constraints:
        if isinstance(c, Midpoint) and c.a in out and c.b in out:
            a = np.asarray(out[c.a])
            b = np.asarray(out[c.b])
            setp(c.point, 0.5 * (a + b))
        elif isinstance(c, OnLine) and c.a in out and c.b in out and c.point in out:
            p = np.asarray(out[c.point])
            a = np.asarray(out[c.a])
            b = np.asarray(out[c.b])
            setp(c.point, _project_to_line(p, a, b))
        elif isinstance(c, Collinear) and all(p in out for p in c.points):
            a = np.asarray(out[c.points[0]])
            b = np.asarray(out[c.points[1]])
            for name in c.points[2:]:
                p = np.asarray(out[name])
                setp(name, _project_to_line(p, a, b))
        elif isinstance(c, Intersection) and all(
            p in out for p in (c.a, c.b, c.c, c.d)
        ):
            # line-line intersection
            a = np.asarray(out[c.a], dtype=float)
            b = np.asarray(out[c.b], dtype=float)
            cc = np.asarray(out[c.c], dtype=float)
            d = np.asarray(out[c.d], dtype=float)
            ab = b - a
            cd = d - cc
            mat = np.array([ab, -cd]).T
            det = float(np.linalg.det(mat))
            if abs(det) > 1e-12:
                t = float(np.linalg.solve(mat, cc - a)[0])
                setp(c.point, a + t * ab)
        elif isinstance(c, OnCircle) and c.point in out:
            geom = _circle_geometry(ir, out, c.circle)
            if geom is not None:
                center, rad = geom
                p = np.asarray(out[c.point])
                v = p - center
                n = float(np.linalg.norm(v))
                if n < 1e-12:
                    v = np.array([rad, 0.0])
                else:
                    v = v / n * rad
                setp(c.point, center + v)
    return out


def layout_schematic(
    ir: GeometryIR,
    *,
    iterations: int = 40,
    base: dict[str, tuple[float, float]] | None = None,
) -> SolvedScene:
    """Produce a labeled schematic that soft-enforces incidences."""
    coords = base if base is not None else _initial_placement(ir)
    for _ in range(iterations):
        coords = _repair_once(ir, coords)

    # Normalize: centroid to origin, scale median radius to 1
    if coords:
        pts = np.array(list(coords.values()))
        center = pts.mean(axis=0)
        pts = pts - center
        radii = np.linalg.norm(pts, axis=1)
        scale = float(np.median(radii)) if len(radii) else 1.0
        if scale < 1e-9:
            scale = 1.0
        coords = {
            k: (float((np.asarray(v) - center)[0] / scale), float((np.asarray(v) - center)[1] / scale))
            for k, v in coords.items()
        }

    res = evaluate_residuals(ir, coords)
    max_r = max(res) if res else 0.0
    return scene_from_coordinates(
        ir,
        coords,
        mode=SolveMode.schematic,
        message=f"schematic layout (max residual {max_r:.4g})",
    )