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.
Files changed (27) hide show
  1. {frameplot-0.5.3/src/frameplot.egg-info → frameplot-0.5.5}/PKG-INFO +1 -1
  2. {frameplot-0.5.3 → frameplot-0.5.5}/pyproject.toml +1 -1
  3. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/order.py +0 -123
  4. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/route.py +39 -3
  5. {frameplot-0.5.3 → frameplot-0.5.5/src/frameplot.egg-info}/PKG-INFO +1 -1
  6. {frameplot-0.5.3 → frameplot-0.5.5}/tests/test_rendering.py +35 -9
  7. {frameplot-0.5.3 → frameplot-0.5.5}/LICENSE +0 -0
  8. {frameplot-0.5.3 → frameplot-0.5.5}/README.md +0 -0
  9. {frameplot-0.5.3 → frameplot-0.5.5}/setup.cfg +0 -0
  10. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/__init__.py +0 -0
  11. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/api.py +0 -0
  12. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/__init__.py +0 -0
  13. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/place.py +0 -0
  14. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/rank.py +0 -0
  15. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/scc.py +0 -0
  16. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/text.py +0 -0
  17. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/types.py +0 -0
  18. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/layout/validate.py +0 -0
  19. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/model.py +0 -0
  20. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/render/__init__.py +0 -0
  21. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/render/png.py +0 -0
  22. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/render/svg.py +0 -0
  23. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot/theme.py +0 -0
  24. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot.egg-info/SOURCES.txt +0 -0
  25. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot.egg-info/dependency_links.txt +0 -0
  26. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot.egg-info/requires.txt +0 -0
  27. {frameplot-0.5.3 → frameplot-0.5.5}/src/frameplot.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frameplot
3
- Version: 0.5.3
3
+ Version: 0.5.5
4
4
  Summary: Turn Python-defined pipeline graphs into presentation-ready SVG and PNG diagrams.
5
5
  Author: Small Turtle 2
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "frameplot"
7
- version = "0.5.3"
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[tuple[bool, tuple[int, int, int, float, float, int, float, int, float, int], CandidatePath]] = []
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
  *,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frameplot
3
- Version: 0.5.3
3
+ Version: 0.5.5
4
4
  Summary: Turn Python-defined pipeline graphs into presentation-ready SVG and PNG diagrams.
5
5
  Author: Small Turtle 2
6
6
  License-Expression: MIT
@@ -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 test_detail_panel_shared_guidance_node_stays_above_stream_groups() -> None:
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 h_node.order == 0
1485
- assert panel_nodes["panel_c_in"].order == panel_nodes["panel_nca_c"].order == 1
1486
- assert panel_nodes["panel_m_in"].order == panel_nodes["panel_nca_m"].order == 2
1487
- assert all(not _point_in_bounds(Point(h_node.center_x, h_node.center_y), overlay.bounds) for overlay in panel_groups)
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