will702's picture
Upload folder using huggingface_hub
eab734a verified
Raw
History Blame Contribute Delete
4.39 kB
"""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})",
)