predictygraph 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {predictygraph-0.1.0 → predictygraph-0.1.1}/PKG-INFO +1 -1
- {predictygraph-0.1.0 → predictygraph-0.1.1}/pyproject.toml +1 -1
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/route.py +49 -22
- predictygraph-0.1.1/tests/test_routing.py +60 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/README.md +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/__init__.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/__main__.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/anim.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/cli.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/default.css +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/Carlito-Bold.ttf +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/Carlito-Regular.ttf +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/DejaVuSans-Bold.ttf +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/DejaVuSans.ttf +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/LiberationMono-Bold.ttf +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/LiberationMono-Regular.ttf +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/LiberationSans-Bold.ttf +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/LiberationSans-Regular.ttf +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/LiberationSerif-Bold.ttf +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/LiberationSerif-Regular.ttf +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/ir.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/layout.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/model.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/parser.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/render/__init__.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/render/apng.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/render/svg.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/safe.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/scene.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/style.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/text.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/theme.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/themes/catppuccin-frappe.toml +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/themes/catppuccin-latte.toml +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/themes/catppuccin-macchiato.toml +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/themes/catppuccin-mocha.toml +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/themes/default.toml +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/themes/dracula.toml +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/themes/github-light.toml +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/themes/gruvbox-dark.toml +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/themes/nord.toml +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/themes/solarized-dark.toml +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/themes/solarized-light.toml +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/tests/test_diagonal.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/tests/test_fixes.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/tests/test_hardening.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/tests/test_predictygraph.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/tests/test_scene.py +0 -0
- {predictygraph-0.1.0 → predictygraph-0.1.1}/tests/test_targets.py +0 -0
|
@@ -20,6 +20,7 @@ from .style import px
|
|
|
20
20
|
LANE = 9.0 # distance between parallel lanes
|
|
21
21
|
CLEAR = 12.0 # min distance between a horizontal run and the boxes above/below it
|
|
22
22
|
RECT_SHAPES = {"box", "square", "round", "pill", "cylinder", "note", "image", "none"}
|
|
23
|
+
ROUND_SHAPES = {"circle", "ellipse", "diamond"}
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def lca(a, b, root):
|
|
@@ -62,6 +63,23 @@ def port(n, side):
|
|
|
62
63
|
"left": (n.ax, n.cy), "right": (n.ax + n.w, n.cy)}[side]
|
|
63
64
|
|
|
64
65
|
|
|
66
|
+
def on_outline(n, p, vert):
|
|
67
|
+
"""Move a port that was shifted along a side back onto the node's outline.
|
|
68
|
+
Boxes are flat, so only circles, ellipses and diamonds need it: the point slides
|
|
69
|
+
along the flow axis until it touches the curve (or slanted edge)."""
|
|
70
|
+
shape = n.attrs.get("shape", "box")
|
|
71
|
+
if isinstance(n, Group) or shape not in ROUND_SHAPES:
|
|
72
|
+
return p
|
|
73
|
+
hw, hh = n.w / 2, n.h / 2
|
|
74
|
+
if vert:
|
|
75
|
+
u = min(abs(p[0] - n.cx) / hw, 1.0)
|
|
76
|
+
reach = hh * (math.sqrt(1 - u * u) if shape != "diamond" else 1 - u)
|
|
77
|
+
return p[0], n.cy + math.copysign(reach, p[1] - n.cy)
|
|
78
|
+
u = min(abs(p[1] - n.cy) / hh, 1.0)
|
|
79
|
+
reach = hw * (math.sqrt(1 - u * u) if shape != "diamond" else 1 - u)
|
|
80
|
+
return n.cx + math.copysign(reach, p[0] - n.cx), p[1]
|
|
81
|
+
|
|
82
|
+
|
|
65
83
|
@dataclass
|
|
66
84
|
class Diagonal:
|
|
67
85
|
"""When may a bend/ortho jog be a slant instead of a right-angle step?"""
|
|
@@ -166,42 +184,48 @@ class Router:
|
|
|
166
184
|
|
|
167
185
|
# ------------------------------------------------------------ passes
|
|
168
186
|
def _parallel_offsets(self):
|
|
187
|
+
"""Edges between the same two nodes get side-by-side lanes. The offset is in
|
|
188
|
+
absolute coordinates, so an edge and its reverse simply take different lanes."""
|
|
169
189
|
pairs: dict[tuple[str, str], list[Edge]] = {}
|
|
170
190
|
for e in self.doc.edges:
|
|
171
191
|
pairs.setdefault(tuple(sorted((e.src, e.dst))), []).append(e)
|
|
172
192
|
for es in pairs.values():
|
|
173
193
|
n = len(es)
|
|
174
194
|
for i, e in enumerate(es):
|
|
175
|
-
|
|
176
|
-
# opposite-direction edges get mirrored sign so they stay apart
|
|
177
|
-
self.offset[e.id] = k if e.src <= e.dst else -k
|
|
195
|
+
self.offset[e.id] = (i - (n - 1) / 2) * 12
|
|
178
196
|
|
|
179
197
|
def _spread_ports(self):
|
|
180
198
|
"""Several curved/bent edges on one side of a node: spread them symmetrically
|
|
181
|
-
around the side's middle
|
|
199
|
+
around the side's middle, ordered by where they go, so they don't cross.
|
|
200
|
+
Parallel edges (same two nodes) take part too, which keeps them apart and in
|
|
201
|
+
the same order at both ends."""
|
|
182
202
|
sides: dict[tuple[str, str], list] = {}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
203
|
+
saved, self.offset = self.offset, {} # order by the unshifted geometry
|
|
204
|
+
try:
|
|
205
|
+
for e in self.doc.edges:
|
|
206
|
+
if e.attrs.get("exit") or e.attrs.get("enter"):
|
|
207
|
+
continue
|
|
208
|
+
pts, axis = self.geometry(e)
|
|
209
|
+
if axis not in ("v", "h"):
|
|
210
|
+
continue
|
|
211
|
+
a, b = self.doc.items[e.src], self.doc.items[e.dst]
|
|
212
|
+
if flow_dir(lca(a, b, self.doc.root)) == "auto" or e.style.get("route", "curve") in ("straight", "ortho"):
|
|
213
|
+
continue # ortho fan-in/out merges into a bus instead
|
|
214
|
+
i = 0 if axis == "v" else 1
|
|
215
|
+
for end, node, here, there in ((0, a, pts[0], pts[1] if len(pts) > 2 else pts[-1]),
|
|
216
|
+
(1, b, pts[-1], pts[-2] if len(pts) > 2 else pts[0])):
|
|
217
|
+
side = ("bottom" if there[1] > here[1] else "top") if axis == "v" else \
|
|
218
|
+
("right" if there[0] > here[0] else "left")
|
|
219
|
+
sides.setdefault((node.id, side), []).append((there[i], e.id, end))
|
|
220
|
+
finally:
|
|
221
|
+
self.offset = saved
|
|
198
222
|
for (nid, side), lst in sides.items():
|
|
199
223
|
if len(lst) < 2:
|
|
200
224
|
continue
|
|
201
225
|
n = self.doc.items[nid]
|
|
202
|
-
if n.attrs.get("shape", "box") in ("diamond", "circle", "ellipse") and not isinstance(n, Group):
|
|
203
|
-
continue
|
|
204
226
|
span = n.w if side in ("top", "bottom") else n.h
|
|
227
|
+
if n.attrs.get("shape", "box") in ROUND_SHAPES and not isinstance(n, Group):
|
|
228
|
+
span *= 0.6 # stay on the part of the outline that faces the side
|
|
205
229
|
step = min(18.0, (span - 12) / (len(lst) - 1)) if span > 12 else 0.0
|
|
206
230
|
lst.sort()
|
|
207
231
|
for k, (_, eid, end) in enumerate(lst):
|
|
@@ -371,18 +395,21 @@ class Router:
|
|
|
371
395
|
elif not wps:
|
|
372
396
|
self._record_channel(e, owner, da, db, vert)
|
|
373
397
|
p0, p1 = self._ports(a, b, vert)
|
|
398
|
+
spread = (e.id, 0) in self.port_shift or (e.id, 1) in self.port_shift
|
|
374
399
|
s0, s1 = self.port_shift.get((e.id, 0), 0.0), self.port_shift.get((e.id, 1), 0.0)
|
|
375
400
|
if vert:
|
|
376
401
|
p0, p1 = (p0[0] + s0, p0[1]), (p1[0] + s1, p1[1])
|
|
377
402
|
else:
|
|
378
403
|
p0, p1 = (p0[0], p0[1] + s0), (p1[0], p1[1] + s1)
|
|
379
|
-
if
|
|
404
|
+
if spread:
|
|
405
|
+
p0, p1 = on_outline(a, p0, vert), on_outline(b, p1, vert)
|
|
380
406
|
return [p0] + wps + [p1], ("v" if vert else "h")
|
|
381
407
|
if off:
|
|
382
408
|
if vert:
|
|
383
409
|
p0, p1 = (p0[0] + off, p0[1]), (p1[0] + off, p1[1])
|
|
384
410
|
else:
|
|
385
411
|
p0, p1 = (p0[0], p0[1] + off), (p1[0], p1[1] + off)
|
|
412
|
+
p0, p1 = on_outline(a, p0, vert), on_outline(b, p1, vert)
|
|
386
413
|
# boxes overlap on the cross axis: one straight line, no jog / double trace
|
|
387
414
|
boxy = all(x.attrs.get("shape", "box") in RECT_SHAPES or isinstance(x, Group) for x in (a, b))
|
|
388
415
|
if not wps and not off and boxy:
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 The predictygraph Authors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
"""Routing regressions: edges between the same nodes, and edges fanning out of a
|
|
6
|
+
round node, must stay apart and must not cross."""
|
|
7
|
+
from predictygraph import _style, parse
|
|
8
|
+
from predictygraph.layout import layout
|
|
9
|
+
from predictygraph.route import Router
|
|
10
|
+
|
|
11
|
+
STATES = """graph dir=LR
|
|
12
|
+
default node shape=circle
|
|
13
|
+
idle "Idle"
|
|
14
|
+
running "Running"
|
|
15
|
+
error "Error"
|
|
16
|
+
done "Done"
|
|
17
|
+
idle -> running "Start"
|
|
18
|
+
running -> error "Fail"
|
|
19
|
+
running -> done "OK"
|
|
20
|
+
error -> running "Retry"
|
|
21
|
+
done -> idle "Reset"
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _ends():
|
|
26
|
+
doc = _style(parse(STATES), None, None, None)
|
|
27
|
+
layout(doc)
|
|
28
|
+
r = Router(doc).route()
|
|
29
|
+
ends = {}
|
|
30
|
+
for e in doc.edges:
|
|
31
|
+
pts, _ = r.geometry(e)
|
|
32
|
+
ends[(e.src, e.dst)] = (pts[0], pts[-1])
|
|
33
|
+
return ends
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_edge_and_its_reverse_do_not_overlap():
|
|
37
|
+
ends = _ends()
|
|
38
|
+
fail = ends[("running", "error")]
|
|
39
|
+
retry = ends[("error", "running")]
|
|
40
|
+
# Fail leaves Running at p0, Retry arrives at Running at p1 (and vice versa at Error)
|
|
41
|
+
assert abs(fail[0][1] - retry[1][1]) >= 8, "Fail and Retry share a port on Running"
|
|
42
|
+
assert abs(fail[1][1] - retry[0][1]) >= 8, "Fail and Retry share a port on Error"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_edges_leaving_one_side_keep_their_order():
|
|
46
|
+
ends = _ends()
|
|
47
|
+
# on Running's right side: Fail (to Error, top) above Retry above OK (to Done, bottom)
|
|
48
|
+
ys = [ends[("running", "error")][0][1], ends[("error", "running")][1][1], ends[("running", "done")][0][1]]
|
|
49
|
+
assert ys == sorted(ys), f"edges cross on Running's right side: {ys}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_shifted_ports_sit_on_the_circle():
|
|
53
|
+
doc = _style(parse(STATES), None, None, None)
|
|
54
|
+
layout(doc)
|
|
55
|
+
r = Router(doc).route()
|
|
56
|
+
for e in doc.edges:
|
|
57
|
+
pts, _ = r.geometry(e)
|
|
58
|
+
for n, p in ((doc.items[e.src], pts[0]), (doc.items[e.dst], pts[-1])):
|
|
59
|
+
d = ((p[0] - n.cx) / (n.w / 2)) ** 2 + ((p[1] - n.cy) / (n.h / 2)) ** 2
|
|
60
|
+
assert abs(d - 1) < 0.02, f"{e.src}->{e.dst} end is off the outline of {n.id}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/LiberationMono-Regular.ttf
RENAMED
|
File without changes
|
|
File without changes
|
{predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/LiberationSans-Regular.ttf
RENAMED
|
File without changes
|
{predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/LiberationSerif-Bold.ttf
RENAMED
|
File without changes
|
{predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/fonts/LiberationSerif-Regular.ttf
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{predictygraph-0.1.0 → predictygraph-0.1.1}/src/predictygraph/themes/catppuccin-macchiato.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|