frameplot 0.5.3__tar.gz → 0.5.5__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.
- {frameplot-0.5.3/src/frameplot.egg-info → frameplot-0.5.5}/PKG-INFO +1 -1
- {frameplot-0.5.3 → frameplot-0.5.5}/pyproject.toml +1 -1
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/order.py +0 -123
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/route.py +39 -3
- {frameplot-0.5.3 → frameplot-0.5.5/src/frameplot.egg-info}/PKG-INFO +1 -1
- {frameplot-0.5.3 → frameplot-0.5.5}/tests/test_rendering.py +35 -9
- {frameplot-0.5.3 → frameplot-0.5.5}/LICENSE +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/README.md +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/setup.cfg +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/__init__.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/api.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/__init__.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/place.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/rank.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/scc.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/text.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/types.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/validate.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/model.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/render/__init__.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/render/png.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/render/svg.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/theme.py +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot.egg-info/SOURCES.txt +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot.egg-info/dependency_links.txt +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot.egg-info/requires.txt +0 -0
- {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "frameplot"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.5"
|
|
8
8
|
description = "Turn Python-defined pipeline graphs into presentation-ready SVG and PNG diagrams."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -33,9 +33,6 @@ def order_nodes(
|
|
|
33
33
|
_resort_rank(nodes_by_rank[rank], outgoing, order, validated)
|
|
34
34
|
_refresh_order(nodes_by_rank[rank], order)
|
|
35
35
|
|
|
36
|
-
if isinstance(validated, ValidatedDetailPanel):
|
|
37
|
-
_apply_detail_panel_shared_source_lanes(validated, ranks, order)
|
|
38
|
-
|
|
39
36
|
if isinstance(validated, ValidatedPipeline) and validated.detail_panel is not None:
|
|
40
37
|
_apply_detail_panel_bias(validated, nodes_by_rank, ranks, order)
|
|
41
38
|
|
|
@@ -120,126 +117,6 @@ def _apply_detail_panel_bias(
|
|
|
120
117
|
for index, node_id in enumerate(rank_focus_nodes):
|
|
121
118
|
order[node_id] = start_row + index
|
|
122
119
|
|
|
123
|
-
|
|
124
|
-
def _apply_detail_panel_shared_source_lanes(
|
|
125
|
-
validated: ValidatedDetailPanel,
|
|
126
|
-
ranks: dict[str, int],
|
|
127
|
-
order: dict[str, int],
|
|
128
|
-
) -> None:
|
|
129
|
-
group_membership = _group_membership(validated)
|
|
130
|
-
components = _weak_components(validated)
|
|
131
|
-
incoming, outgoing = _forward_neighbors(validated, ranks)
|
|
132
|
-
|
|
133
|
-
for component_nodes in components:
|
|
134
|
-
component_set = set(component_nodes)
|
|
135
|
-
shared_sources = [
|
|
136
|
-
node_id
|
|
137
|
-
for node_id in component_nodes
|
|
138
|
-
if _is_detail_panel_shared_source(
|
|
139
|
-
node_id,
|
|
140
|
-
component_set,
|
|
141
|
-
group_membership,
|
|
142
|
-
incoming,
|
|
143
|
-
outgoing,
|
|
144
|
-
order,
|
|
145
|
-
)
|
|
146
|
-
]
|
|
147
|
-
if not shared_sources:
|
|
148
|
-
continue
|
|
149
|
-
|
|
150
|
-
shared_sources.sort(key=lambda node_id: (order[node_id], validated.node_index[node_id], node_id))
|
|
151
|
-
shared_source_ids = set(shared_sources)
|
|
152
|
-
nonshared_rows = _reorder_component_nodes(
|
|
153
|
-
validated,
|
|
154
|
-
ranks,
|
|
155
|
-
component_set - shared_source_ids,
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
for index, node_id in enumerate(shared_sources):
|
|
159
|
-
order[node_id] = index
|
|
160
|
-
for node_id, row in nonshared_rows.items():
|
|
161
|
-
order[node_id] = row + len(shared_sources)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def _is_detail_panel_shared_source(
|
|
165
|
-
node_id: str,
|
|
166
|
-
component_nodes: set[str],
|
|
167
|
-
group_membership: dict[str, tuple[str, ...]],
|
|
168
|
-
incoming: dict[str, list[str]],
|
|
169
|
-
outgoing: dict[str, list[str]],
|
|
170
|
-
order: dict[str, int],
|
|
171
|
-
) -> bool:
|
|
172
|
-
if group_membership.get(node_id):
|
|
173
|
-
return False
|
|
174
|
-
if incoming.get(node_id):
|
|
175
|
-
return False
|
|
176
|
-
|
|
177
|
-
targets = [target_id for target_id in outgoing.get(node_id, ()) if target_id in component_nodes]
|
|
178
|
-
if len(set(targets)) < 2:
|
|
179
|
-
return False
|
|
180
|
-
|
|
181
|
-
downstream_lanes = {
|
|
182
|
-
(
|
|
183
|
-
order[target_id],
|
|
184
|
-
group_membership.get(target_id, ()),
|
|
185
|
-
)
|
|
186
|
-
for target_id in targets
|
|
187
|
-
}
|
|
188
|
-
return len(downstream_lanes) >= 2
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def _group_membership(
|
|
192
|
-
validated: ValidatedPipeline | ValidatedDetailPanel,
|
|
193
|
-
) -> dict[str, tuple[str, ...]]:
|
|
194
|
-
memberships: dict[str, list[str]] = defaultdict(list)
|
|
195
|
-
for group in validated.groups:
|
|
196
|
-
for node_id in group.node_ids:
|
|
197
|
-
memberships[node_id].append(group.id)
|
|
198
|
-
return {
|
|
199
|
-
node_id: tuple(sorted(group_ids))
|
|
200
|
-
for node_id, group_ids in memberships.items()
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def _reorder_component_nodes(
|
|
205
|
-
validated: ValidatedPipeline | ValidatedDetailPanel,
|
|
206
|
-
ranks: dict[str, int],
|
|
207
|
-
included_node_ids: set[str],
|
|
208
|
-
) -> dict[str, int]:
|
|
209
|
-
if not included_node_ids:
|
|
210
|
-
return {}
|
|
211
|
-
|
|
212
|
-
nodes_by_rank: dict[int, list[str]] = defaultdict(list)
|
|
213
|
-
for node_id in included_node_ids:
|
|
214
|
-
nodes_by_rank[ranks[node_id]].append(node_id)
|
|
215
|
-
|
|
216
|
-
for rank_nodes in nodes_by_rank.values():
|
|
217
|
-
rank_nodes.sort(key=validated.node_index.__getitem__)
|
|
218
|
-
|
|
219
|
-
local_order = {
|
|
220
|
-
node_id: index
|
|
221
|
-
for rank_nodes in nodes_by_rank.values()
|
|
222
|
-
for index, node_id in enumerate(rank_nodes)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
incoming, outgoing = _forward_neighbors(
|
|
226
|
-
validated,
|
|
227
|
-
ranks,
|
|
228
|
-
included_node_ids=included_node_ids,
|
|
229
|
-
)
|
|
230
|
-
ordered_ranks = sorted(nodes_by_rank)
|
|
231
|
-
|
|
232
|
-
for _ in range(4):
|
|
233
|
-
for rank in ordered_ranks[1:]:
|
|
234
|
-
_resort_rank(nodes_by_rank[rank], incoming, local_order, validated)
|
|
235
|
-
_refresh_order(nodes_by_rank[rank], local_order)
|
|
236
|
-
for rank in reversed(ordered_ranks[:-1]):
|
|
237
|
-
_resort_rank(nodes_by_rank[rank], outgoing, local_order, validated)
|
|
238
|
-
_refresh_order(nodes_by_rank[rank], local_order)
|
|
239
|
-
|
|
240
|
-
return local_order
|
|
241
|
-
|
|
242
|
-
|
|
243
120
|
def _detail_focus_path_nodes(
|
|
244
121
|
validated: ValidatedPipeline,
|
|
245
122
|
ranks: dict[str, int],
|
|
@@ -810,7 +810,9 @@ def _select_forward_route(
|
|
|
810
810
|
target_node.node.id,
|
|
811
811
|
routing_groups,
|
|
812
812
|
)
|
|
813
|
-
evaluations: list[
|
|
813
|
+
evaluations: list[
|
|
814
|
+
tuple[bool, tuple[int, int, int, int, float, float, int, float, int, float, int], CandidatePath]
|
|
815
|
+
] = []
|
|
814
816
|
|
|
815
817
|
for candidate_index, (kind, candidate) in enumerate(candidates):
|
|
816
818
|
clearance_ok = _path_respects_group_clearance(
|
|
@@ -1560,7 +1562,7 @@ def _select_candidate_with_priority(
|
|
|
1560
1562
|
evaluations: list[
|
|
1561
1563
|
tuple[
|
|
1562
1564
|
bool,
|
|
1563
|
-
tuple[int, int, int, float, float, int, float, int, float, int],
|
|
1565
|
+
tuple[int, int, int, int, float, float, int, float, int, float, int],
|
|
1564
1566
|
CandidatePath,
|
|
1565
1567
|
]
|
|
1566
1568
|
],
|
|
@@ -1748,7 +1750,7 @@ def _forward_priority_key(
|
|
|
1748
1750
|
theme: "Theme",
|
|
1749
1751
|
has_relevant_groups: bool,
|
|
1750
1752
|
interactions: InteractionMetrics,
|
|
1751
|
-
) -> tuple[int, int, int, float, float, int, float, int, float, int]:
|
|
1753
|
+
) -> tuple[int, int, int, int, float, float, int, float, int, float, int]:
|
|
1752
1754
|
collisions, backwards, bends, length = _route_metrics(
|
|
1753
1755
|
candidate,
|
|
1754
1756
|
nodes=nodes,
|
|
@@ -1758,6 +1760,16 @@ def _forward_priority_key(
|
|
|
1758
1760
|
return (
|
|
1759
1761
|
interactions.edge_crossings,
|
|
1760
1762
|
collisions,
|
|
1763
|
+
_clean_direct_elbow_priority(
|
|
1764
|
+
kind,
|
|
1765
|
+
collisions=collisions,
|
|
1766
|
+
backwards=backwards,
|
|
1767
|
+
bends=bends,
|
|
1768
|
+
has_relevant_groups=has_relevant_groups,
|
|
1769
|
+
interactions=interactions,
|
|
1770
|
+
source_node=source_node,
|
|
1771
|
+
target_node=target_node,
|
|
1772
|
+
),
|
|
1761
1773
|
_forward_side_change_penalty(kind, has_relevant_groups=has_relevant_groups),
|
|
1762
1774
|
interactions.edge_overlap_length,
|
|
1763
1775
|
interactions.obstacle_overlap_length,
|
|
@@ -1775,6 +1787,30 @@ def _forward_priority_key(
|
|
|
1775
1787
|
)
|
|
1776
1788
|
|
|
1777
1789
|
|
|
1790
|
+
def _clean_direct_elbow_priority(
|
|
1791
|
+
kind: str,
|
|
1792
|
+
*,
|
|
1793
|
+
collisions: int,
|
|
1794
|
+
backwards: float,
|
|
1795
|
+
bends: int,
|
|
1796
|
+
has_relevant_groups: bool,
|
|
1797
|
+
interactions: InteractionMetrics,
|
|
1798
|
+
source_node: LayoutNode,
|
|
1799
|
+
target_node: LayoutNode,
|
|
1800
|
+
) -> int:
|
|
1801
|
+
if kind != "direct_elbow" or has_relevant_groups:
|
|
1802
|
+
return 1
|
|
1803
|
+
if source_node.order >= target_node.order:
|
|
1804
|
+
return 1
|
|
1805
|
+
if collisions != 0 or bends != 1 or backwards > EPSILON:
|
|
1806
|
+
return 1
|
|
1807
|
+
if interactions.edge_crossings != 0 or interactions.edge_overlap_length > EPSILON:
|
|
1808
|
+
return 1
|
|
1809
|
+
if interactions.obstacle_overlap_length > EPSILON or interactions.obstacle_crossings != 0:
|
|
1810
|
+
return 1
|
|
1811
|
+
return 0
|
|
1812
|
+
|
|
1813
|
+
|
|
1778
1814
|
def _forward_kind_priority(
|
|
1779
1815
|
kind: str,
|
|
1780
1816
|
*,
|
|
@@ -905,6 +905,24 @@ def test_grouped_forward_edge_can_use_right_side_direct_elbow() -> None:
|
|
|
905
905
|
assert all(not _point_in_bounds(point, inputs) for point in bends)
|
|
906
906
|
|
|
907
907
|
|
|
908
|
+
def test_generate_pipeline_script_prefers_clean_single_bend_guidance_elbow() -> None:
|
|
909
|
+
namespace = runpy.run_path("test/generate_pipeline.py")
|
|
910
|
+
pipeline = namespace["build_pipeline"]()
|
|
911
|
+
layout = build_layout(pipeline)
|
|
912
|
+
routed = {edge.edge.id: edge for edge in layout.main.edges}
|
|
913
|
+
guide = layout.main.nodes["h_feature"]
|
|
914
|
+
target = layout.main.nodes["main_blocks"]
|
|
915
|
+
|
|
916
|
+
points = routed["h_to_main"].points
|
|
917
|
+
bends = _bend_points(points)
|
|
918
|
+
|
|
919
|
+
assert len(points) == 3
|
|
920
|
+
assert bends == [points[1]]
|
|
921
|
+
assert points[0] == Point(guide.right, guide.center_y)
|
|
922
|
+
assert points[1] == Point(target.center_x, guide.center_y)
|
|
923
|
+
assert points[-1] == Point(target.center_x, target.y)
|
|
924
|
+
|
|
925
|
+
|
|
908
926
|
def test_back_edge_leaves_group_before_bending() -> None:
|
|
909
927
|
pipeline = Pipeline(
|
|
910
928
|
nodes=[
|
|
@@ -1472,25 +1490,33 @@ def test_detail_panel_nested_groups_keep_visible_inner_gap_and_header_clearance(
|
|
|
1472
1490
|
assert child.y - parent.y > pipeline.theme.subtitle_font_size * metrics.line_height_ratio
|
|
1473
1491
|
|
|
1474
1492
|
|
|
1475
|
-
def
|
|
1493
|
+
def test_detail_panel_shared_guidance_node_uses_general_lane_ordering() -> None:
|
|
1476
1494
|
layout = build_layout(_build_generate_pipeline_fixture())
|
|
1477
1495
|
panel_nodes = layout.detail_panel.graph.nodes
|
|
1478
|
-
panel_groups = layout.detail_panel.graph.groups
|
|
1479
1496
|
routed = {edge.edge.id: edge for edge in layout.detail_panel.graph.edges}
|
|
1480
1497
|
h_node = panel_nodes["panel_h"]
|
|
1481
1498
|
source_lane_one = _source_access_segment(routed["panel_h_to_nca_c"].points)
|
|
1482
1499
|
source_lane_two = _source_access_segment(routed["panel_h_to_nca_m"].points)
|
|
1483
1500
|
|
|
1484
|
-
assert
|
|
1485
|
-
assert
|
|
1486
|
-
assert panel_nodes["panel_m_in"].order ==
|
|
1487
|
-
assert
|
|
1488
|
-
assert routed["panel_h_to_nca_c"].points[0].x == h_node.right
|
|
1489
|
-
assert routed["panel_h_to_nca_m"].points[0].x == h_node.right
|
|
1490
|
-
assert routed["panel_h_to_nca_c"].points[0].y != routed["panel_h_to_nca_m"].points[0].y
|
|
1501
|
+
assert panel_nodes["panel_c_in"].order == panel_nodes["panel_nca_c"].order == 0
|
|
1502
|
+
assert h_node.order == panel_nodes["panel_nca_m"].order == 1
|
|
1503
|
+
assert panel_nodes["panel_m_in"].order == 2
|
|
1504
|
+
assert h_node.order != 0
|
|
1491
1505
|
assert _collinear_overlap_length(*source_lane_one, *source_lane_two) == pytest.approx(0.0, abs=0.01)
|
|
1492
1506
|
|
|
1493
1507
|
|
|
1508
|
+
def test_generate_pipeline_script_detail_panel_aligns_guidance_with_candidate_lane() -> None:
|
|
1509
|
+
namespace = runpy.run_path("test/generate_pipeline.py")
|
|
1510
|
+
pipeline = namespace["build_pipeline"]()
|
|
1511
|
+
layout = build_layout(pipeline)
|
|
1512
|
+
panel_nodes = layout.detail_panel.graph.nodes
|
|
1513
|
+
routed = {edge.edge.id: edge for edge in layout.detail_panel.graph.edges}
|
|
1514
|
+
|
|
1515
|
+
assert panel_nodes["panel_h"].order == panel_nodes["panel_local_c"].order == panel_nodes["panel_global_c"].order == 0
|
|
1516
|
+
assert panel_nodes["panel_local_m"].order == panel_nodes["panel_global_m"].order == 1
|
|
1517
|
+
assert routed["panel_h_to_local_c"].points[0].y == routed["panel_h_to_local_c"].points[-1].y
|
|
1518
|
+
|
|
1519
|
+
|
|
1494
1520
|
def test_generate_pipeline_row_gap_no_longer_scales_with_raw_cross_row_edge_count() -> None:
|
|
1495
1521
|
layout = build_layout(_build_generate_pipeline_fixture())
|
|
1496
1522
|
gap = _row_gap_between_rows(layout.main.nodes, 0, 1)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|