predictygraph 0.2.2__tar.gz → 0.2.4__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.2.2 → predictygraph-0.2.4}/PKG-INFO +32 -8
- {predictygraph-0.2.2 → predictygraph-0.2.4}/README.md +31 -7
- {predictygraph-0.2.2 → predictygraph-0.2.4}/pyproject.toml +1 -1
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/layout.py +41 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/scene.py +43 -10
- predictygraph-0.2.4/tests/test_stack.py +98 -0
- predictygraph-0.2.2/tests/test_stack.py +0 -41
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/__init__.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/__main__.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/anim.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/cli.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/default.css +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts/Carlito-Bold.ttf +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts/Carlito-Regular.ttf +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts/DejaVuSans-Bold.ttf +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts/DejaVuSans.ttf +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts/LiberationMono-Bold.ttf +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts/LiberationMono-Regular.ttf +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts/LiberationSans-Bold.ttf +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts/LiberationSans-Regular.ttf +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts/LiberationSerif-Bold.ttf +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts/LiberationSerif-Regular.ttf +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/ir.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/model.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/parser.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/render/__init__.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/render/apng.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/render/svg.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/route.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/safe.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/style.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/styleutil.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/text.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/theme.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/themes/catppuccin-frappe.toml +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/themes/catppuccin-latte.toml +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/themes/catppuccin-macchiato.toml +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/themes/catppuccin-mocha.toml +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/themes/default.toml +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/themes/dracula.toml +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/themes/github-light.toml +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/themes/gruvbox-dark.toml +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/themes/hami.toml +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/themes/nord.toml +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/themes/solarized-dark.toml +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/themes/solarized-light.toml +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/tests/test_annotations.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/tests/test_diagonal.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/tests/test_fixes.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/tests/test_hardening.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/tests/test_predictygraph.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/tests/test_routing.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/tests/test_scene.py +0 -0
- {predictygraph-0.2.2 → predictygraph-0.2.4}/tests/test_targets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: predictygraph
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Predictable graphs: a readable, CSS-styled diagram DSL with per-group layouts, rendered to SVG/APNG
|
|
5
5
|
Author-Email: Reza Jelveh <git@jelveh.me>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -27,16 +27,40 @@ Editor support: [tree-sitter-pdg](https://github.com/predictygraph/tree-sitter-p
|
|
|
27
27
|
Rendered examples and the gallery: [predictygraph.github.io](https://predictygraph.github.io/). This repo keeps
|
|
28
28
|
only `.pdg` sources; images live in the docs repo (git-lfs).
|
|
29
29
|
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv tool install "predictygraph[apng]" # or: pipx install "predictygraph[apng]"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`[apng]` adds PNG output. `pdg` is the short alias of the `predictygraph` command.
|
|
37
|
+
|
|
38
|
+
## Use
|
|
39
|
+
|
|
30
40
|
```bash
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
pdg examples/annotations.pdg # -> examples/annotations.svg (not committed)
|
|
42
|
+
pdg diagram.pdg --css theme.css -o out.svg
|
|
43
|
+
pdg diagram.pdg -o out.png # PNG (animated if the diagram moves)
|
|
44
|
+
pdg diagram.pdg --check # validate only
|
|
45
|
+
pdg diagram.pdg -o d.png --width 1000 --colors 64 # small PNG for chat
|
|
46
|
+
pdg diagram.pdg --target web # size labels to fit any browser font
|
|
37
47
|
```
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
From a checkout, prefix these with `uv run`. Start with
|
|
50
|
+
[examples/annotations.pdg](examples/annotations.pdg): one small diagram that uses every
|
|
51
|
+
annotation feature, with a comment for each.
|
|
52
|
+
|
|
53
|
+
## Agent skill
|
|
54
|
+
|
|
55
|
+
[skills/predictygraph](skills/predictygraph) teaches an AI agent to write, render and
|
|
56
|
+
check `.pdg` diagrams.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Hermes Agent
|
|
60
|
+
hermes skills install https://raw.githubusercontent.com/predictygraph/predictygraph/master/skills/predictygraph/SKILL.md
|
|
61
|
+
# Claude Code and other agents that read SKILL.md folders: copy the folder
|
|
62
|
+
cp -r skills/predictygraph ~/.claude/skills/
|
|
63
|
+
```
|
|
40
64
|
|
|
41
65
|
## Credits
|
|
42
66
|
|
|
@@ -13,16 +13,40 @@ Editor support: [tree-sitter-pdg](https://github.com/predictygraph/tree-sitter-p
|
|
|
13
13
|
Rendered examples and the gallery: [predictygraph.github.io](https://predictygraph.github.io/). This repo keeps
|
|
14
14
|
only `.pdg` sources; images live in the docs repo (git-lfs).
|
|
15
15
|
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv tool install "predictygraph[apng]" # or: pipx install "predictygraph[apng]"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`[apng]` adds PNG output. `pdg` is the short alias of the `predictygraph` command.
|
|
23
|
+
|
|
24
|
+
## Use
|
|
25
|
+
|
|
16
26
|
```bash
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
pdg examples/annotations.pdg # -> examples/annotations.svg (not committed)
|
|
28
|
+
pdg diagram.pdg --css theme.css -o out.svg
|
|
29
|
+
pdg diagram.pdg -o out.png # PNG (animated if the diagram moves)
|
|
30
|
+
pdg diagram.pdg --check # validate only
|
|
31
|
+
pdg diagram.pdg -o d.png --width 1000 --colors 64 # small PNG for chat
|
|
32
|
+
pdg diagram.pdg --target web # size labels to fit any browser font
|
|
23
33
|
```
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
From a checkout, prefix these with `uv run`. Start with
|
|
36
|
+
[examples/annotations.pdg](examples/annotations.pdg): one small diagram that uses every
|
|
37
|
+
annotation feature, with a comment for each.
|
|
38
|
+
|
|
39
|
+
## Agent skill
|
|
40
|
+
|
|
41
|
+
[skills/predictygraph](skills/predictygraph) teaches an AI agent to write, render and
|
|
42
|
+
check `.pdg` diagrams.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Hermes Agent
|
|
46
|
+
hermes skills install https://raw.githubusercontent.com/predictygraph/predictygraph/master/skills/predictygraph/SKILL.md
|
|
47
|
+
# Claude Code and other agents that read SKILL.md folders: copy the folder
|
|
48
|
+
cp -r skills/predictygraph ~/.claude/skills/
|
|
49
|
+
```
|
|
26
50
|
|
|
27
51
|
## Credits
|
|
28
52
|
|
|
@@ -160,6 +160,9 @@ class LineLayout(Layout):
|
|
|
160
160
|
if j != i + 1:
|
|
161
161
|
continue
|
|
162
162
|
w, _, _ = text_block(e.label, e.label_style or e.style, 11, bold=False)
|
|
163
|
+
if e.attrs.get("step"): # the step badge sits just left of the label
|
|
164
|
+
bw, bh, _ = text_block(e.attrs["step"][:3], e.step_style or e.style, 11, bold=True)
|
|
165
|
+
w += max(bw, bh) + 12
|
|
163
166
|
need[i] = max(need.get(i, 0.0), w + 34)
|
|
164
167
|
return need
|
|
165
168
|
|
|
@@ -555,6 +558,43 @@ def absolutize(g: Group, ox: float, oy: float) -> None:
|
|
|
555
558
|
absolutize(c, c.ax, c.ay)
|
|
556
559
|
|
|
557
560
|
|
|
561
|
+
def shift(item: Item, dx: float) -> None:
|
|
562
|
+
"""Move an item sideways by dx after absolutize: its own x (relative to its parent)
|
|
563
|
+
and the absolute position of it and everything inside it."""
|
|
564
|
+
item.x += dx
|
|
565
|
+
|
|
566
|
+
def move(i: Item) -> None:
|
|
567
|
+
i.ax += dx
|
|
568
|
+
if isinstance(i, Group):
|
|
569
|
+
for c in i.children:
|
|
570
|
+
move(c)
|
|
571
|
+
move(item)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def align_x(doc: Document) -> None:
|
|
575
|
+
"""`align-x=ID`: centre an item horizontally on another item, e.g. line up a box with
|
|
576
|
+
the one it calls in the panel below. Siblings on the side it moves towards travel with
|
|
577
|
+
it, so every gap (and the edge labels in it) keeps its width; the whole block stays
|
|
578
|
+
inside the parent's content box, so no group is resized and nothing overlaps."""
|
|
579
|
+
for item in list(doc.walk()):
|
|
580
|
+
ref = doc.items.get(item.attrs.get("align-x", ""))
|
|
581
|
+
p = item.parent
|
|
582
|
+
if ref is None or ref is item or p is None:
|
|
583
|
+
continue
|
|
584
|
+
pr = box4(p.style.get("padding"), 14)[1]
|
|
585
|
+
lo_edge, hi_edge = p.ax + p.ox, p.ax + p.w - pr
|
|
586
|
+
dx = ref.cx - item.cx
|
|
587
|
+
if dx > 0: # item and everything right of it move right, up to the right edge
|
|
588
|
+
block = [s for s in p.children if s is item or s.ax >= item.ax + item.w - 1e-6]
|
|
589
|
+
dx = min(dx, hi_edge - max(s.ax + s.w for s in block))
|
|
590
|
+
else: # item and everything left of it move left, down to the left edge
|
|
591
|
+
block = [s for s in p.children if s is item or s.ax + s.w <= item.ax + 1e-6]
|
|
592
|
+
dx = max(dx, lo_edge - min(s.ax for s in block))
|
|
593
|
+
if abs(dx) > 0.01:
|
|
594
|
+
for s in block:
|
|
595
|
+
shift(s, dx)
|
|
596
|
+
|
|
597
|
+
|
|
558
598
|
def layout(doc: Document) -> None:
|
|
559
599
|
root = doc.root
|
|
560
600
|
for e in doc.edges: # layout may run again after the router widens gaps
|
|
@@ -562,6 +602,7 @@ def layout(doc: Document) -> None:
|
|
|
562
602
|
e.waypoints, e.wp_owner = [], None
|
|
563
603
|
layout_group(root, doc)
|
|
564
604
|
absolutize(root, 0, 0)
|
|
605
|
+
align_x(doc)
|
|
565
606
|
# translate virtual waypoints into absolute coords and attach to edges
|
|
566
607
|
for g in [root] + [i for i in doc.walk() if isinstance(i, Group)]:
|
|
567
608
|
lay = getattr(g, "layout_obj", None)
|
|
@@ -38,6 +38,43 @@ def point_along(poly: list[tuple[float, float]], frac: float) -> tuple[float, fl
|
|
|
38
38
|
return poly[-1]
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def corner_radius(st: dict, shape: str, w: float, h: float) -> float:
|
|
42
|
+
"""The corner radius a box shape is drawn with (shared by the box and its accent bar)."""
|
|
43
|
+
if shape == "pill":
|
|
44
|
+
return h / 2
|
|
45
|
+
r = px(st.get("border-radius"), 0)
|
|
46
|
+
return max(r, 10) if shape == "round" else r
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def accent_segs(x: float, y: float, w: float, h: float, bw: float, r: float, side: str) -> list:
|
|
50
|
+
"""Path for a `bw`-wide strip down one side of an (x, y, w, h) box with corner radius r.
|
|
51
|
+
The outer edge follows the box's corner arcs and the inner edge is straight, so the
|
|
52
|
+
strip stays inside the rounded outline. Drawn for the left side, mirrored for right."""
|
|
53
|
+
bw, r = min(bw, w / 2), min(r, h / 2)
|
|
54
|
+
if r <= 0:
|
|
55
|
+
pts = [(0, 0), (bw, 0), (bw, h), (0, h)]
|
|
56
|
+
segs = [("M", *pts[0])] + [("L", *p) for p in pts[1:]]
|
|
57
|
+
elif bw < r: # inner edge cuts the corner arc at height dy below the top
|
|
58
|
+
dy = r - math.sqrt(r * r - (r - bw) ** 2)
|
|
59
|
+
segs = [("M", 0, r), ("A", r, r, 0, 0, 1, bw, dy), ("L", bw, h - dy),
|
|
60
|
+
("A", r, r, 0, 0, 1, 0, h - r)]
|
|
61
|
+
else: # strip wider than the radius: full corner arc, then straight along top/bottom
|
|
62
|
+
segs = [("M", 0, r), ("A", r, r, 0, 0, 1, r, 0), ("L", bw, 0), ("L", bw, h), ("L", r, h),
|
|
63
|
+
("A", r, r, 0, 0, 1, 0, h - r)]
|
|
64
|
+
segs.append(("Z",))
|
|
65
|
+
flip = side != "left"
|
|
66
|
+
|
|
67
|
+
def place(s):
|
|
68
|
+
if s[0] == "A": # (A, rx, ry, rot, large, sweep, px, py): mirroring flips the sweep
|
|
69
|
+
_, rx, ry, rot, large, sweep, px_, py_ = s
|
|
70
|
+
return ("A", rx, ry, rot, large, 1 - sweep if flip else sweep,
|
|
71
|
+
x + (w - px_ if flip else px_), y + py_)
|
|
72
|
+
if s[0] == "Z":
|
|
73
|
+
return s
|
|
74
|
+
return (s[0], x + (w - s[1] if flip else s[1]), y + s[2])
|
|
75
|
+
return [place(s) for s in segs]
|
|
76
|
+
|
|
77
|
+
|
|
41
78
|
class SceneBuilder:
|
|
42
79
|
def __init__(self, doc: Document, router: Router):
|
|
43
80
|
self.doc, self.router = doc, router
|
|
@@ -177,11 +214,7 @@ class SceneBuilder:
|
|
|
177
214
|
paint["opacity"] = st["opacity"]
|
|
178
215
|
if st.get("shadow") == "soft":
|
|
179
216
|
paint["filter"] = "shadow"
|
|
180
|
-
r =
|
|
181
|
-
if shape == "round":
|
|
182
|
-
r = max(r, 10)
|
|
183
|
-
if shape == "pill":
|
|
184
|
-
r = h / 2
|
|
217
|
+
r = corner_radius(st, shape, w, h)
|
|
185
218
|
if shape in ("box", "round", "pill", "note", "square"):
|
|
186
219
|
el = Elem("rect", geom={"x": x, "y": y, "w": w, "h": h, "rx": r}, paint=paint)
|
|
187
220
|
elif shape in ("circle", "ellipse"):
|
|
@@ -249,14 +282,14 @@ class SceneBuilder:
|
|
|
249
282
|
return out
|
|
250
283
|
|
|
251
284
|
def accent(self, it, st) -> list[Elem]:
|
|
252
|
-
"""`accent-bar: 4px #22c55e
|
|
285
|
+
"""`accent-bar: 4px #22c55e`: a strip along one side (`accent-side`), clipped to the
|
|
286
|
+
box's own rounded corners so it never pokes outside them."""
|
|
253
287
|
a = accent(st)
|
|
254
288
|
if not a:
|
|
255
289
|
return []
|
|
256
290
|
bw, color, side = a
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return [Elem("rect", cls="pdg-accent", geom={"x": x, "y": it.ay, "w": bw, "h": it.h, "rx": r},
|
|
291
|
+
r = min(corner_radius(st, it.attrs.get("shape", "box"), it.w, it.h), it.h / 2)
|
|
292
|
+
return [Elem("path", cls="pdg-accent", geom={"segs": accent_segs(it.ax, it.ay, it.w, it.h, bw, r, side)},
|
|
260
293
|
paint={"fill": color or st.get("stroke", "#22c55e")})]
|
|
261
294
|
|
|
262
295
|
def group(self, g: Group) -> Elem:
|
|
@@ -278,7 +311,7 @@ class SceneBuilder:
|
|
|
278
311
|
def _title_band(self, g: Group, st: dict) -> Elem:
|
|
279
312
|
"""`title-fill`: a band behind the title, rounded like the group's top corners."""
|
|
280
313
|
bh = box4(st.get("padding"), 14)[0] + g.label_h - 3
|
|
281
|
-
r =
|
|
314
|
+
r = min(corner_radius(st, g.attrs.get("shape", "box"), g.w, g.h), bh)
|
|
282
315
|
return Elem("path", cls="pdg-title-band", paint={"fill": st["title-fill"]}, geom={"segs": [
|
|
283
316
|
("M", g.ax, g.ay + bh), ("L", g.ax, g.ay + r), ("A", r, r, 0, 0, 1, g.ax + r, g.ay),
|
|
284
317
|
("L", g.ax + g.w - r, g.ay), ("A", r, r, 0, 0, 1, g.ax + g.w, g.ay + r),
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 The predictygraph Authors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
"""align=stretch tiers, same-side C-shaped edges, theme css."""
|
|
6
|
+
from predictygraph import build_scene
|
|
7
|
+
from predictygraph.parser import parse
|
|
8
|
+
from predictygraph.theme import theme_css
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_stretch_makes_tiers_equal_width():
|
|
12
|
+
doc = parse("graph layout=column align=stretch\ngroup a {\n x\n}\ngroup b {\n p\n q\n r\n}\n")
|
|
13
|
+
build_scene(doc)
|
|
14
|
+
assert doc.items["a"].w == doc.items["b"].w
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_same_side_edge_goes_around_boxes_between():
|
|
18
|
+
src = ("graph layout=column\nlong \"a fairly wide box\"\nmid \"an even wider box in the middle\"\n"
|
|
19
|
+
"low \"low\"\nlong -> low exit=left enter=left route=ortho\n")
|
|
20
|
+
doc = parse(src)
|
|
21
|
+
build_scene(doc)
|
|
22
|
+
from predictygraph.route import Router
|
|
23
|
+
pts, _ = Router(doc).geometry(doc.edges[0])
|
|
24
|
+
assert min(x for x, _ in pts) < doc.items["mid"].ax
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_hami_theme_has_extra_css():
|
|
28
|
+
css = theme_css("hami")
|
|
29
|
+
assert "accent-bar" in css and "title-fill" in css
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_png_width_colors_and_single_frame(tmp_path):
|
|
33
|
+
import pytest
|
|
34
|
+
pytest.importorskip("cairosvg") # optional extra: predictygraph[apng]
|
|
35
|
+
pytest.importorskip("PIL")
|
|
36
|
+
from PIL import Image
|
|
37
|
+
from predictygraph.render.apng import write_apng
|
|
38
|
+
sc = build_scene(parse('a "x"\nb "y"\na -> b\n'))
|
|
39
|
+
out = write_apng(sc, tmp_path / "d.png", width=300, colors=16)
|
|
40
|
+
im = Image.open(out)
|
|
41
|
+
assert im.size[0] == 300 and getattr(im, "n_frames", 1) == 1 and im.mode == "P"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_align_x_lines_up_across_panels():
|
|
45
|
+
src = ('graph layout=column align=stretch\n'
|
|
46
|
+
'group top layout=row gap=20 {\n a "left box"\n b "short"\n}\n'
|
|
47
|
+
'group bottom layout=row gap=200 {\n c "c"\n d "a much wider box here"\n}\n')
|
|
48
|
+
plain = parse(src)
|
|
49
|
+
build_scene(plain)
|
|
50
|
+
doc = parse(src.replace('b "short"', 'b "short" align-x=d'))
|
|
51
|
+
build_scene(doc)
|
|
52
|
+
assert abs(doc.items["b"].cx - doc.items["d"].cx) < 0.5
|
|
53
|
+
assert abs(plain.items["b"].cx - plain.items["d"].cx) > 1 # it really moved
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_align_x_stays_inside_its_panel():
|
|
57
|
+
src = ('graph layout=column\n'
|
|
58
|
+
'group top layout=row gap=20 {\n a "left box"\n b "short" align-x=d\n}\n'
|
|
59
|
+
'group bottom layout=row gap=400 {\n c "c"\n d "far right"\n}\n')
|
|
60
|
+
doc = parse(src)
|
|
61
|
+
build_scene(doc)
|
|
62
|
+
b, top = doc.items["b"], doc.items["top"]
|
|
63
|
+
assert b.ax + b.w <= top.ax + top.w
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_align_x_never_overlaps_a_sibling():
|
|
67
|
+
src = ('graph layout=column\n'
|
|
68
|
+
'group top layout=row gap=10 {\n a "aaaaaaaa" align-x=z\n b "bbbbbbbb"\n}\n'
|
|
69
|
+
'group bottom layout=row {\n y "y"\n z "z"\n}\n')
|
|
70
|
+
doc = parse(src)
|
|
71
|
+
build_scene(doc)
|
|
72
|
+
a, b = doc.items["a"], doc.items["b"]
|
|
73
|
+
assert a.ax + a.w <= b.ax
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_accent_bar_stays_inside_rounded_corners():
|
|
77
|
+
from predictygraph.scene import accent_segs
|
|
78
|
+
segs = accent_segs(0, 0, 100, 40, 4, 10, "left")
|
|
79
|
+
xs = [s[-2] for s in segs if s[0] != "Z"]
|
|
80
|
+
ys = [s[-1] for s in segs if s[0] != "Z"]
|
|
81
|
+
assert min(xs) >= 0 and max(xs) <= 4
|
|
82
|
+
assert min(ys) > 0 and max(ys) < 40 # starts below the top edge: it follows the arc
|
|
83
|
+
right = accent_segs(0, 0, 100, 40, 4, 10, "right")
|
|
84
|
+
assert min(s[-2] for s in right if s[0] != "Z") >= 96
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_align_x_keeps_gaps_by_moving_neighbours():
|
|
88
|
+
src = ('graph layout=column align=stretch\n'
|
|
89
|
+
'group top layout=row gap=300 {\n p "p"\n q "q"\n}\n'
|
|
90
|
+
'group bottom layout=row gap=30 {\n a "aaaa" align-x=q\n b "bbbb"\n}\n'
|
|
91
|
+
'a -> b "a fairly long label"\n')
|
|
92
|
+
plain = parse(src.replace(" align-x=q", ""))
|
|
93
|
+
build_scene(plain)
|
|
94
|
+
doc = parse(src)
|
|
95
|
+
build_scene(doc)
|
|
96
|
+
gap = lambda d: d.items["b"].ax - (d.items["a"].ax + d.items["a"].w)
|
|
97
|
+
assert abs(gap(doc) - gap(plain)) < 0.01 # b moved with a: the label still fits
|
|
98
|
+
assert doc.items["a"].ax > plain.items["a"].ax
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2026 The predictygraph Authors
|
|
2
|
-
#
|
|
3
|
-
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
-
|
|
5
|
-
"""align=stretch tiers, same-side C-shaped edges, theme css."""
|
|
6
|
-
from predictygraph import build_scene
|
|
7
|
-
from predictygraph.parser import parse
|
|
8
|
-
from predictygraph.theme import theme_css
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_stretch_makes_tiers_equal_width():
|
|
12
|
-
doc = parse("graph layout=column align=stretch\ngroup a {\n x\n}\ngroup b {\n p\n q\n r\n}\n")
|
|
13
|
-
build_scene(doc)
|
|
14
|
-
assert doc.items["a"].w == doc.items["b"].w
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_same_side_edge_goes_around_boxes_between():
|
|
18
|
-
src = ("graph layout=column\nlong \"a fairly wide box\"\nmid \"an even wider box in the middle\"\n"
|
|
19
|
-
"low \"low\"\nlong -> low exit=left enter=left route=ortho\n")
|
|
20
|
-
doc = parse(src)
|
|
21
|
-
build_scene(doc)
|
|
22
|
-
from predictygraph.route import Router
|
|
23
|
-
pts, _ = Router(doc).geometry(doc.edges[0])
|
|
24
|
-
assert min(x for x, _ in pts) < doc.items["mid"].ax
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_hami_theme_has_extra_css():
|
|
28
|
-
css = theme_css("hami")
|
|
29
|
-
assert "accent-bar" in css and "title-fill" in css
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def test_png_width_colors_and_single_frame(tmp_path):
|
|
33
|
-
import pytest
|
|
34
|
-
pytest.importorskip("cairosvg") # optional extra: predictygraph[apng]
|
|
35
|
-
pytest.importorskip("PIL")
|
|
36
|
-
from PIL import Image
|
|
37
|
-
from predictygraph.render.apng import write_apng
|
|
38
|
-
sc = build_scene(parse('a "x"\nb "y"\na -> b\n'))
|
|
39
|
-
out = write_apng(sc, tmp_path / "d.png", width=300, colors=16)
|
|
40
|
-
im = Image.open(out)
|
|
41
|
-
assert im.size[0] == 300 and getattr(im, "n_frames", 1) == 1 and im.mode == "P"
|
|
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.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts/LiberationMono-Regular.ttf
RENAMED
|
File without changes
|
|
File without changes
|
{predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts/LiberationSans-Regular.ttf
RENAMED
|
File without changes
|
{predictygraph-0.2.2 → predictygraph-0.2.4}/src/predictygraph/fonts/LiberationSerif-Bold.ttf
RENAMED
|
File without changes
|
{predictygraph-0.2.2 → predictygraph-0.2.4}/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.2.2 → predictygraph-0.2.4}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|