"""How a routed wire looks: rounded bends, hops where wires cross, dots where they split. The router in wiresheet.py hands over orthogonal polylines and nothing else. Everything here is pure geometry on those point lists -- no canvas, no app state -- so the styling can change without touching how wires find their way around the boxes. """ import math CORNER_RADIUS = 6 # bend radius, shrunk on short segments so two bends never overlap CORNER_STEPS = 5 # points per bend; enough to read as a curve at every zoom level HOP_HALF_WIDTH = 4 # a hop spans this far either side of the wire it jumps over HOP_HEIGHT = 4 HOP_STEPS = 6 HOP_MERGE_GAP = 2 # hops closer than this run together into one wider hop HOP_END_MARGIN = 10 # no hop this close to a target, where the arrowhead sits JUNCTION_RADIUS = 3 EPS = 0.5 def simplify(points): """Drops repeated points and the middle of any straight run. The router emits a fixed number of points per route shape, so a path often carries a "bend" that does not turn at all. Rounding one of those would put a kink in a straight line. """ out = [] for point in points: if out and _same(out[-1], point): continue if len(out) >= 2 and _collinear(out[-2], out[-1], point): out[-1] = point else: out.append(point) return out def find_hops(paths): """Where each wire has to hop: one dict per path, segment index -> crossing xs. A horizontal run hops over a vertical run of a different wire it crosses. Only true crossings count: a wire touching another at a bend or an end is a junction, and a hop there would hide it. """ horizontals, verticals = [], [] for path_index, points in enumerate(paths): for seg_index, (a, b) in enumerate(zip(points, points[1:])): if abs(a[1] - b[1]) < EPS: horizontals.append((path_index, seg_index, a[1], min(a[0], b[0]), max(a[0], b[0]))) else: verticals.append((path_index, a[0], min(a[1], b[1]), max(a[1], b[1]))) hops = [{} for _ in paths] for path_index, seg_index, y, x1, x2 in horizontals: for other, x, y1, y2 in verticals: if other == path_index: continue if x1 + EPS < x < x2 - EPS and y1 + EPS < y < y2 - EPS: hops[path_index].setdefault(seg_index, []).append(x) return hops def fan_out_junctions(paths_by_owner): """Points where wires leaving one output pin go their separate ways. Every pair of wires from the same pin shares a run out of it, and some share a trunk further on. Where the two stop running together is a branch, and a branch without a dot reads as two wires merely crossing. """ points = set() for paths in paths_by_owner.values(): for i, first in enumerate(paths): for second in paths[i + 1:]: split = _divergence(first, second) if split is None: continue if any(_same(split, end) for end in (first[0], first[-1], second[-1])): continue points.add((round(split[0], 1), round(split[1], 1))) return points def styled_points(points, hops=None): """The points to draw for a simplified orthogonal path, bends rounded and hops in.""" hops = hops or {} count = len(points) if count < 3: return list(points) lengths = [_length(a, b) for a, b in zip(points, points[1:])] radii = [0.0] * count for i in range(1, count - 1): radii[i] = min(CORNER_RADIUS, lengths[i - 1] / 2, lengths[i] / 2) out = [points[0]] for i in range(count - 1): a, b = points[i], points[i + 1] ux, uy = _unit(a, b) start = (a[0] + ux * radii[i], a[1] + uy * radii[i]) end = (b[0] - ux * radii[i + 1], b[1] - uy * radii[i + 1]) if i in hops: end_margin = HOP_END_MARGIN if i == count - 2 else 0 out.extend(_hop_points(start, end, hops[i], end_margin)) out.append(end) if i + 1 < count - 1: out.extend(_corner_points(end, b, points[i + 2], radii[i + 1])) return out # --- helpers --- def _same(p, q): return abs(p[0] - q[0]) < EPS and abs(p[1] - q[1]) < EPS def _collinear(p, q, r): return ((abs(p[0] - q[0]) < EPS and abs(q[0] - r[0]) < EPS) or (abs(p[1] - q[1]) < EPS and abs(q[1] - r[1]) < EPS)) def _length(a, b): return abs(b[0] - a[0]) + abs(b[1] - a[1]) # orthogonal, so one term is zero def _unit(a, b): length = _length(a, b) if length < EPS: return 0.0, 0.0 return (b[0] - a[0]) / length, (b[1] - a[1]) / length def _corner_points(entry, corner, after, radius): """A bend from `entry` round `corner` towards `after`, as a quadratic curve.""" if radius < EPS: return [] vx, vy = _unit(corner, after) leave = (corner[0] + vx * radius, corner[1] + vy * radius) out = [] for step in range(1, CORNER_STEPS + 1): t = step / CORNER_STEPS s = 1 - t out.append((s * s * entry[0] + 2 * s * t * corner[0] + t * t * leave[0], s * s * entry[1] + 2 * s * t * corner[1] + t * t * leave[1])) return out def _hop_points(start, end, xs, end_margin): """The bumps along a horizontal run from `start` to `end`, in the order travelled.""" y = start[1] direction = 1 if end[0] >= start[0] else -1 lo = min(start[0], end[0]) + HOP_HALF_WIDTH + 1 hi = max(start[0], end[0]) - HOP_HALF_WIDTH - 1 if direction > 0: hi -= end_margin else: lo += end_margin ordered = sorted((x for x in xs if lo <= x <= hi), key=lambda x: x * direction) groups = [] for x in ordered: if groups and abs(x - groups[-1][-1]) < 2 * HOP_HALF_WIDTH + HOP_MERGE_GAP: groups[-1].append(x) else: groups.append([x]) out = [] for group in groups: x_from = group[0] - direction * HOP_HALF_WIDTH x_to = group[-1] + direction * HOP_HALF_WIDTH for step in range(HOP_STEPS + 1): t = step / HOP_STEPS out.append((x_from + (x_to - x_from) * t, y - HOP_HEIGHT * math.sin(math.pi * t))) return out def _divergence(first, second): """The last point two paths from the same start share before they part, or None.""" if not _same(first[0], second[0]): return None pos = first[0] i = j = 0 while i < len(first) - 1 and j < len(second) - 1: d1, d2 = _unit(pos, first[i + 1]), _unit(pos, second[j + 1]) if _same(pos, first[i + 1]): i += 1 continue if _same(pos, second[j + 1]): j += 1 continue if not _same(d1, d2): return pos step = min(_length(pos, first[i + 1]), _length(pos, second[j + 1])) pos = (pos[0] + d1[0] * step, pos[1] + d1[1] * step) return pos