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:
+1
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
|||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
|
||||||
VERSION = "0.0.0.160"
|
VERSION = "0.0.0.161"
|
||||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||||
DEFAULT_GRID_SIZE = 10
|
DEFAULT_GRID_SIZE = 10
|
||||||
DEFAULT_ADD_TIMESTAMP = True
|
DEFAULT_ADD_TIMESTAMP = True
|
||||||
|
|||||||
+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
|
||||||
+351
-215
@@ -5,27 +5,39 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import tkinter.font as tkfont
|
import tkinter.font as tkfont
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import app_state
|
import app_state
|
||||||
import live_hooks
|
import live_hooks
|
||||||
import schema
|
import schema
|
||||||
import undo
|
import undo
|
||||||
|
import wire_style
|
||||||
from sax_dom import (get_component_path, read_meta_property, remove_prop, set_prop_value,
|
from sax_dom import (get_component_path, read_meta_property, remove_prop, set_prop_value,
|
||||||
update_meta_property)
|
update_meta_property)
|
||||||
|
|
||||||
|
|
||||||
LINK_STUB = 8 # straight run leaving a slot before the wire is allowed to turn
|
|
||||||
LINK_CLEAR = 5 # gap kept between a horizontal wire lane and any box above or below
|
|
||||||
LINK_EPS = 0.5 # tolerance so a wire touching its own box edge is not a collision
|
LINK_EPS = 0.5 # tolerance so a wire touching its own box edge is not a collision
|
||||||
|
|
||||||
# Vertical wire runs. A run hugs the box it leaves or enters instead of drifting down the
|
# How far a wire keeps from the boxes it passes, in grid squares. A wire stays one
|
||||||
# middle of the gap, but may not crowd it: WIRE_HUG is where it sits, WIRE_MIN_CLEAR is
|
# square clear of a box's top and bottom and two clear of its sides, and it never turns
|
||||||
# the closest it is ever allowed to get, and two runs sharing one gap stand
|
# closer than two squares to the side of a box -- including the turn just after leaving
|
||||||
# WIRE_LANE_GAP apart. A gap therefore has to be WIRE_HUG either side plus one
|
# a slot and the last one before arriving at one. Two wires running side by side, either
|
||||||
# WIRE_LANE_GAP per extra run, which is why TIDY_GAP below is exactly 2 * WIRE_HUG.
|
# way, stand at least half a square apart.
|
||||||
|
CLEAR_SIDE_GRIDS = 2
|
||||||
|
CLEAR_END_GRIDS = 1
|
||||||
|
CLEAR_WIRE_GRIDS = 0.5
|
||||||
|
|
||||||
|
# The tight spacing, used only for a wire the grid spacing above cannot place -- boxes
|
||||||
|
# laid out closer together than it needs. A vertical run sits WIRE_HUG from the box it
|
||||||
|
# leaves or enters and never nearer than WIRE_MIN_CLEAR; a horizontal one keeps
|
||||||
|
# LINK_CLEAR from a box above or below; the last turn is ARRIVE_TURN_GRIDS back. Side by
|
||||||
|
# side, vertical runs stand WIRE_LANE_GAP apart and horizontal ones WIRE_ROW_GAP.
|
||||||
WIRE_HUG = 10
|
WIRE_HUG = 10
|
||||||
WIRE_MIN_CLEAR = 6
|
WIRE_MIN_CLEAR = 6
|
||||||
|
LINK_CLEAR = 5
|
||||||
|
ARRIVE_TURN_GRIDS = 1.5
|
||||||
WIRE_LANE_GAP = 6
|
WIRE_LANE_GAP = 6
|
||||||
|
WIRE_ROW_GAP = 3
|
||||||
|
|
||||||
# No wire may pass under a component, and that outranks hugging: when the hug band is
|
# No wire may pass under a component, and that outranks hugging: when the hug band is
|
||||||
# already taken, a run may stand off this far instead so the wire can still be drawn
|
# already taken, a run may stand off this far instead so the wire can still be drawn
|
||||||
@@ -33,15 +45,45 @@ WIRE_LANE_GAP = 6
|
|||||||
# knobs, the same way a link leaving the sheet is.
|
# knobs, the same way a link leaving the sheet is.
|
||||||
WIRE_STANDOFF_MAX = 60
|
WIRE_STANDOFF_MAX = 60
|
||||||
|
|
||||||
# The last turn before a target slot is kept this many grid squares back from the box,
|
|
||||||
# so the arrowhead always sits on a straight run of its own instead of on the bend.
|
|
||||||
ARRIVE_TURN_GRIDS = 1.5
|
|
||||||
|
|
||||||
# Two different wires running horizontally at the same height are indistinguishable, so
|
@dataclass(frozen=True)
|
||||||
# a free horizontal lane is nudged until it clears any other wire's by this much. The
|
class Clearance:
|
||||||
# short stubs joining a slot cannot move -- they have to meet their pin -- so a fan-out
|
"""The spacing a route keeps from boxes, at 1:1.
|
||||||
# from one output pin still shares the run right beside the pin.
|
|
||||||
WIRE_ROW_GAP = 3
|
side nearest a vertical run may come to a box's left or right edge
|
||||||
|
hug where a vertical run beside the box it leaves or enters prefers to sit
|
||||||
|
end nearest a horizontal run may come to a box's top or bottom edge
|
||||||
|
arrive how far in front of the target box the last turn is made
|
||||||
|
lane nearest two different wires' vertical runs may stand side by side
|
||||||
|
row nearest two different wires' horizontal runs may stand side by side
|
||||||
|
"""
|
||||||
|
side: float
|
||||||
|
hug: float
|
||||||
|
end: float
|
||||||
|
arrive: float
|
||||||
|
lane: float
|
||||||
|
row: float
|
||||||
|
|
||||||
|
|
||||||
|
def grid_clearance():
|
||||||
|
"""The spacing every wire is routed with first, measured in grid squares."""
|
||||||
|
grid = app_state.GRID_SIZE
|
||||||
|
side = CLEAR_SIDE_GRIDS * grid
|
||||||
|
apart = CLEAR_WIRE_GRIDS * grid
|
||||||
|
return Clearance(side=side, hug=side, end=CLEAR_END_GRIDS * grid, arrive=side,
|
||||||
|
lane=apart, row=apart)
|
||||||
|
|
||||||
|
|
||||||
|
def tight_clearance():
|
||||||
|
"""The older, tighter spacing, kept for a wire that cannot be routed on the grid one.
|
||||||
|
|
||||||
|
Boxes laid out before the grid rule may stand closer than it needs, and a wire that
|
||||||
|
brushes a box is still far better than a link reduced to knobs.
|
||||||
|
"""
|
||||||
|
return Clearance(side=WIRE_MIN_CLEAR, hug=WIRE_HUG, end=LINK_CLEAR,
|
||||||
|
arrive=max(WIRE_HUG, ARRIVE_TURN_GRIDS * app_state.GRID_SIZE),
|
||||||
|
lane=WIRE_LANE_GAP, row=WIRE_ROW_GAP)
|
||||||
|
|
||||||
LINK_COLOR = "#2f6fba"
|
LINK_COLOR = "#2f6fba"
|
||||||
KNOB_OFFSHEET_COLOR = "#9aa0a6" # a link whose far end is on another wiresheet
|
KNOB_OFFSHEET_COLOR = "#9aa0a6" # a link whose far end is on another wiresheet
|
||||||
KNOB_RADIUS = 4
|
KNOB_RADIUS = 4
|
||||||
@@ -292,10 +334,21 @@ def segment_hits_boxes(x1, y1, x2, y2, boxes):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def path_is_clear(points, boxes):
|
def path_is_clear(points, boxes, clear):
|
||||||
for idx in range(len(points) - 1):
|
"""True if no part of the path runs under a box or crowds one.
|
||||||
(x1, y1), (x2, y2) = points[idx], points[idx + 1]
|
|
||||||
if segment_hits_boxes(x1, y1, x2, y2, boxes):
|
Every run between the first turn and the last keeps `clear` from each box: `end`
|
||||||
|
above and below, `side` left and right. The two runs pinned to the slots are only
|
||||||
|
held out from under the boxes, since they have to reach their pins wherever the
|
||||||
|
rows happen to be.
|
||||||
|
"""
|
||||||
|
points = wire_style.simplify(points)
|
||||||
|
padded = [(x1 - clear.side, y1 - clear.end, x2 + clear.side, y2 + clear.end)
|
||||||
|
for x1, y1, x2, y2 in boxes]
|
||||||
|
last = len(points) - 2
|
||||||
|
for idx, ((x1, y1), (x2, y2)) in enumerate(zip(points, points[1:])):
|
||||||
|
pinned = idx in (0, last)
|
||||||
|
if segment_hits_boxes(x1, y1, x2, y2, boxes if pinned else padded):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -319,22 +372,17 @@ def vertical_run_clearance(cx, y1, y2, boxes):
|
|||||||
return nearest
|
return nearest
|
||||||
|
|
||||||
|
|
||||||
def arrive_turn():
|
def channel_candidates(src_box, dst_box, boxes, low, high, clear):
|
||||||
"""How far in front of a target box the wire makes its last turn, at 1:1."""
|
|
||||||
return max(WIRE_HUG, ARRIVE_TURN_GRIDS * app_state.GRID_SIZE)
|
|
||||||
|
|
||||||
|
|
||||||
def channel_candidates(src_box, dst_box, boxes, low, high):
|
|
||||||
"""Vertical channel positions to try, best first.
|
"""Vertical channel positions to try, best first.
|
||||||
|
|
||||||
A run hugs the box it leaves or the box it enters, WIRE_HUG away -- that is the rule
|
A run hugs the box it leaves or the box it enters, `clear.hug` away -- that is the
|
||||||
that a vertical line stays within WIRE_HUG of a component. The midpoint of the gap
|
rule that a vertical line stays close to a component. The midpoint of the gap is
|
||||||
is kept only as a last resort, for when every hug position is blocked.
|
kept only as a last resort, for when every hug position is blocked.
|
||||||
"""
|
"""
|
||||||
ordered = [src_box[2] + WIRE_HUG, dst_box[0] - arrive_turn()]
|
ordered = [src_box[2] + clear.hug, dst_box[0] - clear.arrive]
|
||||||
for bx1, _, bx2, _ in boxes:
|
for bx1, _, bx2, _ in boxes:
|
||||||
ordered.append(bx2 + WIRE_HUG)
|
ordered.append(bx2 + clear.hug)
|
||||||
ordered.append(bx1 - WIRE_HUG)
|
ordered.append(bx1 - clear.hug)
|
||||||
ordered.append((low + high) / 2)
|
ordered.append((low + high) / 2)
|
||||||
|
|
||||||
seen, out = set(), []
|
seen, out = set(), []
|
||||||
@@ -345,96 +393,123 @@ def channel_candidates(src_box, dst_box, boxes, low, high):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def free_lane(cx, run_lo, run_hi, boxes, taken, low, high, max_steps=8):
|
def same_signal(pins, other_pins):
|
||||||
"""Nudges cx outwards in WIRE_LANE_GAP steps until the run has a lane of its own.
|
"""True if two wires may lie exactly on top of each other.
|
||||||
|
|
||||||
A lane is rejected while it crowds a box (closer than WIRE_MIN_CLEAR) or while it
|
A wire's `pins` are the slots at its two ends. Wires leaving one output carry the
|
||||||
|
same signal, and wires arriving at one input merge into it, so where two of them
|
||||||
|
share a pin they may share a run too -- that is a branch or a merge, not a clash.
|
||||||
|
"""
|
||||||
|
return bool(pins and other_pins and pins & other_pins)
|
||||||
|
|
||||||
|
|
||||||
|
def runs_clash(at, lo, hi, other_at, other_lo, other_hi, pins, other_pins, apart):
|
||||||
|
"""True if a run would sit too close beside, on top of, or end to end with another.
|
||||||
|
|
||||||
|
`at` is the run's fixed coordinate (x for a vertical run, y for a horizontal one)
|
||||||
|
and lo..hi its extent along the other axis. Two runs closer than `apart` side by
|
||||||
|
side clash where they overlap, and so do two on one line that stop less than
|
||||||
|
`apart` short of each other, which would read as one continuous wire. A wire never
|
||||||
|
clashes with itself: its own runs meet at its bends.
|
||||||
|
"""
|
||||||
|
if pins is not None and pins == other_pins:
|
||||||
|
return False
|
||||||
|
if hi < other_lo - apart + LINK_EPS or lo > other_hi + apart - LINK_EPS:
|
||||||
|
return False
|
||||||
|
gap = abs(at - other_at)
|
||||||
|
if gap >= apart - LINK_EPS:
|
||||||
|
return False
|
||||||
|
return not (gap < LINK_EPS and same_signal(pins, other_pins))
|
||||||
|
|
||||||
|
|
||||||
|
def lane_is_crowded(lane, run_lo, run_hi, claimed, pins, apart):
|
||||||
|
"""True if a vertical run at `lane` would crowd one already placed.
|
||||||
|
|
||||||
|
Wires sharing a pin may share a trunk exactly -- that is a branch, and gets a
|
||||||
|
junction dot. Running alongside it a few pixels off is refused for every wire alike.
|
||||||
|
"""
|
||||||
|
return any(runs_clash(lane, run_lo, run_hi, tx, t_lo, t_hi, pins, t_pins, apart)
|
||||||
|
for tx, t_lo, t_hi, t_pins in claimed)
|
||||||
|
|
||||||
|
|
||||||
|
def free_lane(cx, run_lo, run_hi, boxes, taken, low, high, clear, max_steps=8,
|
||||||
|
pins=None):
|
||||||
|
"""Nudges cx outwards in `clear.lane` steps until the run has a lane of its own.
|
||||||
|
|
||||||
|
A lane is rejected while it crowds a box (closer than `clear.side`) or while it
|
||||||
would run alongside an already placed run that overlaps it vertically -- two runs
|
would run alongside an already placed run that overlaps it vertically -- two runs
|
||||||
passing through the same gap have to stand WIRE_LANE_GAP apart. Returns None when
|
passing through the same gap have to stand `clear.lane` apart. Returns None when
|
||||||
no lane inside the gap works. `max_steps` of 1 permits no nudging at all, which is
|
no lane inside the gap works. `max_steps` of 1 permits no nudging at all, which is
|
||||||
how the caller asks for the exact hug position and nothing else.
|
how the caller asks for the exact hug position and nothing else.
|
||||||
"""
|
"""
|
||||||
for step in range(0, max_steps):
|
for step in range(0, max_steps):
|
||||||
for sign in ((0,) if step == 0 else (1, -1)):
|
for sign in ((0,) if step == 0 else (1, -1)):
|
||||||
lane = cx + sign * step * WIRE_LANE_GAP
|
lane = cx + sign * step * clear.lane
|
||||||
if not (low <= lane <= high):
|
if not (low <= lane <= high):
|
||||||
continue
|
continue
|
||||||
if vertical_run_clearance(lane, run_lo, run_hi, boxes) < WIRE_MIN_CLEAR - LINK_EPS:
|
if vertical_run_clearance(lane, run_lo, run_hi, boxes) < clear.side - LINK_EPS:
|
||||||
continue
|
continue
|
||||||
crowded = any(
|
if not lane_is_crowded(lane, run_lo, run_hi, taken, pins, clear.lane):
|
||||||
abs(lane - tx) < WIRE_LANE_GAP - LINK_EPS
|
|
||||||
and not (run_hi < t_lo - LINK_EPS or run_lo > t_hi + LINK_EPS)
|
|
||||||
for tx, t_lo, t_hi in taken
|
|
||||||
)
|
|
||||||
if not crowded:
|
|
||||||
return lane
|
return lane
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def hug_lane(edge_x, side, run_lo, run_hi, boxes, claimed, max_offset=WIRE_HUG,
|
def hug_lane(edge_x, side, run_lo, run_hi, boxes, claimed, clear, max_offset=0,
|
||||||
min_offset=WIRE_MIN_CLEAR):
|
min_offset=0, pins=None):
|
||||||
"""Lane for a run pinned to one box edge, starting WIRE_MIN_CLEAR out from it.
|
"""Lane for a run pinned to one box edge, starting `clear.side` out from it.
|
||||||
|
|
||||||
`side` is +1 for a run leaving a box's right edge, -1 for one entering its left
|
`side` is +1 for a run leaving a box's right edge, -1 for one entering its left
|
||||||
edge. Offsets inside the hug band are offered first, furthest out of the band
|
edge. Offsets inside the hug band are offered first, furthest out of the band
|
||||||
first so the run keeps clear of its own box; only when every one of those is taken
|
first so the run keeps clear of its own box; only when every one of those is taken
|
||||||
does it reach past WIRE_HUG, out to `max_offset`. Returns None when nothing up to
|
does it reach past `clear.hug`, out to `max_offset`. Returns None when nothing up to
|
||||||
that limit clears both the boxes and the runs already placed. `min_offset` raises
|
that limit clears both the boxes and the runs already placed. `min_offset` raises
|
||||||
the floor, and the hug position with it, for a run that must stand further off.
|
the floor, and the hug position with it, for a run that must stand further off.
|
||||||
"""
|
"""
|
||||||
floor = max(WIRE_MIN_CLEAR, int(math.ceil(min_offset)))
|
floor = int(math.ceil(max(clear.side, min_offset)))
|
||||||
hug = max(WIRE_HUG, floor)
|
hug = max(int(math.ceil(clear.hug)), floor)
|
||||||
offsets = list(range(hug, floor - 1, -1))
|
offsets = list(range(hug, floor - 1, -1))
|
||||||
if max_offset > hug:
|
if max_offset > hug:
|
||||||
offsets += list(range(hug + 1, int(max_offset) + 1))
|
offsets += list(range(hug + 1, int(max_offset) + 1))
|
||||||
|
|
||||||
for offset in offsets:
|
for offset in offsets:
|
||||||
lane = edge_x + side * offset
|
lane = edge_x + side * offset
|
||||||
if vertical_run_clearance(lane, run_lo, run_hi, boxes) < WIRE_MIN_CLEAR - LINK_EPS:
|
if vertical_run_clearance(lane, run_lo, run_hi, boxes) < clear.side - LINK_EPS:
|
||||||
continue
|
continue
|
||||||
if any(
|
if lane_is_crowded(lane, run_lo, run_hi, claimed, pins, clear.lane):
|
||||||
abs(lane - tx) < WIRE_LANE_GAP - LINK_EPS
|
|
||||||
and not (run_hi < t_lo - LINK_EPS or run_lo > t_hi + LINK_EPS)
|
|
||||||
for tx, t_lo, t_hi in claimed
|
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
return lane
|
return lane
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def row_is_stacked(y, x_lo, x_hi, rows, owner):
|
def row_is_stacked(y, x_lo, x_hi, rows, pins, apart):
|
||||||
"""True if a horizontal run here would hide under another wire's.
|
"""True if a horizontal run here would hide under, or blur into, another wire's.
|
||||||
|
|
||||||
Runs belonging to the same source pin are ignored: several wires leaving one output
|
A wire sharing a pin may lie exactly on top: several wires leaving one output are the
|
||||||
are the same signal, and sharing the run out of the pin is how a fan-out should look.
|
same signal, and sharing the run out of the pin is how a fan-out should look.
|
||||||
"""
|
"""
|
||||||
return any(
|
return any(runs_clash(y, x_lo, x_hi, ry, rx1, rx2, pins, row_pins, apart)
|
||||||
row_owner != owner
|
for ry, rx1, rx2, row_pins in rows)
|
||||||
and abs(y - ry) < WIRE_ROW_GAP - LINK_EPS
|
|
||||||
and not (x_hi < rx1 - LINK_EPS or x_lo > rx2 + LINK_EPS)
|
|
||||||
for ry, rx1, rx2, row_owner in rows
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def free_rows(cy, x_lo, x_hi, rows, owner, max_steps=8):
|
def free_rows(cy, x_lo, x_hi, rows, pins, apart, max_steps=8):
|
||||||
"""Heights near cy, closest first, where no other wire is already running.
|
"""Heights near cy, closest first, where no other wire is already running.
|
||||||
|
|
||||||
A row is skipped while another wire's horizontal run sits within WIRE_ROW_GAP of
|
A row is skipped while another wire's horizontal run sits within `apart` of it and
|
||||||
it and overlaps it along x -- that is the case where one line hides under another.
|
overlaps it along x -- that is the case where one line hides under another.
|
||||||
Every clear row is offered rather than only the nearest, because the nearest may
|
Every clear row is offered rather than only the nearest, because the nearest may
|
||||||
still turn out to cross a box once the whole path is assembled.
|
still turn out to cross a box once the whole path is assembled.
|
||||||
"""
|
"""
|
||||||
out = []
|
out = []
|
||||||
for step in range(0, max_steps):
|
for step in range(0, max_steps):
|
||||||
for sign in ((0,) if step == 0 else (1, -1)):
|
for sign in ((0,) if step == 0 else (1, -1)):
|
||||||
row = cy + sign * step * WIRE_ROW_GAP
|
row = cy + sign * step * apart
|
||||||
if not row_is_stacked(row, x_lo, x_hi, rows, owner):
|
if not row_is_stacked(row, x_lo, x_hi, rows, pins, apart):
|
||||||
out.append(row)
|
out.append(row)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def route_link_path(src_box, src_y, dst_box, dst_y, boxes, taken=None,
|
def route_link_path(src_box, src_y, dst_box, dst_y, boxes, taken=None,
|
||||||
direct_only=False, owner=None):
|
direct_only=False, pins=None, clear=None):
|
||||||
"""Routes an orthogonal wire from a source slot to a target slot around every box.
|
"""Routes an orthogonal wire from a source slot to a target slot around every box.
|
||||||
|
|
||||||
Returns None when no path can reach the target without passing under a component.
|
Returns None when no path can reach the target without passing under a component.
|
||||||
@@ -442,16 +517,19 @@ def route_link_path(src_box, src_y, dst_box, dst_y, boxes, taken=None,
|
|||||||
exactly as it shows one whose far end lives on another sheet.
|
exactly as it shows one whose far end lives on another sheet.
|
||||||
|
|
||||||
`taken` is the sheet's ledger of what other wires have already claimed:
|
`taken` is the sheet's ledger of what other wires have already claimed:
|
||||||
{"cols": [(x, y1, y2), ...], "rows": [(y, x1, x2), ...]}. Each new wire claims a
|
{"cols": [(x, y1, y2, pins), ...], "rows": [(y, x1, x2, pins), ...]}. Each new wire
|
||||||
vertical lane and a horizontal lane of its own against it; pass None to route in
|
claims a vertical lane and a horizontal lane of its own against it; pass None to
|
||||||
isolation. The search relaxes in tiers: a wire that can only be drawn with its run
|
route in isolation. `pins` are the slots at the wire's two ends (see same_signal). The search relaxes in tiers: a wire that can only be drawn with its run
|
||||||
standing off further than WIRE_HUG is still far better than no wire at all.
|
standing off further than it would like is still far better than no wire at all.
|
||||||
|
`clear` is the spacing kept from the boxes, grid_clearance() unless given.
|
||||||
"""
|
"""
|
||||||
|
clear = clear or grid_clearance()
|
||||||
start = (src_box[2], src_y)
|
start = (src_box[2], src_y)
|
||||||
end = (dst_box[0], dst_y)
|
end = (dst_box[0], dst_y)
|
||||||
leave = (start[0] + LINK_STUB, src_y)
|
# The first turn stands clear.side out from the source, the last clear.arrive in
|
||||||
# The last turn stands arrive_turn() in front of the target, never closer.
|
# front of the target, never closer.
|
||||||
arrive = (end[0] - max(LINK_STUB, arrive_turn()), dst_y)
|
leave = (start[0] + clear.side, src_y)
|
||||||
|
arrive = (end[0] - clear.arrive, dst_y)
|
||||||
|
|
||||||
low, high = min(leave[0], arrive[0]), max(leave[0], arrive[0])
|
low, high = min(leave[0], arrive[0]), max(leave[0], arrive[0])
|
||||||
run_lo, run_hi = min(src_y, dst_y), max(src_y, dst_y)
|
run_lo, run_hi = min(src_y, dst_y), max(src_y, dst_y)
|
||||||
@@ -461,12 +539,17 @@ def route_link_path(src_box, src_y, dst_box, dst_y, boxes, taken=None,
|
|||||||
|
|
||||||
def rows_of(points):
|
def rows_of(points):
|
||||||
"""The horizontal runs a finished path lays down, for the ledger."""
|
"""The horizontal runs a finished path lays down, for the ledger."""
|
||||||
|
points = wire_style.simplify(points)
|
||||||
return [
|
return [
|
||||||
(y1, min(x1, x2), max(x1, x2), owner)
|
(y1, min(x1, x2), max(x1, x2), pins)
|
||||||
for (x1, y1), (x2, y2) in zip(points, points[1:])
|
for (x1, y1), (x2, y2) in zip(points, points[1:])
|
||||||
if abs(y1 - y2) < LINK_EPS and abs(x1 - x2) > LINK_EPS
|
if abs(y1 - y2) < LINK_EPS and abs(x1 - x2) > LINK_EPS
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def stacked(candidate):
|
||||||
|
return any(row_is_stacked(y, x1, x2, claimed_rows, pins, clear.row)
|
||||||
|
for y, x1, x2, _ in rows_of(candidate))
|
||||||
|
|
||||||
def straight_through(max_steps, allow_stack=False):
|
def straight_through(max_steps, allow_stack=False):
|
||||||
"""A single vertical channel in the gap between the two boxes.
|
"""A single vertical channel in the gap between the two boxes.
|
||||||
|
|
||||||
@@ -474,33 +557,31 @@ def route_link_path(src_box, src_y, dst_box, dst_y, boxes, taken=None,
|
|||||||
they are refused when they would hide under another wire: the wire is better
|
they are refused when they would hide under another wire: the wire is better
|
||||||
off detouring, which puts it on a lane it can actually move.
|
off detouring, which puts it on a lane it can actually move.
|
||||||
"""
|
"""
|
||||||
candidates = channel_candidates(src_box, dst_box, boxes, low, high)
|
candidates = channel_candidates(src_box, dst_box, boxes, low, high, clear)
|
||||||
for cx in candidates:
|
for cx in candidates:
|
||||||
lane = free_lane(cx, run_lo, run_hi, boxes, claimed, low, high, max_steps)
|
lane = free_lane(cx, run_lo, run_hi, boxes, claimed, low, high, clear,
|
||||||
|
max_steps, pins)
|
||||||
if lane is None:
|
if lane is None:
|
||||||
continue
|
continue
|
||||||
candidate = [start, leave, (lane, src_y), (lane, dst_y), arrive, end]
|
candidate = [start, leave, (lane, src_y), (lane, dst_y), arrive, end]
|
||||||
if not path_is_clear(candidate, boxes):
|
if not path_is_clear(candidate, boxes, clear):
|
||||||
continue
|
continue
|
||||||
if not allow_stack and any(
|
if not allow_stack and stacked(candidate):
|
||||||
row_is_stacked(y, x1, x2, claimed_rows, owner)
|
|
||||||
for y, x1, x2, _ in rows_of(candidate)
|
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
return candidate, [(lane, run_lo, run_hi)]
|
return candidate, [(lane, run_lo, run_hi, pins)]
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
def around_boxes(max_offset, allow_stack=False):
|
def around_boxes(max_offset, allow_stack=False):
|
||||||
"""Over or under everything, hugging each end. Covers right-to-left feedback.
|
"""Over or under everything, hugging each end. Covers right-to-left feedback.
|
||||||
|
|
||||||
With `allow_stack` the long run may settle on a row another wire already uses,
|
With `allow_stack` a run may settle on a row another wire already uses, which is
|
||||||
which is a last resort: a wire sharing a row is still far better than a link
|
a last resort: a wire sharing a row is still far better than a link shown only
|
||||||
shown only as knobs.
|
as knobs.
|
||||||
"""
|
"""
|
||||||
lanes = set()
|
lanes = set()
|
||||||
for _, by1, _, by2 in boxes:
|
for _, by1, _, by2 in boxes:
|
||||||
lanes.add(by1 - LINK_CLEAR)
|
lanes.add(by1 - clear.end)
|
||||||
lanes.add(by2 + LINK_CLEAR)
|
lanes.add(by2 + clear.end)
|
||||||
|
|
||||||
# Also offer the clear air above and below the whole sheet, and the middle of
|
# Also offer the clear air above and below the whole sheet, and the middle of
|
||||||
# each horizontal band between one row of boxes and the next. A lane pinned to
|
# each horizontal band between one row of boxes and the next. A lane pinned to
|
||||||
@@ -508,23 +589,25 @@ def route_link_path(src_box, src_y, dst_box, dst_y, boxes, taken=None,
|
|||||||
# middle of a band usually crosses the sheet unobstructed.
|
# middle of a band usually crosses the sheet unobstructed.
|
||||||
tops = sorted({b[1] for b in boxes})
|
tops = sorted({b[1] for b in boxes})
|
||||||
bottoms = sorted({b[3] for b in boxes})
|
bottoms = sorted({b[3] for b in boxes})
|
||||||
lanes.add(min(tops) - LINK_CLEAR)
|
lanes.add(min(tops) - clear.end)
|
||||||
lanes.add(max(bottoms) + LINK_CLEAR)
|
lanes.add(max(bottoms) + clear.end)
|
||||||
for bottom in bottoms:
|
for bottom in bottoms:
|
||||||
nearest_below = [t for t in tops if t > bottom + 2 * LINK_CLEAR]
|
nearest_below = [t for t in tops if t > bottom + 2 * clear.end]
|
||||||
if nearest_below:
|
if nearest_below:
|
||||||
lanes.add((bottom + min(nearest_below)) / 2)
|
lanes.add((bottom + min(nearest_below)) / 2)
|
||||||
|
|
||||||
midline = (src_y + dst_y) / 2
|
midline = (src_y + dst_y) / 2
|
||||||
for base_cy in sorted(lanes, key=lambda c: abs(c - midline)):
|
for base_cy in sorted(lanes, key=lambda c: abs(c - midline)):
|
||||||
out_x = hug_lane(src_box[2], 1, min(src_y, base_cy), max(src_y, base_cy),
|
out_x = hug_lane(src_box[2], 1, min(src_y, base_cy), max(src_y, base_cy),
|
||||||
boxes, claimed, max_offset)
|
boxes, claimed, clear, max_offset, pins=pins)
|
||||||
if out_x is None:
|
if out_x is None:
|
||||||
continue
|
continue
|
||||||
|
# Checked against its own outward run too, but as another wire's: the two
|
||||||
|
# ends of one wire must never fold back onto each other.
|
||||||
in_x = hug_lane(dst_box[0], -1, min(base_cy, dst_y), max(base_cy, dst_y),
|
in_x = hug_lane(dst_box[0], -1, min(base_cy, dst_y), max(base_cy, dst_y),
|
||||||
boxes, claimed + [(out_x, min(src_y, base_cy),
|
boxes, claimed + [(out_x, min(src_y, base_cy),
|
||||||
max(src_y, base_cy))], max_offset,
|
max(src_y, base_cy), None)], clear,
|
||||||
min_offset=arrive_turn())
|
max_offset, min_offset=clear.arrive, pins=pins)
|
||||||
if in_x is None:
|
if in_x is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -532,7 +615,7 @@ def route_link_path(src_box, src_y, dst_box, dst_y, boxes, taken=None,
|
|||||||
# off any other wire already crossing at that height. Each clear height is
|
# off any other wire already crossing at that height. Each clear height is
|
||||||
# tried in turn, since a height clear of wires can still cross a box.
|
# tried in turn, since a height clear of wires can still cross a box.
|
||||||
span_lo, span_hi = min(out_x, in_x), max(out_x, in_x)
|
span_lo, span_hi = min(out_x, in_x), max(out_x, in_x)
|
||||||
options = free_rows(base_cy, span_lo, span_hi, claimed_rows, owner)
|
options = free_rows(base_cy, span_lo, span_hi, claimed_rows, pins, clear.row)
|
||||||
if allow_stack and base_cy not in options:
|
if allow_stack and base_cy not in options:
|
||||||
options.append(base_cy) # last resort: share the row rather than drop the wire
|
options.append(base_cy) # last resort: share the row rather than drop the wire
|
||||||
|
|
||||||
@@ -544,16 +627,22 @@ def route_link_path(src_box, src_y, dst_box, dst_y, boxes, taken=None,
|
|||||||
# row lengthens both vertical runs, so their clearance is checked
|
# row lengthens both vertical runs, so their clearance is checked
|
||||||
# again over the spans they actually cover.
|
# again over the spans they actually cover.
|
||||||
if (vertical_run_clearance(out_x, out_lo, out_hi, boxes)
|
if (vertical_run_clearance(out_x, out_lo, out_hi, boxes)
|
||||||
< WIRE_MIN_CLEAR - LINK_EPS):
|
< clear.side - LINK_EPS):
|
||||||
continue
|
continue
|
||||||
if (vertical_run_clearance(in_x, in_lo, in_hi, boxes)
|
if (vertical_run_clearance(in_x, in_lo, in_hi, boxes)
|
||||||
< WIRE_MIN_CLEAR - LINK_EPS):
|
< clear.side - LINK_EPS):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
candidate = [start, (out_x, src_y), (out_x, cy),
|
candidate = [start, (out_x, src_y), (out_x, cy),
|
||||||
(in_x, cy), (in_x, dst_y), end]
|
(in_x, cy), (in_x, dst_y), end]
|
||||||
if path_is_clear(candidate, boxes):
|
if not path_is_clear(candidate, boxes, clear):
|
||||||
return candidate, [(out_x, out_lo, out_hi), (in_x, in_lo, in_hi)]
|
continue
|
||||||
|
# The runs out of and into the slots are pinned to their rows, so they
|
||||||
|
# are checked here: the long run was already kept clear by free_rows.
|
||||||
|
if not allow_stack and stacked(candidate):
|
||||||
|
continue
|
||||||
|
return candidate, [(out_x, out_lo, out_hi, pins),
|
||||||
|
(in_x, in_lo, in_hi, pins)]
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
# Tier by how much of the spacing guidance has to give way. Crossing a box is never
|
# Tier by how much of the spacing guidance has to give way. Crossing a box is never
|
||||||
@@ -562,26 +651,22 @@ def route_link_path(src_box, src_y, dst_box, dst_y, boxes, taken=None,
|
|||||||
# First pass: only the straight route, whose horizontal runs are pinned to their
|
# First pass: only the straight route, whose horizontal runs are pinned to their
|
||||||
# slots and cannot be moved later. Letting these claim their rows first stops a
|
# slots and cannot be moved later. Letting these claim their rows first stops a
|
||||||
# detouring wire from parking its movable lane on top of one of them.
|
# detouring wire from parking its movable lane on top of one of them.
|
||||||
attempt_names = ("straight(1)",)
|
|
||||||
attempts = (lambda: straight_through(1),)
|
attempts = (lambda: straight_through(1),)
|
||||||
else:
|
else:
|
||||||
attempt_names = (
|
standoff = max(WIRE_STANDOFF_MAX, 3 * clear.side)
|
||||||
"straight(1)", "around(hug)", "straight(8)", "around(standoff)",
|
|
||||||
"straight(1,stack)", "straight(8,stack)", "around(standoff,stack)",
|
|
||||||
)
|
|
||||||
attempts = (
|
attempts = (
|
||||||
lambda: straight_through(1), # hugging, own lane
|
lambda: straight_through(1), # hugging, own lane
|
||||||
lambda: around_boxes(WIRE_HUG), # hugging, routed around
|
lambda: around_boxes(clear.hug), # hugging, routed around
|
||||||
lambda: straight_through(8), # lane nudged out of the band
|
lambda: straight_through(8), # lane nudged out of the band
|
||||||
lambda: around_boxes(WIRE_STANDOFF_MAX), # standing off, routed around
|
lambda: around_boxes(standoff), # standing off, routed around
|
||||||
# Last resorts, in order. A wire sharing a row with another is still far
|
# Last resorts, in order. A wire sharing a row with another is still far
|
||||||
# better than a link reduced to knobs, so stacking is only reached once
|
# better than a link reduced to knobs, so stacking is only reached once
|
||||||
# every route that keeps its own row has been ruled out.
|
# every route that keeps its own row has been ruled out.
|
||||||
lambda: straight_through(1, allow_stack=True),
|
lambda: straight_through(1, allow_stack=True),
|
||||||
lambda: straight_through(8, allow_stack=True),
|
lambda: straight_through(8, allow_stack=True),
|
||||||
lambda: around_boxes(WIRE_STANDOFF_MAX, allow_stack=True),
|
lambda: around_boxes(standoff, allow_stack=True),
|
||||||
)
|
)
|
||||||
for name, attempt in zip(attempt_names, attempts):
|
for attempt in attempts:
|
||||||
candidate, runs = attempt()
|
candidate, runs = attempt()
|
||||||
if candidate is not None:
|
if candidate is not None:
|
||||||
if taken is not None:
|
if taken is not None:
|
||||||
@@ -646,50 +731,47 @@ LINK_SELECT_WIDTH = 2
|
|||||||
LINK_HINT_DASH = (5, 3) # dashes of the line shown between a selected pair of knobs
|
LINK_HINT_DASH = (5, 3) # dashes of the line shown between a selected pair of knobs
|
||||||
|
|
||||||
|
|
||||||
def draw_links():
|
@dataclass
|
||||||
"""Paints wires between slots on this sheet and knobs for links leaving it."""
|
class WireJob:
|
||||||
canvas = app_state.canvas
|
"""One link with both ends on this sheet, on its way to becoming a wire."""
|
||||||
canvas.delete("link")
|
tag: str
|
||||||
app_state.canvas_link_map.clear()
|
src_box: tuple
|
||||||
app_state.knob_link_ends.clear()
|
src_y: float
|
||||||
app_state.selected_link_tag = None
|
dst_box: tuple
|
||||||
if app_state.xml_root_element is None:
|
dst_y: float
|
||||||
return
|
owner: str # the source pin; its wires branch with a junction dot
|
||||||
|
target: str # the target pin
|
||||||
|
points: list = None # the routed path, or None while unrouted / unroutable
|
||||||
|
|
||||||
links_elem = app_state.xml_root_element.find("links")
|
@property
|
||||||
if links_elem is None:
|
def pins(self):
|
||||||
return
|
"""Both end slots, told apart by side since one slot can be either end."""
|
||||||
|
return frozenset((("out", self.owner), ("in", self.target)))
|
||||||
|
|
||||||
path_to_tag = {}
|
def stub_rows(self, clear):
|
||||||
for comp_element, comp_tag in app_state.canvas_tag_by_element.items():
|
"""The two runs pinned to the slots, before the first turn and after the last.
|
||||||
path_to_tag[get_component_path(comp_element, app_state.xml_root_element)] = comp_tag
|
|
||||||
|
|
||||||
if not path_to_tag:
|
Whatever route the wire takes, these are where it has to be, so they are
|
||||||
return
|
claimed for every wire before any is routed. Otherwise an early wire can lay
|
||||||
|
its long run along a row a later wire's slot is obliged to leave by.
|
||||||
|
"""
|
||||||
|
return [(self.src_y, self.src_box[2], self.src_box[2] + clear.side, self.pins),
|
||||||
|
(self.dst_y, self.dst_box[0] - clear.arrive, self.dst_box[0], self.pins)]
|
||||||
|
|
||||||
boxes = [b for b in (get_box_bounds(t) for t in path_to_tag.values()) if b]
|
def route(self, boxes, ledger, clear, direct_only=False):
|
||||||
|
self.points = route_link_path(self.src_box, self.src_y, self.dst_box, self.dst_y,
|
||||||
|
boxes, ledger, direct_only=direct_only,
|
||||||
|
pins=self.pins, clear=clear)
|
||||||
|
|
||||||
# What the wires drawn so far have claimed, shared across the whole sheet: vertical
|
def knob_ends(self):
|
||||||
# runs as (x, y1, y2) so two through one gap end up WIRE_LANE_GAP apart, and
|
"""Where the two knobs of an unroutable link sit: (x1, y1, x2, y2)."""
|
||||||
# horizontal runs as (y, x1, x2) so no two cross the sheet at the same height.
|
return (self.src_box[2] + KNOB_RADIUS, self.src_y,
|
||||||
claimed_runs = {"cols": [], "rows": []}
|
self.dst_box[0] - KNOB_RADIUS, self.dst_y)
|
||||||
jobs = [] # [wire_tag, src_box, src_y, dst_box, dst_y, points, owner]
|
|
||||||
unroutable = 0
|
|
||||||
link_index = 0
|
|
||||||
|
|
||||||
# Knobs are gathered per slot rather than drawn as they come up, because a slot
|
|
||||||
# carrying both an off-sheet link and an unreachable one on this sheet is drawn as
|
|
||||||
# a single half-and-half marker.
|
|
||||||
knobs = {} # (x, y) -> {"on_sheet": bool, "off_sheet": bool, "tags": set}
|
|
||||||
|
|
||||||
def want_knob(x, y, kind, wire_tag=None):
|
def collect_link_jobs(links_elem, path_to_tag, want_knob):
|
||||||
entry = knobs.setdefault(
|
"""Turns the sheet's <link>s into wire jobs, and off-sheet ones into knob requests."""
|
||||||
(round(x, 1), round(y, 1)),
|
jobs = []
|
||||||
{"on_sheet": False, "off_sheet": False, "tags": set()},
|
|
||||||
)
|
|
||||||
entry[kind] = True
|
|
||||||
if wire_tag:
|
|
||||||
entry["tags"].add(wire_tag)
|
|
||||||
for link in links_elem.findall("link"):
|
for link in links_elem.findall("link"):
|
||||||
from_ref = link.get("from", "")
|
from_ref = link.get("from", "")
|
||||||
to_ref = link.get("to", "")
|
to_ref = link.get("to", "")
|
||||||
@@ -698,91 +780,144 @@ def draw_links():
|
|||||||
|
|
||||||
from_path, from_slot = from_ref.rsplit(".", 1)
|
from_path, from_slot = from_ref.rsplit(".", 1)
|
||||||
to_path, to_slot = to_ref.rsplit(".", 1)
|
to_path, to_slot = to_ref.rsplit(".", 1)
|
||||||
|
|
||||||
src_tag = path_to_tag.get(from_path)
|
src_tag = path_to_tag.get(from_path)
|
||||||
dst_tag = path_to_tag.get(to_path)
|
dst_tag = path_to_tag.get(to_path)
|
||||||
|
if not (src_tag or dst_tag):
|
||||||
|
continue
|
||||||
|
|
||||||
|
wire_tag = f"link_wire_{len(app_state.canvas_link_map)}"
|
||||||
if src_tag and dst_tag:
|
if src_tag and dst_tag:
|
||||||
src_box, src_y = resolve_slot_anchor(src_tag, from_slot)
|
src_box, src_y = resolve_slot_anchor(src_tag, from_slot)
|
||||||
dst_box, dst_y = resolve_slot_anchor(dst_tag, to_slot)
|
dst_box, dst_y = resolve_slot_anchor(dst_tag, to_slot)
|
||||||
if src_box is None or dst_box is None:
|
if src_box is None or dst_box is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
wire_tag = f"link_wire_{link_index}"
|
|
||||||
link_index += 1
|
|
||||||
app_state.canvas_link_map[wire_tag] = link
|
app_state.canvas_link_map[wire_tag] = link
|
||||||
jobs.append([wire_tag, src_box, src_y, dst_box, dst_y, None, from_ref])
|
jobs.append(WireJob(wire_tag, src_box, src_y, dst_box, dst_y, from_ref, to_ref))
|
||||||
elif src_tag or dst_tag:
|
|
||||||
# One end is on another sheet. The knob still stands for a real link, so
|
|
||||||
# it is tagged and registered like a wire and can be picked and deleted.
|
|
||||||
if src_tag:
|
|
||||||
box, slot_y = resolve_slot_anchor(src_tag, from_slot)
|
|
||||||
knob_x = box[2] + KNOB_RADIUS if box is not None else None
|
|
||||||
else:
|
|
||||||
box, slot_y = resolve_slot_anchor(dst_tag, to_slot)
|
|
||||||
knob_x = box[0] - KNOB_RADIUS if box is not None else None
|
|
||||||
if box is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
wire_tag = f"link_wire_{link_index}"
|
|
||||||
link_index += 1
|
|
||||||
app_state.canvas_link_map[wire_tag] = link
|
|
||||||
want_knob(knob_x, slot_y, "off_sheet", wire_tag)
|
|
||||||
|
|
||||||
# Sort by horizontal gap distance so shorter, simpler links claim lanes first.
|
|
||||||
# Ties are broken by vertical span, keeping the cheapest wires at the front.
|
|
||||||
jobs.sort(key=lambda j: (abs(j[3][0] - j[1][2]), abs(j[4] - j[2])))
|
|
||||||
|
|
||||||
# Route in two passes. A wire that can run straight has its horizontal segments
|
|
||||||
# pinned to its two slots, so it gets first claim on those rows; wires that have to
|
|
||||||
# detour then pick lanes that keep clear of them. Routing in one pass lets a detour
|
|
||||||
# settle on a row a later straight wire is obliged to use.
|
|
||||||
for job in jobs:
|
|
||||||
wire_tag, src_box, src_y, dst_box, dst_y, _, owner = job
|
|
||||||
job[5] = route_link_path(src_box, src_y, dst_box, dst_y, boxes,
|
|
||||||
claimed_runs, direct_only=True, owner=owner)
|
|
||||||
for job in jobs:
|
|
||||||
if job[5] is None:
|
|
||||||
wire_tag, src_box, src_y, dst_box, dst_y, _, owner = job
|
|
||||||
job[5] = route_link_path(src_box, src_y, dst_box, dst_y, boxes,
|
|
||||||
claimed_runs, owner=owner)
|
|
||||||
|
|
||||||
for wire_tag, src_box, src_y, dst_box, dst_y, points, _ in jobs:
|
|
||||||
if points is None:
|
|
||||||
# No route that stays out from under the boxes. Rather than hide a wire
|
|
||||||
# beneath a component, show the link as knobs on both slots -- the same
|
|
||||||
# marker a link to another sheet gets. Tagged so it stays selectable.
|
|
||||||
want_knob(src_box[2] + KNOB_RADIUS, src_y, "on_sheet", wire_tag)
|
|
||||||
want_knob(dst_box[0] - KNOB_RADIUS, dst_y, "on_sheet", wire_tag)
|
|
||||||
# Both ends are on this sheet, so selecting the link can show where it
|
|
||||||
# actually goes with a dashed line between the two knobs.
|
|
||||||
app_state.knob_link_ends[wire_tag] = (
|
|
||||||
src_box[2] + KNOB_RADIUS, src_y, dst_box[0] - KNOB_RADIUS, dst_y
|
|
||||||
)
|
|
||||||
unroutable += 1
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
flat = [coord for point in points for coord in point]
|
# One end is on another sheet. The knob still stands for a real link, so it is
|
||||||
|
# tagged and registered like a wire and can be picked and deleted.
|
||||||
|
if src_tag:
|
||||||
|
box, slot_y = resolve_slot_anchor(src_tag, from_slot)
|
||||||
|
knob_x = box[2] + KNOB_RADIUS if box is not None else None
|
||||||
|
else:
|
||||||
|
box, slot_y = resolve_slot_anchor(dst_tag, to_slot)
|
||||||
|
knob_x = box[0] - KNOB_RADIUS if box is not None else None
|
||||||
|
if box is None:
|
||||||
|
continue
|
||||||
|
app_state.canvas_link_map[wire_tag] = link
|
||||||
|
want_knob(knob_x, slot_y, "off_sheet", wire_tag)
|
||||||
|
return jobs
|
||||||
|
|
||||||
|
|
||||||
|
def paint_wires(canvas, jobs):
|
||||||
|
"""Draws routed wires: rounded bends, hops over crossings, dots where a fan-out splits.
|
||||||
|
|
||||||
|
Junction dots go down first so the wires sit on top of them; a click near a branch
|
||||||
|
then still lands on a wire rather than on an untagged dot.
|
||||||
|
"""
|
||||||
|
for job in jobs:
|
||||||
|
job.points = wire_style.simplify(job.points)
|
||||||
|
|
||||||
|
by_owner = {}
|
||||||
|
for job in jobs:
|
||||||
|
by_owner.setdefault(job.owner, []).append(job.points)
|
||||||
|
r = wire_style.JUNCTION_RADIUS
|
||||||
|
for x, y in wire_style.fan_out_junctions(by_owner):
|
||||||
|
canvas.create_oval(x - r, y - r, x + r, y + r, fill=LINK_COLOR, outline="",
|
||||||
|
tags=("link", "link_junction"))
|
||||||
|
|
||||||
|
hops = wire_style.find_hops([job.points for job in jobs])
|
||||||
|
for job, job_hops in zip(jobs, hops):
|
||||||
|
flat = [c for point in wire_style.styled_points(job.points, job_hops) for c in point]
|
||||||
canvas.create_line(
|
canvas.create_line(
|
||||||
*flat, fill=LINK_COLOR, width=1,
|
*flat, fill=LINK_COLOR, width=1, joinstyle=tk.ROUND,
|
||||||
arrow=tk.LAST, arrowshape=(8, 9, 3), tags=("link", wire_tag)
|
arrow=tk.LAST, arrowshape=(8, 9, 3), tags=("link", job.tag)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_links():
|
||||||
|
"""Paints wires between slots on this sheet and knobs for links leaving it."""
|
||||||
|
canvas = app_state.canvas
|
||||||
|
canvas.delete("link")
|
||||||
|
app_state.canvas_link_map.clear()
|
||||||
|
app_state.knob_link_ends.clear()
|
||||||
|
app_state.selected_link_tag = None
|
||||||
|
app_state.unroutable_link_count = 0
|
||||||
|
if app_state.xml_root_element is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
links_elem = app_state.xml_root_element.find("links")
|
||||||
|
if links_elem is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
path_to_tag = {
|
||||||
|
get_component_path(comp_element, app_state.xml_root_element): comp_tag
|
||||||
|
for comp_element, comp_tag in app_state.canvas_tag_by_element.items()
|
||||||
|
}
|
||||||
|
if not path_to_tag:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Knobs are gathered per slot rather than drawn as they come up, because a slot
|
||||||
|
# carrying both an off-sheet link and an unreachable one on this sheet is drawn as
|
||||||
|
# a single half-and-half marker.
|
||||||
|
knobs = {} # (x, y) -> {"on_sheet": bool, "off_sheet": bool, "tags": set}
|
||||||
|
|
||||||
|
def want_knob(x, y, kind, wire_tag):
|
||||||
|
entry = knobs.setdefault(
|
||||||
|
(round(x, 1), round(y, 1)),
|
||||||
|
{"on_sheet": False, "off_sheet": False, "tags": set()},
|
||||||
|
)
|
||||||
|
entry[kind] = True
|
||||||
|
entry["tags"].add(wire_tag)
|
||||||
|
|
||||||
|
jobs = collect_link_jobs(links_elem, path_to_tag, want_knob)
|
||||||
|
boxes = [b for b in (get_box_bounds(t) for t in path_to_tag.values()) if b]
|
||||||
|
|
||||||
|
# Shorter, simpler links claim lanes first; ties go to the smaller vertical span.
|
||||||
|
jobs.sort(key=lambda j: (abs(j.dst_box[0] - j.src_box[2]), abs(j.dst_y - j.src_y)))
|
||||||
|
|
||||||
|
# What the wires placed so far have claimed, shared across the whole sheet.
|
||||||
|
# Routed in two passes: a wire that can run straight has its horizontal runs pinned
|
||||||
|
# to its two slots, so it gets first claim on those rows; wires that have to detour
|
||||||
|
# then pick lanes that keep clear of them.
|
||||||
|
# Every wire is offered the full grid spacing before any wire settles for less: boxes
|
||||||
|
# placed closer than the grid rule allows still get a wire, just a tighter one.
|
||||||
|
roomy = grid_clearance()
|
||||||
|
ledger = {"cols": [], "rows": [row for job in jobs for row in job.stub_rows(roomy)]}
|
||||||
|
for job in jobs:
|
||||||
|
job.route(boxes, ledger, roomy, direct_only=True)
|
||||||
|
for clear in (roomy, tight_clearance()):
|
||||||
|
for job in jobs:
|
||||||
|
if job.points is None:
|
||||||
|
job.route(boxes, ledger, clear)
|
||||||
|
|
||||||
|
routed = [job for job in jobs if job.points is not None]
|
||||||
|
paint_wires(canvas, routed)
|
||||||
|
|
||||||
|
# No route stays out from under the boxes: rather than hide a wire beneath a
|
||||||
|
# component, show the link as knobs on both slots -- the marker a link to another
|
||||||
|
# sheet gets -- and remember the ends so selecting it can draw a dashed hint.
|
||||||
|
for job in jobs:
|
||||||
|
if job.points is None:
|
||||||
|
x1, y1, x2, y2 = job.knob_ends()
|
||||||
|
want_knob(x1, y1, "on_sheet", job.tag)
|
||||||
|
want_knob(x2, y2, "on_sheet", job.tag)
|
||||||
|
app_state.knob_link_ends[job.tag] = job.knob_ends()
|
||||||
|
|
||||||
for (x, y), entry in knobs.items():
|
for (x, y), entry in knobs.items():
|
||||||
draw_knob(canvas, x, y, entry["on_sheet"], entry["off_sheet"],
|
draw_knob(canvas, x, y, entry["on_sheet"], entry["off_sheet"],
|
||||||
tuple(entry["tags"]))
|
tuple(entry["tags"]))
|
||||||
|
|
||||||
# Wires sit behind the boxes so they never paint over a neighbouring component.
|
# Wires sit behind the boxes so they never paint over a neighbouring component.
|
||||||
# Nothing should actually be back there now, since a wire that cannot avoid a box is
|
# Knobs are raised back above: they sit just outside their own box edge, and while
|
||||||
# drawn as knobs instead. Knobs themselves are raised back above: they sit just
|
# underneath it a click on one lands on the box instead of selecting the link.
|
||||||
# outside their own box edge, and while they are underneath it a click on one lands
|
|
||||||
# on the box instead of selecting the link.
|
|
||||||
if canvas.find_withtag("component"):
|
if canvas.find_withtag("component"):
|
||||||
canvas.tag_lower("link", "component")
|
canvas.tag_lower("link", "component")
|
||||||
if canvas.find_withtag("link_knob"):
|
if canvas.find_withtag("link_knob"):
|
||||||
canvas.tag_raise("link_knob", "component")
|
canvas.tag_raise("link_knob", "component")
|
||||||
|
|
||||||
app_state.unroutable_link_count = unroutable
|
app_state.unroutable_link_count = len(jobs) - len(routed)
|
||||||
|
|
||||||
|
|
||||||
_selected_link_restore = [] # [(item, fill, outline, width)] captured when a link is selected
|
_selected_link_restore = [] # [(item, fill, outline, width)] captured when a link is selected
|
||||||
@@ -1221,12 +1356,13 @@ def wire_demand_by_boundary(column_of_comp):
|
|||||||
def column_gap_for(run_count):
|
def column_gap_for(run_count):
|
||||||
"""Clear space a column gap needs to carry `run_count` vertical wire runs.
|
"""Clear space a column gap needs to carry `run_count` vertical wire runs.
|
||||||
|
|
||||||
Lanes start WIRE_MIN_CLEAR out from the source box and arrive_turn() in front of
|
Lanes start clear.side out from the source box and clear.arrive in front of the
|
||||||
the target box, and stand WIRE_LANE_GAP apart, so n of them span
|
target box, and stand clear.lane apart, so n of them span
|
||||||
WIRE_MIN_CLEAR + arrive_turn() + (n - 1) * WIRE_LANE_GAP.
|
clear.side + clear.arrive + (n - 1) * clear.lane.
|
||||||
"""
|
"""
|
||||||
return max(TIDY_GAP, WIRE_MIN_CLEAR + arrive_turn()
|
clear = grid_clearance()
|
||||||
+ max(0, run_count - 1) * WIRE_LANE_GAP)
|
return max(TIDY_GAP, clear.side + clear.arrive
|
||||||
|
+ max(0, run_count - 1) * clear.lane)
|
||||||
|
|
||||||
|
|
||||||
def plan_columns(columns, column_keys, demand, scale):
|
def plan_columns(columns, column_keys, demand, scale):
|
||||||
|
|||||||
Reference in New Issue
Block a user