codefig 0.17.0__py3-none-any.whl
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.
- codefig/__init__.py +210 -0
- codefig/_validate.py +12 -0
- codefig/autowrap.py +277 -0
- codefig/charts.py +1806 -0
- codefig/code.py +602 -0
- codefig/compose.py +374 -0
- codefig/constraints.py +1571 -0
- codefig/dim.py +214 -0
- codefig/edge_align.py +231 -0
- codefig/edges.py +2824 -0
- codefig/export.py +204 -0
- codefig/figure.py +1923 -0
- codefig/fills.py +146 -0
- codefig/flows.py +102 -0
- codefig/glyphs.py +395 -0
- codefig/icon.py +288 -0
- codefig/icons/LICENSE-tabler-icons.txt +21 -0
- codefig/icons/__init__.py +101 -0
- codefig/icons/tabler.json +1 -0
- codefig/legend.py +483 -0
- codefig/lint.py +1737 -0
- codefig/lint_geom.py +528 -0
- codefig/node.py +864 -0
- codefig/notices.py +34 -0
- codefig/overlaps.py +392 -0
- codefig/policy.py +137 -0
- codefig/primitives.py +2721 -0
- codefig/py.typed +0 -0
- codefig/regions.py +727 -0
- codefig/render_svg.py +512 -0
- codefig/routing.py +932 -0
- codefig/shapes.py +642 -0
- codefig/solids.py +514 -0
- codefig/steps.py +203 -0
- codefig/styles.py +893 -0
- codefig/textutil.py +889 -0
- codefig/timeline.py +1288 -0
- codefig/tree.py +277 -0
- codefig/venues.py +93 -0
- codefig-0.17.0.dist-info/METADATA +542 -0
- codefig-0.17.0.dist-info/RECORD +44 -0
- codefig-0.17.0.dist-info/WHEEL +5 -0
- codefig-0.17.0.dist-info/licenses/LICENSE +21 -0
- codefig-0.17.0.dist-info/top_level.txt +1 -0
codefig/regions.py
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
"""Regions over arbitrary members (v0.7.0) — overlapping / non-contiguous groupings.
|
|
2
|
+
|
|
3
|
+
A :class:`Region` is drawn *behind* a :class:`~codefig.edges.Diagram`'s content around
|
|
4
|
+
any set of member nodes, wherever the layout put them: ``Region([prefill_cpu,
|
|
5
|
+
decode_cpu, xfer], label="KVCache pool", style="dashed")``. Unlike a
|
|
6
|
+
:class:`~codefig.primitives.Group` it does not own or move its members, so two regions
|
|
7
|
+
may share members and overlap (Mooncake / S-LoRA "pool spanning two instances").
|
|
8
|
+
|
|
9
|
+
* ``shape="rect"`` (rounded bounding box) or ``"hull"`` (rounded convex hull — hugs an
|
|
10
|
+
L-shaped or diagonal member set without swallowing non-members).
|
|
11
|
+
* ``style="filled"`` (translucent tint + thin outline, default), ``"dashed"`` /
|
|
12
|
+
``"dotted"`` (outline only), or ``"outline"``.
|
|
13
|
+
* The label is placed at the candidate spot that collides least with boxes, edges and
|
|
14
|
+
other regions' labels: a top / bottom *strip* (left, centre or right; the outline
|
|
15
|
+
grows to include it) or *on the border* (``"border-top-left"`` …, a legend-style
|
|
16
|
+
label sitting on the outline with a knockout). ``label_pos=`` forces a spot.
|
|
17
|
+
* Non-members never sit inside a region (v0.8.0, ``exclude="auto"``): the panel layout
|
|
18
|
+
first *pushes* an intruding box out of the region along its cheapest axis (relational
|
|
19
|
+
constraints, see :mod:`codefig.constraints`); if that is impossible (the box is
|
|
20
|
+
pinned between members by a Row / Grid) the outline is *carved* — an orthogonal,
|
|
21
|
+
possibly non-contiguous outline that notches around the intruder.
|
|
22
|
+
``exclude="push"`` / ``"carve"`` pick one mechanism, ``"none"`` disables both (lint
|
|
23
|
+
CF-REG-001 then warns).
|
|
24
|
+
* v0.9.0: ``exclude="auto"`` also considers *reordering* the intruder within its flex
|
|
25
|
+
container (just before / after the members' span) and keeps whichever candidate —
|
|
26
|
+
reorder or one of the four push sides — scores best on intrusions, edges through
|
|
27
|
+
boxes, edge crossings, then total edge length + footprint. ``"push"`` never
|
|
28
|
+
reorders. Reordering mutates the container's child order (see
|
|
29
|
+
``Panel._region_moves`` for what was done). Carved outlines keep the full corner
|
|
30
|
+
radius outside and use a tighter one in the notches.
|
|
31
|
+
* Nested regions (members ⊂ another region's members) get extra padding automatically
|
|
32
|
+
so their outlines never coincide.
|
|
33
|
+
|
|
34
|
+
Regions are layout-neutral: the Diagram grows its margin when a region or its label
|
|
35
|
+
sticks out of the content box. They are not obstacles for edge routing.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import math
|
|
41
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
42
|
+
|
|
43
|
+
from .node import Node
|
|
44
|
+
from .styles import themed
|
|
45
|
+
from .textutil import measure_text
|
|
46
|
+
|
|
47
|
+
Rect = Tuple[float, float, float, float]
|
|
48
|
+
Pt = Tuple[float, float]
|
|
49
|
+
|
|
50
|
+
_CORNERS = ("top-left", "top-right", "bottom-left", "bottom-right")
|
|
51
|
+
_STRIP = ("top-left", "top-right", "bottom-left", "bottom-right", "top-center", "bottom-center")
|
|
52
|
+
_SPOTS = _STRIP + tuple("border-" + c for c in _STRIP)
|
|
53
|
+
_EXCLUDE = ("auto", "push", "carve", "none")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _hull(pts: Sequence[Pt]) -> List[Pt]:
|
|
57
|
+
"""Convex hull (monotone chain), counter-clockwise in SVG coordinates."""
|
|
58
|
+
p = sorted(set((round(x, 3), round(y, 3)) for x, y in pts))
|
|
59
|
+
if len(p) <= 2:
|
|
60
|
+
return list(p)
|
|
61
|
+
|
|
62
|
+
def cross(o, a, b):
|
|
63
|
+
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
|
|
64
|
+
|
|
65
|
+
lo: List[Pt] = []
|
|
66
|
+
for q in p:
|
|
67
|
+
while len(lo) >= 2 and cross(lo[-2], lo[-1], q) <= 0:
|
|
68
|
+
lo.pop()
|
|
69
|
+
lo.append(q)
|
|
70
|
+
up: List[Pt] = []
|
|
71
|
+
for q in reversed(p):
|
|
72
|
+
while len(up) >= 2 and cross(up[-2], up[-1], q) <= 0:
|
|
73
|
+
up.pop()
|
|
74
|
+
up.append(q)
|
|
75
|
+
return lo[:-1] + up[:-1]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _signed_area(poly: Sequence[Pt]) -> float:
|
|
79
|
+
return 0.5 * sum(poly[i - 1][0] * poly[i][1] - poly[i][0] * poly[i - 1][1]
|
|
80
|
+
for i in range(len(poly)))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _rounded_path(poly: Sequence[Pt], r: float, r_concave: Optional[float] = None) -> str:
|
|
84
|
+
"""Closed path through ``poly`` with corners rounded by up to ``r``.
|
|
85
|
+
|
|
86
|
+
``r_concave`` (v0.9.0) caps the radius of reflex (inward) corners — the notches
|
|
87
|
+
of a carved outline — so outer corners keep the full radius while a notch hugs
|
|
88
|
+
the excluded box without cutting into its clearance."""
|
|
89
|
+
n = len(poly)
|
|
90
|
+
if n < 3:
|
|
91
|
+
return ""
|
|
92
|
+
orient = 1.0 if _signed_area(poly) >= 0 else -1.0
|
|
93
|
+
out = []
|
|
94
|
+
for i in range(n):
|
|
95
|
+
p0, p1, p2 = poly[i - 1], poly[i], poly[(i + 1) % n]
|
|
96
|
+
v1 = (p0[0] - p1[0], p0[1] - p1[1])
|
|
97
|
+
v2 = (p2[0] - p1[0], p2[1] - p1[1])
|
|
98
|
+
l1, l2 = math.hypot(*v1), math.hypot(*v2)
|
|
99
|
+
rc = r
|
|
100
|
+
if r_concave is not None:
|
|
101
|
+
turn = (p1[0] - p0[0]) * (p2[1] - p1[1]) - (p1[1] - p0[1]) * (p2[0] - p1[0])
|
|
102
|
+
if turn * orient < 0:
|
|
103
|
+
rc = min(r, r_concave)
|
|
104
|
+
rr = min(rc, l1 / 2, l2 / 2)
|
|
105
|
+
a = (p1[0] + v1[0] / max(l1, 1e-9) * rr, p1[1] + v1[1] / max(l1, 1e-9) * rr)
|
|
106
|
+
b = (p1[0] + v2[0] / max(l2, 1e-9) * rr, p1[1] + v2[1] / max(l2, 1e-9) * rr)
|
|
107
|
+
out.append(("L" if out else "M") + f" {a[0]:.2f},{a[1]:.2f}")
|
|
108
|
+
out.append(f"Q {p1[0]:.2f},{p1[1]:.2f} {b[0]:.2f},{b[1]:.2f}")
|
|
109
|
+
return " ".join(out) + " Z"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _overlap(a: Rect, b: Rect) -> float:
|
|
113
|
+
w = min(a[0] + a[2], b[0] + b[2]) - max(a[0], b[0])
|
|
114
|
+
h = min(a[1] + a[3], b[1] + b[3]) - max(a[1], b[1])
|
|
115
|
+
return w * h if w > 0 and h > 0 else 0.0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _seg_hits(r: Rect, p: Pt, q: Pt) -> bool:
|
|
119
|
+
x, y, w, h = r
|
|
120
|
+
if max(p[0], q[0]) < x or min(p[0], q[0]) > x + w or max(p[1], q[1]) < y or min(p[1], q[1]) > y + h:
|
|
121
|
+
return False
|
|
122
|
+
if abs(p[0] - q[0]) < 1e-6 or abs(p[1] - q[1]) < 1e-6:
|
|
123
|
+
return True # axis-aligned segment whose bbox meets the rect
|
|
124
|
+
for t in (i / 12 for i in range(13)):
|
|
125
|
+
sx, sy = p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t
|
|
126
|
+
if x <= sx <= x + w and y <= sy <= y + h:
|
|
127
|
+
return True
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _pip(x: float, y: float, poly: Sequence[Pt]) -> bool:
|
|
132
|
+
"""Point in polygon (even-odd ray cast)."""
|
|
133
|
+
inside = False
|
|
134
|
+
n = len(poly)
|
|
135
|
+
for i in range(n):
|
|
136
|
+
ax, ay = poly[i]
|
|
137
|
+
bx, by = poly[(i + 1) % n]
|
|
138
|
+
if (ay > y) != (by > y):
|
|
139
|
+
xc = ax + (y - ay) * (bx - ax) / (by - ay)
|
|
140
|
+
if xc > x:
|
|
141
|
+
inside = not inside
|
|
142
|
+
return inside
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _poly_rect_area(poly: Sequence[Pt], r: Rect) -> float:
|
|
146
|
+
"""Area of a convex polygon ∩ rect (Sutherland–Hodgman clip + shoelace)."""
|
|
147
|
+
if len(poly) < 3:
|
|
148
|
+
return 0.0
|
|
149
|
+
x0, y0, x1, y1 = r[0], r[1], r[0] + r[2], r[1] + r[3]
|
|
150
|
+
pts = list(poly)
|
|
151
|
+
for inside, inter in (
|
|
152
|
+
(lambda p: p[0] >= x0, lambda p, q: (x0, p[1] + (q[1] - p[1]) * (x0 - p[0]) / (q[0] - p[0]))),
|
|
153
|
+
(lambda p: p[0] <= x1, lambda p, q: (x1, p[1] + (q[1] - p[1]) * (x1 - p[0]) / (q[0] - p[0]))),
|
|
154
|
+
(lambda p: p[1] >= y0, lambda p, q: (p[0] + (q[0] - p[0]) * (y0 - p[1]) / (q[1] - p[1]), y0)),
|
|
155
|
+
(lambda p: p[1] <= y1, lambda p, q: (p[0] + (q[0] - p[0]) * (y1 - p[1]) / (q[1] - p[1]), y1)),
|
|
156
|
+
):
|
|
157
|
+
out: List[Pt] = []
|
|
158
|
+
for i in range(len(pts)):
|
|
159
|
+
p, q = pts[i - 1], pts[i]
|
|
160
|
+
if inside(q):
|
|
161
|
+
if not inside(p):
|
|
162
|
+
out.append(inter(p, q))
|
|
163
|
+
out.append(q)
|
|
164
|
+
elif inside(p):
|
|
165
|
+
out.append(inter(p, q))
|
|
166
|
+
pts = out
|
|
167
|
+
if not pts:
|
|
168
|
+
return 0.0
|
|
169
|
+
a = 0.0
|
|
170
|
+
for i in range(len(pts)):
|
|
171
|
+
a += pts[i - 1][0] * pts[i][1] - pts[i][0] * pts[i - 1][1]
|
|
172
|
+
return abs(a) / 2.0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _in_any(x: float, y: float, rects: Sequence[Rect]) -> bool:
|
|
176
|
+
return any(r[0] <= x <= r[0] + r[2] and r[1] <= y <= r[1] + r[3] for r in rects)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _open_cut(c: Rect, bb: Tuple[float, float, float, float], keep: Sequence[Rect]) -> Rect:
|
|
180
|
+
"""Extend an interior cut to the nearest outline side that no kept rect blocks, so an
|
|
181
|
+
intruder becomes a notch (or splits the region) instead of a framed hole."""
|
|
182
|
+
x, y, w, h = c
|
|
183
|
+
bx0, by0, bx1, by1 = bb
|
|
184
|
+
e = 1.0
|
|
185
|
+
opts = [(x - bx0, (bx0 - e, y, x + w - bx0 + e, h)),
|
|
186
|
+
(bx1 - x - w, (x, y, bx1 - x + e, h)),
|
|
187
|
+
(y - by0, (x, by0 - e, w, y + h - by0 + e)),
|
|
188
|
+
(by1 - y - h, (x, y, w, by1 - y + e))]
|
|
189
|
+
if min(d for d, _ in opts) <= 0:
|
|
190
|
+
return c # already reaches the outline
|
|
191
|
+
for _d, r in sorted(opts, key=lambda t: t[0]):
|
|
192
|
+
if not any(_overlap(r, k) > 0.0 for k in keep):
|
|
193
|
+
return r
|
|
194
|
+
return c
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _orth_carve(base: Sequence[Rect], keep: Sequence[Rect], cut: Sequence[Rect],
|
|
198
|
+
bridge: bool = False) -> List[List[Pt]]:
|
|
199
|
+
"""Orthogonal outline(s) of (∪base − ∪cut) ∪ ∪keep, as closed loops (holes included).
|
|
200
|
+
|
|
201
|
+
``bridge=True`` closes the area orthogonally (a cell between two inside cells of the
|
|
202
|
+
same row / column is inside unless cut) — the rectilinear analogue of a convex hull.
|
|
203
|
+
"""
|
|
204
|
+
xs = sorted({v for r in list(base) + list(keep) + list(cut) for v in (r[0], r[0] + r[2])})
|
|
205
|
+
ys = sorted({v for r in list(base) + list(keep) + list(cut) for v in (r[1], r[1] + r[3])})
|
|
206
|
+
nx, ny = len(xs) - 1, len(ys) - 1
|
|
207
|
+
if nx < 1 or ny < 1:
|
|
208
|
+
return []
|
|
209
|
+
cx = [(xs[i] + xs[i + 1]) / 2 for i in range(nx)]
|
|
210
|
+
cy = [(ys[j] + ys[j + 1]) / 2 for j in range(ny)]
|
|
211
|
+
kept = [[_in_any(cx[i], cy[j], keep) for j in range(ny)] for i in range(nx)]
|
|
212
|
+
cutc = [[_in_any(cx[i], cy[j], cut) and not kept[i][j] for j in range(ny)] for i in range(nx)]
|
|
213
|
+
ins = [[(kept[i][j] or (_in_any(cx[i], cy[j], base) and not cutc[i][j]))
|
|
214
|
+
for j in range(ny)] for i in range(nx)]
|
|
215
|
+
if bridge:
|
|
216
|
+
for _ in range(4):
|
|
217
|
+
changed = False
|
|
218
|
+
for j in range(ny):
|
|
219
|
+
idx = [i for i in range(nx) if ins[i][j]]
|
|
220
|
+
if idx:
|
|
221
|
+
for i in range(idx[0], idx[-1] + 1):
|
|
222
|
+
if not ins[i][j] and not cutc[i][j]:
|
|
223
|
+
ins[i][j] = changed = True
|
|
224
|
+
for i in range(nx):
|
|
225
|
+
idx = [j for j in range(ny) if ins[i][j]]
|
|
226
|
+
if idx:
|
|
227
|
+
for j in range(idx[0], idx[-1] + 1):
|
|
228
|
+
if not ins[i][j] and not cutc[i][j]:
|
|
229
|
+
ins[i][j] = changed = True
|
|
230
|
+
if not changed:
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
def inside(i: int, j: int) -> bool:
|
|
234
|
+
return 0 <= i < nx and 0 <= j < ny and ins[i][j]
|
|
235
|
+
|
|
236
|
+
out_edges: Dict[Tuple[int, int], List[Tuple[int, int]]] = {}
|
|
237
|
+
|
|
238
|
+
def add(a, b):
|
|
239
|
+
out_edges.setdefault(a, []).append(b)
|
|
240
|
+
|
|
241
|
+
for i in range(nx):
|
|
242
|
+
for j in range(ny):
|
|
243
|
+
if not ins[i][j]:
|
|
244
|
+
continue
|
|
245
|
+
if not inside(i, j - 1):
|
|
246
|
+
add((i, j), (i + 1, j))
|
|
247
|
+
if not inside(i + 1, j):
|
|
248
|
+
add((i + 1, j), (i + 1, j + 1))
|
|
249
|
+
if not inside(i, j + 1):
|
|
250
|
+
add((i + 1, j + 1), (i, j + 1))
|
|
251
|
+
if not inside(i - 1, j):
|
|
252
|
+
add((i, j + 1), (i, j))
|
|
253
|
+
loops: List[List[Pt]] = []
|
|
254
|
+
while out_edges:
|
|
255
|
+
start = next(iter(out_edges))
|
|
256
|
+
loop = [start]
|
|
257
|
+
cur = start
|
|
258
|
+
prev_dir = None
|
|
259
|
+
while True:
|
|
260
|
+
nxt_list = out_edges.get(cur)
|
|
261
|
+
if not nxt_list:
|
|
262
|
+
break
|
|
263
|
+
if len(nxt_list) > 1 and prev_dir is not None:
|
|
264
|
+
# saddle: take the right turn (keeps diagonal cells apart)
|
|
265
|
+
def turn(b):
|
|
266
|
+
d = (b[0] - cur[0], b[1] - cur[1])
|
|
267
|
+
return -(prev_dir[0] * d[1] - prev_dir[1] * d[0])
|
|
268
|
+
nxt_list.sort(key=turn)
|
|
269
|
+
nxt = nxt_list.pop(0)
|
|
270
|
+
if not nxt_list:
|
|
271
|
+
del out_edges[cur]
|
|
272
|
+
prev_dir = (nxt[0] - cur[0], nxt[1] - cur[1])
|
|
273
|
+
cur = nxt
|
|
274
|
+
if cur == start:
|
|
275
|
+
break
|
|
276
|
+
loop.append(cur)
|
|
277
|
+
pts = [(xs[i], ys[j]) for i, j in loop]
|
|
278
|
+
# drop collinear vertices
|
|
279
|
+
simp: List[Pt] = []
|
|
280
|
+
n = len(pts)
|
|
281
|
+
for k in range(n):
|
|
282
|
+
a, b, c = pts[k - 1], pts[k], pts[(k + 1) % n]
|
|
283
|
+
if abs((b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0])) > 1e-9:
|
|
284
|
+
simp.append(b)
|
|
285
|
+
if len(simp) >= 4:
|
|
286
|
+
loops.append(simp)
|
|
287
|
+
# outer loops (largest |area|) first
|
|
288
|
+
def area(lp):
|
|
289
|
+
return abs(sum(lp[k - 1][0] * lp[k][1] - lp[k][0] * lp[k - 1][1] for k in range(len(lp)))) / 2
|
|
290
|
+
loops.sort(key=lambda lp: -area(lp))
|
|
291
|
+
return loops
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class Region:
|
|
295
|
+
"""Translucent / dashed area around arbitrary member nodes (see module docs).
|
|
296
|
+
|
|
297
|
+
members: nodes (or anything with ``.geom``) inside one Diagram.
|
|
298
|
+
label: text in a strip or on the border (``label_pos`` auto | top-left | top-center |
|
|
299
|
+
top-right | bottom-… | border-top-left | …); ``label_color`` defaults to the region colour.
|
|
300
|
+
exclude: auto | push | carve | none — how non-members are kept out (module docs).
|
|
301
|
+
color: outline / tint colour (default: the theme's accents, cycled per region).
|
|
302
|
+
style: ``"filled"`` (tint + thin outline, default) | ``"dashed"`` | ``"dotted"`` |
|
|
303
|
+
``"outline"`` (the last three: outline only, no tint).
|
|
304
|
+
fill_opacity: tint strength for ``style="filled"`` (default 0.10).
|
|
305
|
+
pad: clearance around members (theme length); ``radius``: corner rounding.
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
reg_pad = themed(7.0)
|
|
309
|
+
reg_nest = themed(6.0)
|
|
310
|
+
|
|
311
|
+
def __init__(self, members: Sequence[Any], label: Optional[str] = None, *,
|
|
312
|
+
shape: str = "rect", style: str = "filled", color: Optional[str] = None,
|
|
313
|
+
fill_opacity: float = 0.10, pad: Optional[float] = None,
|
|
314
|
+
radius: Optional[float] = None, label_pos: str = "auto",
|
|
315
|
+
label_color: Optional[str] = None, stroke_width: Optional[float] = None,
|
|
316
|
+
exclude: str = "auto", theme: Any = None) -> None:
|
|
317
|
+
if not members:
|
|
318
|
+
raise ValueError("Region needs at least one member")
|
|
319
|
+
from ._validate import check_enum
|
|
320
|
+
shape = check_enum("Region(shape)", shape, ("rect", "hull"))
|
|
321
|
+
style = check_enum("Region(style)", style, ("filled", "dashed", "dotted", "outline"))
|
|
322
|
+
label_pos = check_enum("Region(label_pos)", label_pos, ("auto",) + _SPOTS,
|
|
323
|
+
{"top": "top-center", "bottom": "bottom-center",
|
|
324
|
+
"left": "top-left", "right": "top-right"})
|
|
325
|
+
exclude = check_enum("Region(exclude)", exclude, _EXCLUDE)
|
|
326
|
+
self.exclude = exclude
|
|
327
|
+
self.members = list(members)
|
|
328
|
+
self.label = label
|
|
329
|
+
self.shape = shape
|
|
330
|
+
self.style = style
|
|
331
|
+
self.color = color
|
|
332
|
+
self.fill_opacity = float(fill_opacity)
|
|
333
|
+
self.reg_pad = pad
|
|
334
|
+
self.radius = radius
|
|
335
|
+
self.label_pos = label_pos
|
|
336
|
+
self.label_color = label_color
|
|
337
|
+
self.stroke_width = stroke_width
|
|
338
|
+
self._theme = theme
|
|
339
|
+
self._extra_pad = 0.0
|
|
340
|
+
self._geo: Optional[Dict[str, Any]] = None
|
|
341
|
+
|
|
342
|
+
# themed() looks for .theme
|
|
343
|
+
@property
|
|
344
|
+
def theme(self):
|
|
345
|
+
from .styles import current_theme
|
|
346
|
+
|
|
347
|
+
if self._theme is not None:
|
|
348
|
+
return self._theme
|
|
349
|
+
for m in self.members:
|
|
350
|
+
th = getattr(m, "theme", None)
|
|
351
|
+
if th is not None:
|
|
352
|
+
return th
|
|
353
|
+
return current_theme()
|
|
354
|
+
|
|
355
|
+
def _rects(self) -> List[Rect]:
|
|
356
|
+
out = []
|
|
357
|
+
for m in self.members:
|
|
358
|
+
g = getattr(m, "geom", None)
|
|
359
|
+
if g is not None and g.w > 0 and g.h > 0:
|
|
360
|
+
out.append((g.x, g.y, g.w, g.h))
|
|
361
|
+
return out
|
|
362
|
+
|
|
363
|
+
def _label_size(self) -> Tuple[float, float]:
|
|
364
|
+
if not self.label:
|
|
365
|
+
return 0.0, 0.0
|
|
366
|
+
th = self.theme
|
|
367
|
+
return measure_text(self.label, th.small_size, weight="bold")
|
|
368
|
+
|
|
369
|
+
def _member_ids(self) -> set:
|
|
370
|
+
ids = set()
|
|
371
|
+
|
|
372
|
+
def add(n):
|
|
373
|
+
ids.add(id(n))
|
|
374
|
+
for k in getattr(n, "children", lambda: ())():
|
|
375
|
+
add(k)
|
|
376
|
+
|
|
377
|
+
for m in self.members:
|
|
378
|
+
add(m)
|
|
379
|
+
return ids
|
|
380
|
+
|
|
381
|
+
# ---- geometry ---------------------------------------------------------------
|
|
382
|
+
def _pad(self) -> float:
|
|
383
|
+
return float(self.reg_pad) + self._extra_pad
|
|
384
|
+
|
|
385
|
+
def base_rect(self) -> Rect:
|
|
386
|
+
"""Padded bounding box of the members (no label) in layout coordinates."""
|
|
387
|
+
rects = self._rects()
|
|
388
|
+
pad = self._pad()
|
|
389
|
+
x0 = min(r[0] for r in rects) - pad
|
|
390
|
+
y0 = min(r[1] for r in rects) - pad
|
|
391
|
+
x1 = max(r[0] + r[2] for r in rects) + pad
|
|
392
|
+
y1 = max(r[1] + r[3] for r in rects) + pad
|
|
393
|
+
return (x0, y0, x1 - x0, y1 - y0)
|
|
394
|
+
|
|
395
|
+
def _hull_poly(self, extra: Sequence[Rect] = ()) -> List[Pt]:
|
|
396
|
+
pad = self._pad()
|
|
397
|
+
pts: List[Pt] = []
|
|
398
|
+
for r in self._rects():
|
|
399
|
+
pts += [(r[0] - pad, r[1] - pad), (r[0] + r[2] + pad, r[1] - pad),
|
|
400
|
+
(r[0] + r[2] + pad, r[1] + r[3] + pad), (r[0] - pad, r[1] + r[3] + pad)]
|
|
401
|
+
for r in extra:
|
|
402
|
+
pts += [(r[0], r[1]), (r[0] + r[2], r[1]), (r[0] + r[2], r[1] + r[3]),
|
|
403
|
+
(r[0], r[1] + r[3])]
|
|
404
|
+
return _hull(pts)
|
|
405
|
+
|
|
406
|
+
def intrudes(self, rect: Rect, *, min_area: float = 0.5) -> bool:
|
|
407
|
+
"""Would ``rect`` sit inside this region's *un-carved* area (padded bbox / hull)?"""
|
|
408
|
+
if self.shape == "hull":
|
|
409
|
+
return _poly_rect_area(self._hull_poly(), rect) > min_area
|
|
410
|
+
return _overlap(self.base_rect(), rect) > min_area
|
|
411
|
+
|
|
412
|
+
def solve(self, boxes: Sequence[Tuple[Any, Rect]], segs: Sequence[Tuple[Pt, Pt]],
|
|
413
|
+
taken: Sequence[Rect]) -> Dict[str, Any]:
|
|
414
|
+
"""Pick the label spot and outline. ``boxes``: (node, rect) of drawn non-ancestor nodes."""
|
|
415
|
+
th = self.theme
|
|
416
|
+
rects = self._rects()
|
|
417
|
+
x0, y0, bw, bh = self.base_rect()
|
|
418
|
+
x1, y1 = x0 + bw, y0 + bh
|
|
419
|
+
lw, lh = self._label_size()
|
|
420
|
+
mine = self._member_ids()
|
|
421
|
+
others = [r for n, r in boxes if id(n) not in mine]
|
|
422
|
+
lp = th.L(3.0)
|
|
423
|
+
best = None
|
|
424
|
+
cands = _SPOTS if self.label_pos == "auto" else (self.label_pos,)
|
|
425
|
+
for k, c in enumerate(cands):
|
|
426
|
+
if not self.label:
|
|
427
|
+
break
|
|
428
|
+
border = c.startswith("border-")
|
|
429
|
+
cc = c[7:] if border else c
|
|
430
|
+
if cc.endswith("left"):
|
|
431
|
+
lx = x0 + lp + (th.L(3.0) if border else 0.0)
|
|
432
|
+
elif cc.endswith("right"):
|
|
433
|
+
lx = x1 - lp - lw - (th.L(3.0) if border else 0.0)
|
|
434
|
+
else:
|
|
435
|
+
lx = (x0 + x1) / 2 - lw / 2
|
|
436
|
+
top = cc.startswith("top")
|
|
437
|
+
if border:
|
|
438
|
+
ly = (y0 if top else y1) - lh / 2
|
|
439
|
+
ext = (min(x0, lx - lp), y0, max(x1, lx + lw + lp), y1)
|
|
440
|
+
strip = (lx - lp * 0.5, ly, lw + lp, lh)
|
|
441
|
+
else:
|
|
442
|
+
ly = y0 - lh - th.L(1.0) if top else y1 + th.L(1.0)
|
|
443
|
+
ext = (min(x0, lx - lp), min(y0, ly - lp * 0.6), max(x1, lx + lw + lp),
|
|
444
|
+
max(y1, ly + lh + lp * 0.6))
|
|
445
|
+
# the label pulls the outline outwards: what does that strip newly cover?
|
|
446
|
+
strip = (ext[0], ly - lp * 0.6, ext[2] - ext[0], lh + lp * 1.2)
|
|
447
|
+
lab = (lx, ly, lw, lh)
|
|
448
|
+
score = sum(_overlap(lab, r) for r in others) * 4.0
|
|
449
|
+
score += sum(_overlap(strip, r) for r in others)
|
|
450
|
+
score += sum(_overlap(lab, r) for r in taken) * 4.0
|
|
451
|
+
score += sum(1 for p, q in segs if _seg_hits(lab, p, q)) * lh * lw * 0.5
|
|
452
|
+
# mild preferences: strip before border, corners before centres, stable order
|
|
453
|
+
score += (lh * lw * 0.02 if border else 0.0) + k * 1e-3
|
|
454
|
+
if best is None or score < best[0]:
|
|
455
|
+
best = (score, c, lab, ext, border)
|
|
456
|
+
if best is not None:
|
|
457
|
+
_s, corner, lab, ext, border = best
|
|
458
|
+
ex0, ey0, ex1, ey1 = ext
|
|
459
|
+
else:
|
|
460
|
+
corner, lab, border = None, None, False
|
|
461
|
+
ex0, ey0, ex1, ey1 = x0, y0, x1, y1
|
|
462
|
+
lab_box = None
|
|
463
|
+
if lab is not None and not border:
|
|
464
|
+
lx, ly, lw_, lh_ = lab
|
|
465
|
+
lab_box = (lx - lp, ly - lp * 0.6, lw_ + 2 * lp, lh_ + lp * 1.2)
|
|
466
|
+
# non-members still inside the outline (push failed / exclude="carve")
|
|
467
|
+
if self.shape == "hull":
|
|
468
|
+
hull = self._hull_poly([lab_box] if lab_box else [])
|
|
469
|
+
inside = [r for r in others if _poly_rect_area(hull, r) > 0.5]
|
|
470
|
+
else:
|
|
471
|
+
ext_r = (ex0, ey0, ex1 - ex0, ey1 - ey0)
|
|
472
|
+
inside = [r for r in others if _overlap(ext_r, r) > 0.5]
|
|
473
|
+
loops: List[List[Pt]]
|
|
474
|
+
carved = bool(inside) and self.exclude in ("auto", "carve")
|
|
475
|
+
if carved:
|
|
476
|
+
gap = th.L(3.0)
|
|
477
|
+
keep = [(r[0] - 0.5, r[1] - 0.5, r[2] + 1.0, r[3] + 1.0) for r in rects]
|
|
478
|
+
if lab is not None:
|
|
479
|
+
keep.append((lab[0] - 0.5, lab[1] - 0.5, lab[2] + 1.0, lab[3] + 1.0))
|
|
480
|
+
cut = [(r[0] - gap, r[1] - gap, r[2] + 2 * gap, r[3] + 2 * gap) for r in inside]
|
|
481
|
+
if self.shape == "hull":
|
|
482
|
+
bb = (min(r[0] for r in rects) - self._pad(), min(r[1] for r in rects) - self._pad(),
|
|
483
|
+
max(r[0] + r[2] for r in rects) + self._pad(),
|
|
484
|
+
max(r[1] + r[3] for r in rects) + self._pad())
|
|
485
|
+
else:
|
|
486
|
+
bb = (ex0, ey0, ex1, ey1)
|
|
487
|
+
cut = [_open_cut(c, bb, keep) for c in cut]
|
|
488
|
+
if self.shape == "hull":
|
|
489
|
+
pad = self._pad()
|
|
490
|
+
base = [(r[0] - pad, r[1] - pad, r[2] + 2 * pad, r[3] + 2 * pad) for r in rects]
|
|
491
|
+
if lab_box:
|
|
492
|
+
base.append(lab_box)
|
|
493
|
+
loops = _orth_carve(base, keep, cut, bridge=True)
|
|
494
|
+
else:
|
|
495
|
+
loops = _orth_carve([(ex0, ey0, ex1 - ex0, ey1 - ey0)], keep, cut)
|
|
496
|
+
elif self.shape == "hull":
|
|
497
|
+
loops = [hull]
|
|
498
|
+
else:
|
|
499
|
+
loops = [[(ex0, ey0), (ex1, ey0), (ex1, ey1), (ex0, ey1)]]
|
|
500
|
+
xs = [p[0] for lp_ in loops for p in lp_]
|
|
501
|
+
ys = [p[1] for lp_ in loops for p in lp_]
|
|
502
|
+
self._geo = {"poly": loops[0], "loops": loops, "label": lab, "corner": corner,
|
|
503
|
+
"border": border, "carved": carved,
|
|
504
|
+
"bbox": (min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys))}
|
|
505
|
+
return self._geo
|
|
506
|
+
|
|
507
|
+
def covers(self, rect: Rect, frac: float = 0.15) -> bool:
|
|
508
|
+
"""Does the *drawn* outline cover more than ``frac`` of ``rect``? (lint CF-REG-001)"""
|
|
509
|
+
if self._geo is None:
|
|
510
|
+
return False
|
|
511
|
+
x, y, w, h = rect
|
|
512
|
+
if w <= 0 or h <= 0:
|
|
513
|
+
return False
|
|
514
|
+
n = 7
|
|
515
|
+
hit = 0
|
|
516
|
+
for i in range(n):
|
|
517
|
+
for j in range(n):
|
|
518
|
+
px = x + w * (i + 0.5) / n
|
|
519
|
+
py = y + h * (j + 0.5) / n
|
|
520
|
+
inside = False
|
|
521
|
+
for loop in self._geo["loops"]:
|
|
522
|
+
if _pip(px, py, loop):
|
|
523
|
+
inside = not inside
|
|
524
|
+
hit += inside
|
|
525
|
+
return hit / (n * n) > frac
|
|
526
|
+
|
|
527
|
+
@property
|
|
528
|
+
def label_rect(self) -> Optional[Rect]:
|
|
529
|
+
"""(x, y, w, h) of the placed label after layout (None if unlabeled / not laid out)."""
|
|
530
|
+
geo = getattr(self, "_geo", None)
|
|
531
|
+
return geo["label"] if geo else None
|
|
532
|
+
|
|
533
|
+
@property
|
|
534
|
+
def bbox(self) -> Optional[Rect]:
|
|
535
|
+
"""Outline bounding box after the Diagram laid it out (None before)."""
|
|
536
|
+
return None if self._geo is None else self._geo["bbox"]
|
|
537
|
+
|
|
538
|
+
def _color(self, index: int) -> str:
|
|
539
|
+
if self.color:
|
|
540
|
+
return self.color
|
|
541
|
+
acc = list(self.theme.accents) or ["#555555"]
|
|
542
|
+
return acc[index % len(acc)]
|
|
543
|
+
|
|
544
|
+
def draw(self, buf: List[str], ox: float, oy: float, index: int = 0) -> None:
|
|
545
|
+
from .render_svg import draw_path
|
|
546
|
+
|
|
547
|
+
if self._geo is None:
|
|
548
|
+
return
|
|
549
|
+
th = self.theme
|
|
550
|
+
col = self._color(index)
|
|
551
|
+
r = self.radius if self.radius is not None else th.L(8.0)
|
|
552
|
+
loops = self._geo["loops"]
|
|
553
|
+
if self._geo.get("carved"):
|
|
554
|
+
# v0.9.0: outer corners keep the full radius; reflex notch corners and
|
|
555
|
+
# hole loops get a tighter one so they hug the excluded box
|
|
556
|
+
rn = min(r, th.L(4.0))
|
|
557
|
+
big = max(range(len(loops)), key=lambda i: abs(_signed_area(loops[i]))) if loops else 0
|
|
558
|
+
d = " ".join(_rounded_path([(x + ox, y + oy) for x, y in lp_],
|
|
559
|
+
r if i == big else rn, rn)
|
|
560
|
+
for i, lp_ in enumerate(loops))
|
|
561
|
+
else:
|
|
562
|
+
d = " ".join(_rounded_path([(x + ox, y + oy) for x, y in lp_], r) for lp_ in loops)
|
|
563
|
+
sw = self.stroke_width if self.stroke_width is not None else th.stroke_width
|
|
564
|
+
dash = None
|
|
565
|
+
if self.style == "dashed":
|
|
566
|
+
dash = " ".join(f"{v:g}" for v in th.dash) if getattr(th, "dash", None) else "4 3"
|
|
567
|
+
elif self.style == "dotted":
|
|
568
|
+
dash = " ".join(f"{v:g}" for v in th.dot) if getattr(th, "dot", None) else "1 2"
|
|
569
|
+
if self.style == "filled":
|
|
570
|
+
draw_path(buf, d, fill=col, stroke="none", stroke_width=0, opacity=self.fill_opacity,
|
|
571
|
+
fill_rule="evenodd")
|
|
572
|
+
draw_path(buf, d, fill="none", stroke=col, stroke_width=sw, dasharray=dash)
|
|
573
|
+
|
|
574
|
+
def draw_label(self, buf: List[str], ox: float, oy: float, index: int = 0) -> None:
|
|
575
|
+
from .render_svg import draw_text
|
|
576
|
+
|
|
577
|
+
if self._geo is None or self._geo["label"] is None:
|
|
578
|
+
return
|
|
579
|
+
th = self.theme
|
|
580
|
+
lx, ly, lw, lh = self._geo["label"]
|
|
581
|
+
fs = th.small_size
|
|
582
|
+
col = self.label_color or self._color(index)
|
|
583
|
+
if self._geo.get("border"): # legend-style: knock the outline out behind the text
|
|
584
|
+
from .render_svg import draw_rect
|
|
585
|
+
|
|
586
|
+
kp = th.L(1.5)
|
|
587
|
+
draw_rect(buf, lx + ox - kp, ly + oy, lw + 2 * kp, lh, fill="#ffffff",
|
|
588
|
+
stroke="none", stroke_width=0)
|
|
589
|
+
draw_text(buf, self.label, lx + ox, ly + oy + lh / 2 + fs * 0.35, font_size=fs,
|
|
590
|
+
weight="bold", fill=col)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def diagram_boxes(diagram: Any) -> Tuple[List[Tuple[Any, Rect]], Dict[int, Any]]:
|
|
594
|
+
"""(node, rect) of every drawn box inside ``diagram`` + a child-id → parent map."""
|
|
595
|
+
from .policy import role_of
|
|
596
|
+
|
|
597
|
+
parent: Dict[int, Any] = {}
|
|
598
|
+
boxes: List[Tuple[Any, Rect]] = []
|
|
599
|
+
|
|
600
|
+
def rec(n: Any) -> None:
|
|
601
|
+
kids = list(n.children())
|
|
602
|
+
for k in kids:
|
|
603
|
+
parent[id(k)] = n
|
|
604
|
+
if n is not diagram.content:
|
|
605
|
+
role = role_of(n)
|
|
606
|
+
if (role in ("hard", "soft") and not kids) or role == "hard":
|
|
607
|
+
g = n.geom
|
|
608
|
+
if g.w > 0 and g.h > 0:
|
|
609
|
+
boxes.append((n, (g.x, g.y, g.w, g.h)))
|
|
610
|
+
for k in kids:
|
|
611
|
+
rec(k)
|
|
612
|
+
|
|
613
|
+
rec(diagram.content)
|
|
614
|
+
return boxes, parent
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _ancestors(region: Region, parent: Dict[int, Any]) -> set:
|
|
618
|
+
out = set()
|
|
619
|
+
for m in region.members:
|
|
620
|
+
p = parent.get(id(m))
|
|
621
|
+
while p is not None and id(p) not in out:
|
|
622
|
+
out.add(id(p))
|
|
623
|
+
p = parent.get(id(p))
|
|
624
|
+
return out
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def region_others(diagram: Any, region: Region,
|
|
628
|
+
pre: Optional[Tuple[List[Tuple[Any, Rect]], Dict[int, Any]]] = None
|
|
629
|
+
) -> List[Tuple[Any, Rect]]:
|
|
630
|
+
"""Drawn boxes that are neither members (nor inside one) nor ancestors of members."""
|
|
631
|
+
boxes, parent = pre or diagram_boxes(diagram)
|
|
632
|
+
skip = region._member_ids() | _ancestors(region, parent)
|
|
633
|
+
return [(n, r) for n, r in boxes if id(n) not in skip]
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def region_intruders(diagram: Any, region: Region) -> List[Any]:
|
|
637
|
+
"""Non-member boxes inside the region's un-carved area (targets of the layout push)."""
|
|
638
|
+
if not region._rects():
|
|
639
|
+
return []
|
|
640
|
+
out = []
|
|
641
|
+
for n, r in region_others(diagram, region):
|
|
642
|
+
if region.intrudes(r):
|
|
643
|
+
out.append(n)
|
|
644
|
+
# keep only outermost intruders (a Group intruding implies its children do)
|
|
645
|
+
ids = {id(n) for n in out}
|
|
646
|
+
_b, parent = diagram_boxes(diagram)
|
|
647
|
+
|
|
648
|
+
def has_intruding_ancestor(n):
|
|
649
|
+
p = parent.get(id(n))
|
|
650
|
+
while p is not None:
|
|
651
|
+
if id(p) in ids:
|
|
652
|
+
return True
|
|
653
|
+
p = parent.get(id(p))
|
|
654
|
+
return False
|
|
655
|
+
|
|
656
|
+
return [n for n in out if not has_intruding_ancestor(n)]
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def push_constraints(region: Region, intruder: Any, side: str, margin: float) -> list:
|
|
660
|
+
"""no_overlap constraints that move ``intruder`` to ``side`` of all members."""
|
|
661
|
+
from .constraints import no_overlap
|
|
662
|
+
|
|
663
|
+
axis = "x" if side in ("left", "right") else "y"
|
|
664
|
+
out = []
|
|
665
|
+
for m in region.members:
|
|
666
|
+
if side in ("right", "bottom"):
|
|
667
|
+
c = no_overlap(m, intruder, axis, margin=margin, strength="strong")
|
|
668
|
+
else:
|
|
669
|
+
c = no_overlap(intruder, m, axis, margin=margin, strength="strong")
|
|
670
|
+
c.subjects, c.refs = [intruder], [m]
|
|
671
|
+
out.append(c)
|
|
672
|
+
return out
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def push_sides(region: Region, intruder: Any) -> List[Tuple[float, str]]:
|
|
676
|
+
"""Candidate push directions for ``intruder``, cheapest displacement first."""
|
|
677
|
+
x0, y0, w, h = region.base_rect()
|
|
678
|
+
g = intruder.geom
|
|
679
|
+
moves = [(x0 + w - g.x, "right"), (g.x + g.w - x0, "left"),
|
|
680
|
+
(y0 + h - g.y, "bottom"), (g.y + g.h - y0, "top")]
|
|
681
|
+
return sorted(moves)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def nest_pads(regions: Sequence[Region]) -> None:
|
|
685
|
+
"""Regions that strictly contain another region's members get extra padding."""
|
|
686
|
+
sets = [r._member_ids() for r in regions]
|
|
687
|
+
for i, r in enumerate(regions):
|
|
688
|
+
inner = sum(1 for j, s in enumerate(sets) if j != i and s < sets[i])
|
|
689
|
+
r._extra_pad = inner * float(r.reg_nest)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def layout_regions(diagram: Any, regions: Sequence[Region]) -> None:
|
|
693
|
+
"""Solve every region of ``diagram`` (nest padding, label corners)."""
|
|
694
|
+
if not regions:
|
|
695
|
+
return
|
|
696
|
+
nest_pads(regions)
|
|
697
|
+
pre = diagram_boxes(diagram)
|
|
698
|
+
segs: List[Tuple[Pt, Pt]] = []
|
|
699
|
+
try:
|
|
700
|
+
for _e, pts in diagram._routed()[0]:
|
|
701
|
+
segs += [(pts[k], pts[k + 1]) for k in range(len(pts) - 1)]
|
|
702
|
+
except Exception: # pragma: no cover - routing failures are reported elsewhere
|
|
703
|
+
pass
|
|
704
|
+
taken: List[Rect] = []
|
|
705
|
+
# outer (bigger) regions first so inner labels avoid them
|
|
706
|
+
order = sorted(regions, key=lambda rr: -len(rr._member_ids()))
|
|
707
|
+
for r in order:
|
|
708
|
+
geo = r.solve(region_others(diagram, r, pre), segs, taken)
|
|
709
|
+
if geo["label"] is not None:
|
|
710
|
+
taken.append(geo["label"])
|
|
711
|
+
if len(order) > 1 and any(r.label for r in order):
|
|
712
|
+
# v0.12: second pass — each label also avoids the *outlines* of the other
|
|
713
|
+
# regions (an overlapping region's border used to run through a label)
|
|
714
|
+
def outline(rr: Region) -> List[Tuple[Pt, Pt]]:
|
|
715
|
+
g = rr._geo or {}
|
|
716
|
+
out = []
|
|
717
|
+
for lp_ in g.get("loops", []):
|
|
718
|
+
out += [(lp_[k], lp_[(k + 1) % len(lp_)]) for k in range(len(lp_))]
|
|
719
|
+
return out
|
|
720
|
+
|
|
721
|
+
for r in order:
|
|
722
|
+
if not r.label:
|
|
723
|
+
continue
|
|
724
|
+
others_segs = [sg for o in order if o is not r for sg in outline(o)]
|
|
725
|
+
others_lab = [o._geo["label"] for o in order
|
|
726
|
+
if o is not r and o._geo and o._geo["label"] is not None]
|
|
727
|
+
r.solve(region_others(diagram, r, pre), segs + others_segs, others_lab)
|