"""Pure-geometry orthogonal routing and crossing detection. Kept free of Qt imports so it can be unit-tested independently. Points are ``(x, y)`` float tuples; segments are ``(p0, p1)`` axis-aligned pairs. """ from __future__ import annotations from typing import Iterable Point = tuple[float, float] Segment = tuple[Point, Point] def _snap(v: float, grid: int) -> float: if grid <= 0: return v return round(v / grid) * grid def orthogonal_route( start: Point, start_normal: Point, end: Point, end_normal: Point, *, grid: int = 20, stub: int | None = None, ) -> list[Point]: """Compute an axis-aligned poly-line from ``start`` to ``end``. The path leaves ``start`` along ``start_normal`` and arrives at ``end`` against ``end_normal``, turning through a mid-line. Endpoints are exact; intermediate bends are snapped to ``grid``. """ if stub is None: stub = grid sx, sy = start ex, ey = end snx, sny = start_normal enx, eny = end_normal s1 = (sx + snx * stub, sy + sny * stub) e1 = (ex + enx * stub, ey + eny * stub) pts: list[Point] = [(sx, sy), s1] if snx != 0: # start exits horizontally -> pivot on a vertical mid-line midx = _snap((s1[0] + e1[0]) / 2.0, grid) pts += [(midx, s1[1]), (midx, e1[1])] else: # start exits vertically -> pivot on a horizontal mid-line midy = _snap((s1[1] + e1[1]) / 2.0, grid) pts += [(s1[0], midy), (e1[0], midy)] pts += [e1, (ex, ey)] return _cleanup(pts) def _cleanup(pts: list[Point], eps: float = 1e-6) -> list[Point]: """Drop duplicate and collinear intermediate points.""" out: list[Point] = [] for p in pts: if out and abs(p[0] - out[-1][0]) < eps and abs(p[1] - out[-1][1]) < eps: continue out.append(p) # remove collinear middles cleaned: list[Point] = [] for i, p in enumerate(out): if 0 < i < len(out) - 1: a, b = out[i - 1], out[i + 1] # collinear if all three share an x or all share a y if (abs(a[0] - p[0]) < eps and abs(p[0] - b[0]) < eps) or \ (abs(a[1] - p[1]) < eps and abs(p[1] - b[1]) < eps): continue cleaned.append(p) return cleaned def polyline_segments(points: Iterable[Point]) -> list[Segment]: pts = list(points) return [(pts[i], pts[i + 1]) for i in range(len(pts) - 1)] def _is_horizontal(seg: Segment, eps: float = 1e-6) -> bool: return abs(seg[0][1] - seg[1][1]) < eps def _is_vertical(seg: Segment, eps: float = 1e-6) -> bool: return abs(seg[0][0] - seg[1][0]) < eps def segment_crossings( path: list[Segment], others: list[Segment], *, eps: float = 1e-6, ) -> list[Point]: """Return true perpendicular crossing points between ``path`` and ``others``. Only counts a horizontal segment crossing a vertical one (or vice versa) strictly in the interior of both — shared endpoints and overlaps are ignored. Used to render arc "hops" where pipes cross. """ crossings: list[Point] = [] for a in path: for b in others: pt = _perpendicular_crossing(a, b, eps) if pt is not None: crossings.append(pt) return crossings def _perpendicular_crossing(a: Segment, b: Segment, eps: float) -> Point | None: if _is_horizontal(a) and _is_vertical(b): h, v = a, b elif _is_vertical(a) and _is_horizontal(b): v, h = a, b else: return None hy = h[0][1] vx = v[0][0] hx0, hx1 = sorted((h[0][0], h[1][0])) vy0, vy1 = sorted((v[0][1], v[1][1])) # strictly interior on both segments (avoid endpoints / T-joins) if hx0 + eps < vx < hx1 - eps and vy0 + eps < hy < vy1 - eps: return (vx, hy) return None