Link View: order the peers so the wires stop crossing
The picture was right and unreadable: eleven links round one component
drew twelve wire crossings, because each column was stacked in the order
the file happens to list its links and the lanes were handed out by how
far a wire had to travel.
Both columns are now stacked in the order of the chosen component's own
slots -- what feeds its first slot at the top, what feeds its fifth below
that. Nothing here is read from meta, so the view was always free to put
the peers where they read best, and this is the order that turns the set
of wires on one side into a ladder: both ends run the same way down the
page, so no wire has any reason to cross another.
That leaves which lane each wire turns in. For a ladder there is an order
that cannot produce a crossing, and read off the slots rather than off the
direction the line is drawn in, it is the same on both sides -- two fans,
each nesting outwards from the component's own row:
reaching a peer slot above its focus slot -> turns nearer the focus
the higher up the focus it lands
reaching a peer slot below its focus slot -> turns nearer the focus
the lower down the focus it lands
Two wires of one fan, the outer going round the inner, pass above and
below its vertical but never through it; and a wire of one fan can never
share a stretch of the page with a wire of the other, so the two fans
reuse each other's lanes and the gap stays as narrow as the picture
allows. Where several wires leave the same slot no order of focus heights
separates them, so the longest branch turns first and the rest carry on
along the shared run beside the pin -- a wiresheet's own fan-out. Where
two slots already line up the wire is one straight line and is settled
before anything has to be fitted past it; the layout is rounded to whole
pixels so that a pair that ought to line up does.
The reasoning is not trusted on its own: every wire is checked against the
ones already routed (routes_meet) and takes the next lane out if it
clashes. One thing genuinely breaks the ladder -- a peer whose own slots
run counter to the focus's, linking its row 4 to row 3 and its row 3 to
row 4. Ends inverted like that have to cross wherever they are drawn,
since neither box's slot order is this view's to change.
Measured over 461 linked components in four real files, 1113 wires: no
wire passes through a box at all, and ten crossings remain in six
components, every one of them an inverted pair of that kind. Before this
the same four files drew 45.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -379,6 +379,11 @@ derived from the links every time it is drawn and no part of it is stored in the
|
||||
(`meta` records a component's place on its own sheet, which says nothing about where it
|
||||
belongs relative to another one).
|
||||
|
||||
Each column is stacked **in the order of the chosen component's own slots**: whatever
|
||||
feeds its first slot sits at the top, whatever feeds its fifth sits below that. Nothing
|
||||
is read from `meta`, so the view is free to put the peers wherever they read best, and
|
||||
this is the order that makes the wires readable -- see **Keeping the wires apart** below.
|
||||
|
||||
Every line is **grey**, because this is not a wiresheet and a wire-blue line would invite
|
||||
the reader to treat it as one:
|
||||
|
||||
@@ -394,14 +399,58 @@ be on a sheet; a reference the document cannot resolve gets a dashed placeholder
|
||||
the path, because a link the view silently dropped would be the one thing a reader could
|
||||
not find out from here.
|
||||
|
||||
Each gap is sized by `column_gap` to hold a lane for every wire crossing it, and lanes are
|
||||
handed out nearest-target-first, so a wire reaching a distant box turns outside one
|
||||
reaching a near box instead of across it. A dozen links widen the gap rather than
|
||||
Hovering a wire darkens it and names the link in the footer; clicking a wire or a
|
||||
component follows it. `collect_links`, `sheet_display_name` and the whole of the routing
|
||||
geometry -- `segments_meet`, `routes_meet`, `column_gap`, `_slot_order_key` -- are pure
|
||||
functions, so both what the view reads out of a document and how it decides where a wire
|
||||
goes can be exercised without a window.
|
||||
|
||||
### Keeping the wires apart
|
||||
|
||||
Ordering each column by the chosen component's slots turns the set of wires on one side
|
||||
into a **ladder**: both ends run the same way down the page, so no wire has any reason to
|
||||
cross another. What is left is choosing which lane each one turns in, and `column_gap`
|
||||
reserves the room for that -- a lane per wire, so a dozen links widen the gap rather than
|
||||
crowding a dozen verticals into a 30px band.
|
||||
|
||||
Hovering a wire darkens it and names the link in the footer; clicking a wire or a
|
||||
component follows it. `collect_links` and `sheet_display_name` are pure DOM work, so what
|
||||
the view reads out of a document can be exercised without a window.
|
||||
For a ladder there is a lane order that cannot produce a crossing, and read off the slots
|
||||
rather than off the direction the line happens to be drawn in, it is the same on both
|
||||
sides -- two fans, each nesting outwards from the chosen component's own row:
|
||||
|
||||
| Wire | Turns nearer the focus |
|
||||
|---|---|
|
||||
| reaching a peer slot **above** its focus slot | the higher up the focus it lands |
|
||||
| reaching a peer slot **below** its focus slot | the lower down the focus it lands |
|
||||
|
||||
Take two wires of the upper fan, the higher one nested inside the lower: the lower one's
|
||||
run into the focus passes below where the higher one's vertical ends, and its run out to
|
||||
its peer passes above where that vertical begins, so it goes round it and never through
|
||||
it. The lower fan is the same argument upside down. A wire of one fan can never share a
|
||||
stretch of the page with a wire of the other -- ordering the ladder is what rules that
|
||||
out -- so the two fans reuse each other's lanes and the gap stays as narrow as the
|
||||
picture allows.
|
||||
|
||||
Two special cases sit on top of that rule. Where several wires leave the **same** slot no
|
||||
order of focus heights can separate them, so the longest branch turns first, against the
|
||||
focus, and the shorter ones carry on along the shared run beside the pin and drop away
|
||||
outside it -- which is how a wiresheet draws a fan-out too. And where two slots already
|
||||
line up, the wire is one straight line with no vertical run to place at all; those are
|
||||
settled first, because such a wire crosses the whole gap and everything else has to be
|
||||
fitted past it. The layout is rounded to whole pixels so that a pair that ought to line
|
||||
up does, rather than being drawn with a 1px jog in it.
|
||||
|
||||
Every wire is still checked against the ones already routed before it takes a lane
|
||||
(`routes_meet`), and takes the next lane out if it clashes. The argument above rests on
|
||||
the ladder being ordered, and one thing breaks that: a peer whose **own** slots run
|
||||
counter to the focus's -- linking its row 4 to the focus's row 3 and its row 3 to the
|
||||
focus's row 4. Two links with their ends inverted like that have to cross wherever they
|
||||
are drawn, and no reordering can help, because the order inside each box is the
|
||||
component's own. The check is what stops that one forced crossing from being made worse
|
||||
by the ones around it.
|
||||
|
||||
Measured over 461 linked components in four real files -- 1113 wires -- this leaves **no
|
||||
wire passing through a box at all**, and ten crossings in six components, every one of
|
||||
them an inverted pair of the kind above.
|
||||
|
||||
### Following a link to its other sheet
|
||||
|
||||
@@ -568,3 +617,8 @@ And two things worth knowing about the manifests themselves rather than the code
|
||||
display-only.
|
||||
- The Link View stacks each side in one column, so a component with a dozen peers makes a
|
||||
picture taller than the pane. It scrolls, but it does not wrap into a second column.
|
||||
- Two links between the same pair of components whose slot rows are inverted have to
|
||||
cross, since neither box's slot order is the view's to change. Six components in the
|
||||
four files measured do this.
|
||||
- `column_gap` budgets one lane per wire, but the router reuses lanes wherever it can, so
|
||||
a side that needs few of them is drawn with a gap wider than its wires require.
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
||||
import configparser
|
||||
import os
|
||||
|
||||
VERSION = "0.0.0.055"
|
||||
VERSION = "0.0.0.056"
|
||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||
DEFAULT_GRID_SIZE = 10
|
||||
DEFAULT_ADD_TIMESTAMP = True
|
||||
|
||||
+210
-41
@@ -72,19 +72,67 @@ LANE_GAP = 9 # between the vertical runs of two wires in one gap
|
||||
SELF_LOOP_DROP = 18 # how far below its box a component's link to itself runs
|
||||
LABEL_GAP = 6 # between a box's bottom and the note under it
|
||||
|
||||
EPSILON = 0.5 # two coordinates this close are the same coordinate
|
||||
|
||||
|
||||
def column_gap(wire_count):
|
||||
"""How wide the gap beside a column has to be to hold its wires.
|
||||
|
||||
Every wire crossing a gap turns in a vertical run of its own, and two runs on the
|
||||
same line are one line as far as a reader is concerned -- so the gap is sized to the
|
||||
number of wires rather than fixed, and a component with a dozen links gets a wider
|
||||
gap instead of a dozen wires in a 30px band. COLUMN_GAP is the floor, for the few
|
||||
links that would otherwise be drawn closer together than the boxes they join.
|
||||
A wire that cannot run straight turns in a vertical run of its own, and two runs on
|
||||
the same line are one line as far as a reader is concerned -- so the gap is sized to
|
||||
the number of wires rather than fixed, and a component with a dozen links gets a
|
||||
wider gap instead of a dozen wires in a 30px band. COLUMN_GAP is the floor, for the
|
||||
few links that would otherwise be drawn closer together than the boxes they join.
|
||||
"""
|
||||
return max(COLUMN_GAP, 2 * STUB + LANE_GAP * (wire_count + 1))
|
||||
|
||||
|
||||
def spans_overlap(first, second):
|
||||
"""Whether two 1-D spans share more than a single point."""
|
||||
low = max(min(first), min(second))
|
||||
high = min(max(first), max(second))
|
||||
return high - low > EPSILON
|
||||
|
||||
|
||||
def strictly_within(value, span):
|
||||
"""Whether a coordinate is inside a span rather than at either end of it."""
|
||||
return min(span) + EPSILON < value < max(span) - EPSILON
|
||||
|
||||
|
||||
def segments_meet(first, second):
|
||||
"""Whether two axis-aligned segments make a mark a reader would read as a tangle.
|
||||
|
||||
Two kinds count. A vertical run crossing a horizontal one is a crossing, and two
|
||||
vertical runs sharing a line over the same stretch are one line where there should
|
||||
be two.
|
||||
|
||||
Two *horizontal* runs sharing a line deliberately do not count: every wire leaving
|
||||
one output slot leaves it at the same height, so a fan-out shares the run beside its
|
||||
pin. That is how a wiresheet draws a fan-out too, and forcing those apart would move
|
||||
a wire off the slot it belongs to.
|
||||
"""
|
||||
(a1, a2), (b1, b2) = first, second
|
||||
a_vertical = abs(a1[0] - a2[0]) < EPSILON
|
||||
b_vertical = abs(b1[0] - b2[0]) < EPSILON
|
||||
|
||||
if a_vertical and b_vertical:
|
||||
return (abs(a1[0] - b1[0]) < EPSILON
|
||||
and spans_overlap((a1[1], a2[1]), (b1[1], b2[1])))
|
||||
if not a_vertical and not b_vertical:
|
||||
return False
|
||||
|
||||
(v1, v2), (h1, h2) = (first, second) if a_vertical else (second, first)
|
||||
return (strictly_within(v1[0], (h1[0], h2[0]))
|
||||
and strictly_within(h1[1], (v1[1], v2[1])))
|
||||
|
||||
|
||||
def routes_meet(first, second):
|
||||
"""Whether two wires, each a list of corner points, meet anywhere along their way."""
|
||||
first_segments = list(zip(first, first[1:]))
|
||||
second_segments = list(zip(second, second[1:]))
|
||||
return any(segments_meet(a, b) for a in first_segments for b in second_segments)
|
||||
|
||||
|
||||
def sheet_display_name(sheet_element):
|
||||
"""What to call the wiresheet an element sits on.
|
||||
|
||||
@@ -186,6 +234,18 @@ def group_by_peer(entries):
|
||||
return groups
|
||||
|
||||
|
||||
def _slot_order_key(group, focus_row_of):
|
||||
"""Where a peer belongs in its column: the first focus slot its links land on.
|
||||
|
||||
A peer wired to several of the chosen component's slots is placed by the topmost of
|
||||
them, and the path breaks a tie so that two peers on one slot keep a stable order
|
||||
from one draw to the next.
|
||||
"""
|
||||
unknown = len(focus_row_of)
|
||||
rows = [focus_row_of.get(entry["comp_slot"], unknown) for entry in group["entries"]]
|
||||
return (min(rows), group["peer_path"])
|
||||
|
||||
|
||||
class LinkViewPanel:
|
||||
"""One component's links, drawn around it on a canvas of this view's own.
|
||||
|
||||
@@ -217,6 +277,10 @@ class LinkViewPanel:
|
||||
self.link_by_tag = {}
|
||||
self.hover_restore = {}
|
||||
|
||||
# Numbers the wires of one draw, so every line has a tag of its own even when
|
||||
# two of them stand for links between the same pair of components.
|
||||
self._wire_seq = 0
|
||||
|
||||
self.frame = tk.Frame(parent)
|
||||
|
||||
# The footer is packed first and against the bottom: pack hands out leftover
|
||||
@@ -338,6 +402,7 @@ class LinkViewPanel:
|
||||
canvas.delete("all")
|
||||
self.link_by_tag.clear()
|
||||
self.hover_restore.clear()
|
||||
self._wire_seq = 0
|
||||
|
||||
self.entries = collect_links(self.comp)
|
||||
count = len(self.entries)
|
||||
@@ -352,7 +417,21 @@ class LinkViewPanel:
|
||||
outgoing = group_by_peer([e for e in self.entries if e["side"] == "out"])
|
||||
self_links = [e for e in self.entries if e["side"] == "self"]
|
||||
|
||||
focus_size = wiresheet.measure_box(self.comp, fonts, linked_slots)[:2]
|
||||
focus_width, focus_height, focus_rows = wiresheet.measure_box(
|
||||
self.comp, fonts, linked_slots)[:3]
|
||||
focus_size = (focus_width, focus_height)
|
||||
|
||||
# Which row of the chosen component each of its slots is on. Peers are put in
|
||||
# that order, top to bottom, so the wires form a ladder instead of a weave: a
|
||||
# component feeding the first slot sits above one feeding the fifth, and neither
|
||||
# has any reason to cross the other on the way in. Read from the measurement
|
||||
# rather than from the drawn box, because the order has to be settled before
|
||||
# there is anything drawn to read it off.
|
||||
focus_row_of = {raw: index for index, (raw, _label, _value)
|
||||
in enumerate(focus_rows)}
|
||||
incoming.sort(key=lambda group: _slot_order_key(group, focus_row_of))
|
||||
outgoing.sort(key=lambda group: _slot_order_key(group, focus_row_of))
|
||||
|
||||
left_sizes = [self._group_size(group, fonts, linked_slots) for group in incoming]
|
||||
right_sizes = [self._group_size(group, fonts, linked_slots) for group in outgoing]
|
||||
|
||||
@@ -370,7 +449,12 @@ class LinkViewPanel:
|
||||
|
||||
focus_x = MARGIN + (left_width + left_gap if incoming else 0)
|
||||
right_x = focus_x + focus_size[0] + right_gap
|
||||
focus_y = MARGIN + (content_height - focus_size[1]) / 2
|
||||
|
||||
# Rounded to whole pixels. Every height and slot offset here is an integer, so
|
||||
# rounding the one division in the layout is what lets two slots that ought to
|
||||
# line up line up exactly -- and an exactly aligned pair is a wire drawn as one
|
||||
# straight line rather than one with a 1px jog in it.
|
||||
focus_y = MARGIN + round((content_height - focus_size[1]) / 2)
|
||||
|
||||
focus_box = self._draw_box(self.comp, focus_x, focus_y, fonts, linked_slots,
|
||||
("lv_focus",), outline_tags=("lv_focus_outline",))
|
||||
@@ -383,27 +467,25 @@ class LinkViewPanel:
|
||||
columns = ((incoming, left_sizes, left_height, left_width, MARGIN, "in"),
|
||||
(outgoing, right_sizes, right_height, right_width, right_x, "out"))
|
||||
for column, sizes, column_height, column_width, column_x, side in columns:
|
||||
y = MARGIN + (content_height - column_height) / 2
|
||||
y = MARGIN + round((content_height - column_height) / 2)
|
||||
for group, (width, height) in zip(column, sizes):
|
||||
# Boxes are right-aligned on the left and left-aligned on the right, so
|
||||
# every wire crosses the same gap however wide its own box turned out.
|
||||
x = column_x + (column_width - width) if side == "in" else column_x
|
||||
box = self._draw_group_box(group, x, y, fonts, linked_slots)
|
||||
for entry in group["entries"]:
|
||||
jobs[side].append((entry, box))
|
||||
jobs[side].append(self._wire_ends(entry, side, focus_box, box))
|
||||
y += height + ROW_GAP
|
||||
|
||||
# Lanes are handed out nearest-first, so a wire reaching a distant box turns
|
||||
# outside one reaching a near box rather than across it. Both ends of a link on
|
||||
# this side are known by now, which is what lets the order be worked out at all.
|
||||
# Each side is routed as a set, not wire by wire: which lane a wire may turn in
|
||||
# depends on where the others turn, so they cannot be settled one at a time.
|
||||
for side, side_jobs in jobs.items():
|
||||
side_jobs.sort(key=lambda job: abs(
|
||||
self._slot_y(job[1], job[0]["peer_slot"])
|
||||
- self._slot_y(focus_box, job[0]["comp_slot"])))
|
||||
for rank, (entry, box) in enumerate(side_jobs):
|
||||
self._draw_wire(entry, side, focus_box, box, rank)
|
||||
self._route_side(side_jobs, focus_box, side)
|
||||
for job in side_jobs:
|
||||
self._draw_wire(job)
|
||||
|
||||
# A self-link crosses both gaps, so it takes the next lane out on each side.
|
||||
# A self-link crosses both gaps, so it takes the lane beyond every wire that has
|
||||
# a peer to reach, on each side.
|
||||
for offset, entry in enumerate(self_links):
|
||||
self._draw_self_wire(entry, focus_box,
|
||||
len(jobs["in"]) + offset,
|
||||
@@ -517,48 +599,134 @@ class LinkViewPanel:
|
||||
|
||||
Out from the focus rather than in from the peer, because every wire on one side
|
||||
leaves the same box edge while the boxes it reaches are all different heights:
|
||||
anchoring to the shared edge is what makes the fan nest instead of tangle.
|
||||
`column_gap` sized the gap to hold every rank, so a lane always lands inside it.
|
||||
anchoring to the shared edge is what lets the lanes nest. `column_gap` sized the
|
||||
gap to hold one lane per wire, so a rank inside that count lands inside the gap.
|
||||
"""
|
||||
if side == "in":
|
||||
return focus_box["x"] - STUB - (rank + 1) * LANE_GAP
|
||||
return focus_box["x"] + focus_box["width"] + STUB + (rank + 1) * LANE_GAP
|
||||
|
||||
def _draw_wire(self, entry, side, focus_box, peer_box, rank):
|
||||
"""One link, from the box that feeds it to the box that reads it.
|
||||
def _wire_ends(self, entry, side, focus_box, peer_box):
|
||||
"""Where one link's wire has to start and finish, before it is routed.
|
||||
|
||||
Drawn peer-to-focus on the left and focus-to-peer on the right, so every arrow
|
||||
points the way the signal travels and the picture reads left to right the way a
|
||||
wiresheet does. A link whose far end is on another sheet is dashed, and keeps
|
||||
the grey knob its own sheet paints on that slot -- so the slot looks the same
|
||||
here as it does there, with the far component drawn in as well.
|
||||
wiresheet does. A link whose far end is on another sheet keeps the grey knob its
|
||||
own sheet paints on that slot, so the slot looks the same here as it does there,
|
||||
and the wire stands off far enough to leave that marker its own space.
|
||||
"""
|
||||
focus_y = self._slot_y(focus_box, entry["comp_slot"])
|
||||
peer_y = self._slot_y(peer_box, entry["peer_slot"])
|
||||
off_sheet = not entry["same_sheet"]
|
||||
standoff = 2 * wiresheet.KNOB_RADIUS if off_sheet else 0
|
||||
|
||||
if side == "in":
|
||||
start = (peer_box["x"] + peer_box["width"], peer_y)
|
||||
end_x = focus_box["x"]
|
||||
knob_x = end_x - wiresheet.KNOB_RADIUS
|
||||
end = (end_x - 2 * wiresheet.KNOB_RADIUS if off_sheet else end_x, focus_y)
|
||||
end = (focus_box["x"] - standoff, focus_y)
|
||||
knob_x = focus_box["x"] - wiresheet.KNOB_RADIUS
|
||||
else:
|
||||
start = (focus_box["x"] + focus_box["width"], focus_y)
|
||||
knob_x = start[0] + wiresheet.KNOB_RADIUS
|
||||
if off_sheet:
|
||||
start = (start[0] + 2 * wiresheet.KNOB_RADIUS, focus_y)
|
||||
start = (focus_box["x"] + focus_box["width"] + standoff, focus_y)
|
||||
end = (peer_box["x"], peer_y)
|
||||
knob_x = focus_box["x"] + focus_box["width"] + wiresheet.KNOB_RADIUS
|
||||
|
||||
tag = f"lv_link_{id(entry['link'])}_{side}_{rank}"
|
||||
self._draw_route(
|
||||
self._route(start, end, self._lane_x(focus_box, side, rank)),
|
||||
tag, off_sheet
|
||||
)
|
||||
# The two heights are kept apart from the two ends: which end is the focus's
|
||||
# swaps between the sides, and the lane rule is written in terms of the slots
|
||||
# rather than the direction the line happens to be drawn in.
|
||||
return {"entry": entry, "side": side, "start": start, "end": end,
|
||||
"focus_y": focus_y, "peer_y": peer_y,
|
||||
"knob": (knob_x, focus_y) if off_sheet else None,
|
||||
"off_sheet": off_sheet, "route": None}
|
||||
|
||||
if off_sheet:
|
||||
def _route_side(self, side_jobs, focus_box, side):
|
||||
"""Chooses a lane for every wire on one side so that no two of them meet.
|
||||
|
||||
The peers are already stacked in the order of the focus slots their links land
|
||||
on, so both ends of the set run the same way down the page -- a ladder, not a
|
||||
weave. For a ladder there is a lane order that cannot produce a crossing, and it
|
||||
is two fans rather than one order. Reading it off the slots, not off the
|
||||
direction the line is drawn in, it is the same on both sides:
|
||||
|
||||
a wire reaching a peer slot *above* its focus slot turns nearer the focus
|
||||
the higher up the focus it lands
|
||||
|
||||
a wire reaching a peer slot *below* its focus slot turns nearer the focus
|
||||
the lower down the focus it lands
|
||||
|
||||
So each fan nests outwards from the focus's own row. Take two wires of the upper
|
||||
fan, the higher one nested inside the lower: the lower one's run into the focus
|
||||
passes below where the higher one's vertical ends, and its run out to its peer
|
||||
passes above where that vertical begins, so it goes round it and never through
|
||||
it. The lower fan is the same argument upside down. And a wire of one fan can
|
||||
never share a stretch of the page with a wire of the other -- ordering the
|
||||
ladder is what rules that out -- so the two fans reuse each other's lanes and
|
||||
the gap stays as narrow as the picture allows.
|
||||
|
||||
Each fan is walked from the lane against the focus outwards, and every wire
|
||||
still has to pass the collision check before it takes a lane. The argument above
|
||||
rests on the ladder being ordered, and a peer whose own slots run counter to the
|
||||
focus's is not quite a ladder; the check is what makes that case come out tidy
|
||||
anyway rather than trusting the reasoning to cover it.
|
||||
"""
|
||||
straight, above, below = [], [], []
|
||||
for job in side_jobs:
|
||||
offset = job["peer_y"] - job["focus_y"]
|
||||
if abs(offset) < EPSILON:
|
||||
straight.append(job)
|
||||
elif offset < 0:
|
||||
above.append(job)
|
||||
else:
|
||||
below.append(job)
|
||||
|
||||
# Within a fan, the focus slot each wire lands on is the order that matters --
|
||||
# except where several wires leave the *same* slot, which no order of focus
|
||||
# heights can separate. That is a fan-out, and there the longest branch has to
|
||||
# turn first, against the focus: the shorter ones then carry on along the shared
|
||||
# run beside the pin and drop away outside it, which is how a wiresheet draws a
|
||||
# fan-out and the only order that keeps a branch from crossing its neighbour.
|
||||
def fan_order(job, direction):
|
||||
return (direction * job["focus_y"],
|
||||
-abs(job["peer_y"] - job["focus_y"]))
|
||||
|
||||
above.sort(key=lambda job: fan_order(job, 1))
|
||||
below.sort(key=lambda job: fan_order(job, -1))
|
||||
|
||||
routed = []
|
||||
for job in straight:
|
||||
# The slots already line up: one straight line, no vertical run to place.
|
||||
# This is what ordering the peers buys, and it is settled first because such
|
||||
# a wire crosses the whole gap and everything else has to be fitted past it.
|
||||
job["route"] = [job["start"], job["end"]]
|
||||
routed.append(job)
|
||||
|
||||
for job in above + below:
|
||||
for rank in range(len(side_jobs)):
|
||||
candidate = self._route(job["start"], job["end"],
|
||||
self._lane_x(focus_box, side, rank))
|
||||
if not any(routes_meet(candidate, other["route"])
|
||||
for other in routed):
|
||||
job["route"] = candidate
|
||||
break
|
||||
else:
|
||||
# No lane in the gap is clear. Rather than leave the link off the
|
||||
# picture it is drawn in the outermost lane and the crossing is
|
||||
# accepted -- a crossed wire says more than a missing one.
|
||||
job["route"] = self._route(
|
||||
job["start"], job["end"],
|
||||
self._lane_x(focus_box, side, len(side_jobs) - 1)
|
||||
)
|
||||
routed.append(job)
|
||||
|
||||
def _draw_wire(self, job):
|
||||
"""Paints one routed wire, and the knob of a link that leaves the sheet."""
|
||||
entry = job["entry"]
|
||||
self._wire_seq += 1
|
||||
tag = f"lv_link_{id(entry['link'])}_{self._wire_seq}"
|
||||
self._draw_route(job["route"], tag, job["off_sheet"])
|
||||
|
||||
if job["knob"] is not None:
|
||||
# The knob belongs to the slot, not to the wire, so it is drawn outside the
|
||||
# wire's tag: hovering the line must not recolour the slot's marker.
|
||||
wiresheet.draw_knob(self.canvas, knob_x, focus_y, False, True)
|
||||
wiresheet.draw_knob(self.canvas, job["knob"][0], job["knob"][1], False, True)
|
||||
|
||||
self._make_followable(tag, entry["link"], 1)
|
||||
|
||||
@@ -570,7 +738,7 @@ class LinkViewPanel:
|
||||
Two slots already at the same height need no turn at all.
|
||||
"""
|
||||
(sx, sy), (ex, ey) = start, end
|
||||
if abs(sy - ey) < 0.5:
|
||||
if abs(sy - ey) < EPSILON:
|
||||
return [(sx, sy), (ex, ey)]
|
||||
return [(sx, sy), (lane_x, sy), (lane_x, ey), (ex, ey)]
|
||||
|
||||
@@ -593,7 +761,8 @@ class LinkViewPanel:
|
||||
(right_lane, below), (left_lane, below),
|
||||
(left_lane, in_y), (focus_box["x"], in_y)]
|
||||
|
||||
tag = f"lv_link_{id(entry['link'])}_self_{out_rank}"
|
||||
self._wire_seq += 1
|
||||
tag = f"lv_link_{id(entry['link'])}_{self._wire_seq}"
|
||||
self._draw_route(points, tag, not entry["same_sheet"])
|
||||
self._make_followable(tag, entry["link"], 1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user