Wiresheet wires: rounded bends, hops, junction dots and grid spacing
Wires now round their bends, hop over the wires they cross, and show a dot where wires from one output split. Wires sharing a pin may share a trunk. Routes keep one grid square from a box's top and bottom and two from its sides, never turn closer than two squares beside a box, and keep half a square from other wires. The runs pinned to every slot are claimed before any wire is routed, so an early wire no longer lays its run along a row a later slot has to leave by. A wire that cannot fit the grid spacing falls back to the old tighter one rather than turning into knobs. The styling geometry lives in wire_style.py; draw_links is split into collecting, routing and painting around a WireJob dataclass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+199
@@ -0,0 +1,199 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user