frameplot 0.5.6__tar.gz → 0.5.7__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.6/src/frameplot.egg-info → frameplot-0.5.7}/PKG-INFO +1 -1
- {frameplot-0.5.6 → frameplot-0.5.7}/pyproject.toml +1 -1
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/layout/route.py +578 -275
- {frameplot-0.5.6 → frameplot-0.5.7/src/frameplot.egg-info}/PKG-INFO +1 -1
- {frameplot-0.5.6 → frameplot-0.5.7}/tests/test_rendering.py +49 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/LICENSE +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/README.md +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/setup.cfg +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/__init__.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/api.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/layout/__init__.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/layout/order.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/layout/place.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/layout/rank.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/layout/scc.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/layout/text.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/layout/types.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/layout/validate.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/model.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/render/__init__.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/render/png.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/render/svg.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot/theme.py +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot.egg-info/SOURCES.txt +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot.egg-info/dependency_links.txt +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/src/frameplot.egg-info/requires.txt +0 -0
- {frameplot-0.5.6 → frameplot-0.5.7}/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.7"
|
|
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"
|
|
@@ -2,11 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
+
from typing import TypeVar
|
|
5
6
|
|
|
6
7
|
from frameplot.layout.types import Bounds, GroupOverlay, LayoutNode, Point, RoutedEdge, union_bounds
|
|
7
8
|
from frameplot.theme import resolve_theme_metrics
|
|
8
9
|
|
|
9
10
|
EPSILON = 0.01
|
|
11
|
+
SelectionItem = TypeVar("SelectionItem")
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
@dataclass(slots=True)
|
|
@@ -133,6 +135,35 @@ class InteractionMetrics:
|
|
|
133
135
|
obstacle_crossings: int = 0
|
|
134
136
|
|
|
135
137
|
|
|
138
|
+
@dataclass(slots=True, frozen=True)
|
|
139
|
+
class CandidateEvaluation:
|
|
140
|
+
clearance_ok: bool
|
|
141
|
+
shared_local: bool
|
|
142
|
+
endpoint_preferred: bool
|
|
143
|
+
clean_direct: bool
|
|
144
|
+
collisions: int
|
|
145
|
+
edge_crossings: int
|
|
146
|
+
edge_overlap_length: float
|
|
147
|
+
kind_priority: int
|
|
148
|
+
backwards: float
|
|
149
|
+
bends: int
|
|
150
|
+
length: float
|
|
151
|
+
candidate_index: int
|
|
152
|
+
candidate: CandidatePath
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(slots=True, frozen=True)
|
|
156
|
+
class PointRouteEvaluation:
|
|
157
|
+
clearance_ok: bool
|
|
158
|
+
shared_local: bool
|
|
159
|
+
edge_crossings: int
|
|
160
|
+
edge_overlap_length: float
|
|
161
|
+
bends: int
|
|
162
|
+
length: float
|
|
163
|
+
slot: int
|
|
164
|
+
points: tuple[Point, ...]
|
|
165
|
+
|
|
166
|
+
|
|
136
167
|
def _resolved_target(validated: "ValidatedPipeline | ValidatedDetailPanel", edge: "Edge"):
|
|
137
168
|
return validated.edge_targets[edge.id]
|
|
138
169
|
|
|
@@ -803,24 +834,12 @@ def _select_forward_route(
|
|
|
803
834
|
target_node=target_node,
|
|
804
835
|
geometry=geometry,
|
|
805
836
|
pair_offset=pair_offset,
|
|
837
|
+
routing_groups=routing_groups,
|
|
806
838
|
theme=theme,
|
|
807
839
|
)
|
|
808
|
-
|
|
809
|
-
source_node.node.id,
|
|
810
|
-
target_node.node.id,
|
|
811
|
-
routing_groups,
|
|
812
|
-
)
|
|
813
|
-
evaluations: list[
|
|
814
|
-
tuple[bool, tuple[int, int, int, int, float, float, int, float, int, float, int], CandidatePath]
|
|
815
|
-
] = []
|
|
840
|
+
evaluations: list[CandidateEvaluation] = []
|
|
816
841
|
|
|
817
842
|
for candidate_index, (kind, candidate) in enumerate(candidates):
|
|
818
|
-
clearance_ok = _path_respects_group_clearance(
|
|
819
|
-
candidate.points,
|
|
820
|
-
source_node.node.id,
|
|
821
|
-
target_node.node.id,
|
|
822
|
-
routing_groups,
|
|
823
|
-
)
|
|
824
843
|
interactions = _candidate_interaction_metrics(
|
|
825
844
|
candidate,
|
|
826
845
|
occupancy=occupancy,
|
|
@@ -828,24 +847,86 @@ def _select_forward_route(
|
|
|
828
847
|
target_node_id=target_node.node.id,
|
|
829
848
|
routing_groups=routing_groups,
|
|
830
849
|
)
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
850
|
+
evaluations.append(
|
|
851
|
+
_build_forward_candidate_evaluation(
|
|
852
|
+
kind=kind,
|
|
853
|
+
candidate=candidate,
|
|
854
|
+
candidate_index=candidate_index,
|
|
855
|
+
source_node=source_node,
|
|
856
|
+
target_node=target_node,
|
|
857
|
+
nodes=nodes,
|
|
858
|
+
outgoing_count=outgoing_count,
|
|
859
|
+
incoming_count=incoming_count,
|
|
860
|
+
theme=theme,
|
|
861
|
+
interactions=interactions,
|
|
862
|
+
routing_groups=routing_groups,
|
|
863
|
+
)
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
selected = _select_candidate_evaluation(evaluations)
|
|
867
|
+
assert selected is not None
|
|
868
|
+
return selected.candidate
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def _build_forward_candidate_evaluation(
|
|
872
|
+
*,
|
|
873
|
+
kind: str,
|
|
874
|
+
candidate: CandidatePath,
|
|
875
|
+
candidate_index: int,
|
|
876
|
+
source_node: LayoutNode,
|
|
877
|
+
target_node: LayoutNode,
|
|
878
|
+
nodes: dict[str, LayoutNode],
|
|
879
|
+
outgoing_count: int,
|
|
880
|
+
incoming_count: int,
|
|
881
|
+
theme: "Theme",
|
|
882
|
+
interactions: InteractionMetrics,
|
|
883
|
+
routing_groups: RoutingGroups | None,
|
|
884
|
+
) -> CandidateEvaluation:
|
|
885
|
+
clearance_ok = _path_respects_group_clearance(
|
|
886
|
+
candidate.points,
|
|
887
|
+
source_node.node.id,
|
|
888
|
+
target_node.node.id,
|
|
889
|
+
routing_groups,
|
|
890
|
+
)
|
|
891
|
+
collisions, backwards, bends, length = _route_metrics(
|
|
892
|
+
candidate,
|
|
893
|
+
nodes=nodes,
|
|
894
|
+
ignored_node_ids={source_node.node.id, target_node.node.id},
|
|
895
|
+
theme=theme,
|
|
896
|
+
)
|
|
897
|
+
return CandidateEvaluation(
|
|
898
|
+
clearance_ok=clearance_ok,
|
|
899
|
+
shared_local=_candidate_uses_shared_local(kind),
|
|
900
|
+
endpoint_preferred=_forward_endpoint_preferred(
|
|
901
|
+
candidate,
|
|
835
902
|
source_node=source_node,
|
|
836
903
|
target_node=target_node,
|
|
837
|
-
nodes=nodes,
|
|
838
904
|
outgoing_count=outgoing_count,
|
|
839
905
|
incoming_count=incoming_count,
|
|
840
|
-
|
|
841
|
-
|
|
906
|
+
routing_groups=routing_groups,
|
|
907
|
+
),
|
|
908
|
+
clean_direct=_forward_clean_direct_preferred(
|
|
909
|
+
kind,
|
|
910
|
+
candidate,
|
|
911
|
+
source_node=source_node,
|
|
912
|
+
target_node=target_node,
|
|
913
|
+
collisions=collisions,
|
|
842
914
|
interactions=interactions,
|
|
843
|
-
)
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
915
|
+
),
|
|
916
|
+
collisions=collisions,
|
|
917
|
+
edge_crossings=interactions.edge_crossings,
|
|
918
|
+
edge_overlap_length=interactions.edge_overlap_length,
|
|
919
|
+
kind_priority=_forward_kind_priority(
|
|
920
|
+
kind=kind,
|
|
921
|
+
outgoing_count=outgoing_count,
|
|
922
|
+
incoming_count=incoming_count,
|
|
923
|
+
),
|
|
924
|
+
backwards=backwards,
|
|
925
|
+
bends=bends,
|
|
926
|
+
length=length,
|
|
927
|
+
candidate_index=candidate_index,
|
|
928
|
+
candidate=candidate,
|
|
929
|
+
)
|
|
849
930
|
|
|
850
931
|
|
|
851
932
|
def _route_forward(
|
|
@@ -1051,18 +1132,13 @@ def _select_edge_join_route(
|
|
|
1051
1132
|
join_plan=join_plan,
|
|
1052
1133
|
geometry=geometry,
|
|
1053
1134
|
pair_offset=pair_offset,
|
|
1135
|
+
routing_groups=routing_groups,
|
|
1054
1136
|
theme=theme,
|
|
1055
1137
|
)
|
|
1056
1138
|
direction = _edge_direction_kind(source_node, target_node)
|
|
1057
|
-
evaluations: list[
|
|
1139
|
+
evaluations: list[CandidateEvaluation] = []
|
|
1058
1140
|
|
|
1059
1141
|
for candidate_index, (kind, candidate) in enumerate(candidates):
|
|
1060
|
-
clearance_ok = _path_respects_group_clearance(
|
|
1061
|
-
candidate.points,
|
|
1062
|
-
source_node.node.id,
|
|
1063
|
-
target_node.node.id,
|
|
1064
|
-
routing_groups,
|
|
1065
|
-
)
|
|
1066
1142
|
interactions = _candidate_interaction_metrics(
|
|
1067
1143
|
candidate,
|
|
1068
1144
|
occupancy=occupancy,
|
|
@@ -1070,25 +1146,72 @@ def _select_edge_join_route(
|
|
|
1070
1146
|
target_node_id=target_node.node.id,
|
|
1071
1147
|
routing_groups=routing_groups,
|
|
1072
1148
|
)
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1149
|
+
evaluations.append(
|
|
1150
|
+
_build_edge_join_candidate_evaluation(
|
|
1151
|
+
kind=kind,
|
|
1152
|
+
candidate=candidate,
|
|
1153
|
+
candidate_index=candidate_index,
|
|
1154
|
+
source_node=source_node,
|
|
1155
|
+
target_node=target_node,
|
|
1156
|
+
nodes=nodes,
|
|
1157
|
+
theme=theme,
|
|
1158
|
+
direction=direction,
|
|
1159
|
+
interactions=interactions,
|
|
1160
|
+
routing_groups=routing_groups,
|
|
1161
|
+
)
|
|
1083
1162
|
)
|
|
1084
|
-
evaluations.append((clearance_ok, priority_key, candidate))
|
|
1085
1163
|
|
|
1086
|
-
selected =
|
|
1164
|
+
selected = _select_candidate_evaluation(evaluations)
|
|
1087
1165
|
if selected is not None:
|
|
1088
|
-
return selected
|
|
1166
|
+
return selected.candidate
|
|
1089
1167
|
raise AssertionError(f"No viable join route found for edge {edge.id}.")
|
|
1090
1168
|
|
|
1091
1169
|
|
|
1170
|
+
def _build_edge_join_candidate_evaluation(
|
|
1171
|
+
*,
|
|
1172
|
+
kind: str,
|
|
1173
|
+
candidate: CandidatePath,
|
|
1174
|
+
candidate_index: int,
|
|
1175
|
+
source_node: LayoutNode,
|
|
1176
|
+
target_node: LayoutNode,
|
|
1177
|
+
nodes: dict[str, LayoutNode],
|
|
1178
|
+
theme: "Theme",
|
|
1179
|
+
direction: str,
|
|
1180
|
+
interactions: InteractionMetrics,
|
|
1181
|
+
routing_groups: RoutingGroups | None,
|
|
1182
|
+
) -> CandidateEvaluation:
|
|
1183
|
+
clearance_ok = _path_respects_group_clearance(
|
|
1184
|
+
candidate.points,
|
|
1185
|
+
source_node.node.id,
|
|
1186
|
+
target_node.node.id,
|
|
1187
|
+
routing_groups,
|
|
1188
|
+
)
|
|
1189
|
+
collisions, backwards, bends, length = _route_metrics(
|
|
1190
|
+
candidate,
|
|
1191
|
+
nodes=nodes,
|
|
1192
|
+
ignored_node_ids={source_node.node.id, target_node.node.id},
|
|
1193
|
+
theme=theme,
|
|
1194
|
+
)
|
|
1195
|
+
return CandidateEvaluation(
|
|
1196
|
+
clearance_ok=clearance_ok,
|
|
1197
|
+
shared_local=_candidate_uses_shared_local(kind),
|
|
1198
|
+
endpoint_preferred=True,
|
|
1199
|
+
clean_direct=False,
|
|
1200
|
+
collisions=collisions,
|
|
1201
|
+
edge_crossings=interactions.edge_crossings,
|
|
1202
|
+
edge_overlap_length=interactions.edge_overlap_length,
|
|
1203
|
+
kind_priority=_edge_join_kind_priority(
|
|
1204
|
+
kind=kind,
|
|
1205
|
+
direction=direction,
|
|
1206
|
+
),
|
|
1207
|
+
backwards=backwards,
|
|
1208
|
+
bends=bends,
|
|
1209
|
+
length=length,
|
|
1210
|
+
candidate_index=candidate_index,
|
|
1211
|
+
candidate=candidate,
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
|
|
1092
1215
|
def _build_edge_join_candidates(
|
|
1093
1216
|
*,
|
|
1094
1217
|
source_node: LayoutNode,
|
|
@@ -1096,19 +1219,32 @@ def _build_edge_join_candidates(
|
|
|
1096
1219
|
join_plan: EdgeJoinPlan,
|
|
1097
1220
|
geometry: ComponentGeometry,
|
|
1098
1221
|
pair_offset: float,
|
|
1222
|
+
routing_groups: RoutingGroups | None,
|
|
1099
1223
|
theme: "Theme",
|
|
1100
1224
|
) -> list[tuple[str, CandidatePath]]:
|
|
1101
1225
|
join_point = join_plan.join_point
|
|
1102
1226
|
source_right = _right_port(source_node, pair_offset)
|
|
1227
|
+
shared_row_lanes = _shared_group_row_lane_positions(
|
|
1228
|
+
source_node.node.id,
|
|
1229
|
+
target_node.node.id,
|
|
1230
|
+
routing_groups,
|
|
1231
|
+
theme,
|
|
1232
|
+
)
|
|
1233
|
+
shared_column_lanes = _shared_group_column_lane_positions(
|
|
1234
|
+
source_node,
|
|
1235
|
+
target_node,
|
|
1236
|
+
geometry,
|
|
1237
|
+
routing_groups,
|
|
1238
|
+
theme,
|
|
1239
|
+
)
|
|
1103
1240
|
outer_row_lanes = _outer_row_lane_positions(geometry, theme)
|
|
1104
1241
|
outer_column_lanes = _outer_column_lane_positions(geometry, theme)
|
|
1105
1242
|
row_lanes = _unique_lanes(
|
|
1106
1243
|
_source_row_lane_positions(source_node, target_node, geometry, theme)
|
|
1107
1244
|
+ _target_row_lane_positions(source_node, target_node, geometry, theme)
|
|
1108
|
-
+ outer_row_lanes
|
|
1109
1245
|
)
|
|
1110
1246
|
column_lanes = _unique_lanes(
|
|
1111
|
-
_join_column_lane_positions(source_node, target_node, geometry, theme)
|
|
1247
|
+
_join_column_lane_positions(source_node, target_node, geometry, theme)
|
|
1112
1248
|
)
|
|
1113
1249
|
candidates: dict[tuple[Point, ...], tuple[str, CandidatePath]] = {}
|
|
1114
1250
|
|
|
@@ -1125,11 +1261,50 @@ def _build_edge_join_candidates(
|
|
|
1125
1261
|
(join_point, "stub"),
|
|
1126
1262
|
],
|
|
1127
1263
|
)
|
|
1264
|
+
for lane_y in shared_row_lanes:
|
|
1265
|
+
source_vertical = _vertical_port(source_node, pair_offset, lane_y)
|
|
1266
|
+
add_candidate(
|
|
1267
|
+
"shared_row_join",
|
|
1268
|
+
source_vertical,
|
|
1269
|
+
[
|
|
1270
|
+
(Point(source_vertical.x, lane_y), "stub"),
|
|
1271
|
+
(Point(join_point.x, lane_y), "reserve"),
|
|
1272
|
+
(join_point, "stub"),
|
|
1273
|
+
],
|
|
1274
|
+
)
|
|
1275
|
+
add_candidate(
|
|
1276
|
+
"shared_row_join",
|
|
1277
|
+
source_right,
|
|
1278
|
+
[
|
|
1279
|
+
(Point(source_right.x, lane_y), "reserve"),
|
|
1280
|
+
(Point(join_point.x, lane_y), "reserve"),
|
|
1281
|
+
(join_point, "stub"),
|
|
1282
|
+
],
|
|
1283
|
+
)
|
|
1128
1284
|
for lane_y in row_lanes:
|
|
1129
1285
|
source_vertical = _vertical_port(source_node, pair_offset, lane_y)
|
|
1130
|
-
kind = "outer_row_join" if lane_y in outer_row_lanes else "row_join"
|
|
1131
1286
|
add_candidate(
|
|
1132
|
-
|
|
1287
|
+
"row_join",
|
|
1288
|
+
source_vertical,
|
|
1289
|
+
[
|
|
1290
|
+
(Point(source_vertical.x, lane_y), "stub"),
|
|
1291
|
+
(Point(join_point.x, lane_y), "reserve"),
|
|
1292
|
+
(join_point, "stub"),
|
|
1293
|
+
],
|
|
1294
|
+
)
|
|
1295
|
+
add_candidate(
|
|
1296
|
+
"row_join",
|
|
1297
|
+
source_right,
|
|
1298
|
+
[
|
|
1299
|
+
(Point(source_right.x, lane_y), "reserve"),
|
|
1300
|
+
(Point(join_point.x, lane_y), "reserve"),
|
|
1301
|
+
(join_point, "stub"),
|
|
1302
|
+
],
|
|
1303
|
+
)
|
|
1304
|
+
for lane_y in outer_row_lanes:
|
|
1305
|
+
source_vertical = _vertical_port(source_node, pair_offset, lane_y)
|
|
1306
|
+
add_candidate(
|
|
1307
|
+
"outer_row_join",
|
|
1133
1308
|
source_vertical,
|
|
1134
1309
|
[
|
|
1135
1310
|
(Point(source_vertical.x, lane_y), "stub"),
|
|
@@ -1138,7 +1313,7 @@ def _build_edge_join_candidates(
|
|
|
1138
1313
|
],
|
|
1139
1314
|
)
|
|
1140
1315
|
add_candidate(
|
|
1141
|
-
|
|
1316
|
+
"outer_row_join",
|
|
1142
1317
|
source_right,
|
|
1143
1318
|
[
|
|
1144
1319
|
(Point(source_right.x, lane_y), "reserve"),
|
|
@@ -1146,11 +1321,34 @@ def _build_edge_join_candidates(
|
|
|
1146
1321
|
(join_point, "stub"),
|
|
1147
1322
|
],
|
|
1148
1323
|
)
|
|
1324
|
+
for lane_x in shared_column_lanes:
|
|
1325
|
+
for lane_y in shared_row_lanes:
|
|
1326
|
+
add_candidate(
|
|
1327
|
+
"shared_join",
|
|
1328
|
+
source_right,
|
|
1329
|
+
[
|
|
1330
|
+
(Point(lane_x, source_right.y), "reserve"),
|
|
1331
|
+
(Point(lane_x, lane_y), "reserve"),
|
|
1332
|
+
(Point(join_point.x, lane_y), "reserve"),
|
|
1333
|
+
(join_point, "stub"),
|
|
1334
|
+
],
|
|
1335
|
+
)
|
|
1149
1336
|
for lane_x in column_lanes:
|
|
1150
1337
|
for lane_y in row_lanes:
|
|
1151
|
-
outer_kind = "outer_join" if lane_x in outer_column_lanes or lane_y in outer_row_lanes else "lane_join"
|
|
1152
1338
|
add_candidate(
|
|
1153
|
-
|
|
1339
|
+
"lane_join",
|
|
1340
|
+
source_right,
|
|
1341
|
+
[
|
|
1342
|
+
(Point(lane_x, source_right.y), "reserve"),
|
|
1343
|
+
(Point(lane_x, lane_y), "reserve"),
|
|
1344
|
+
(Point(join_point.x, lane_y), "reserve"),
|
|
1345
|
+
(join_point, "stub"),
|
|
1346
|
+
],
|
|
1347
|
+
)
|
|
1348
|
+
for lane_x in _unique_lanes(shared_column_lanes + column_lanes + outer_column_lanes):
|
|
1349
|
+
for lane_y in outer_row_lanes:
|
|
1350
|
+
add_candidate(
|
|
1351
|
+
"outer_join",
|
|
1154
1352
|
source_right,
|
|
1155
1353
|
[
|
|
1156
1354
|
(Point(lane_x, source_right.y), "reserve"),
|
|
@@ -1168,10 +1366,19 @@ def _build_edge_join_candidates(
|
|
|
1168
1366
|
(join_point, "stub"),
|
|
1169
1367
|
],
|
|
1170
1368
|
)
|
|
1369
|
+
for lane_x in shared_column_lanes:
|
|
1370
|
+
add_candidate(
|
|
1371
|
+
"shared_column_join",
|
|
1372
|
+
source_right,
|
|
1373
|
+
[
|
|
1374
|
+
(Point(lane_x, source_right.y), "reserve"),
|
|
1375
|
+
(Point(lane_x, join_point.y), "reserve"),
|
|
1376
|
+
(join_point, "stub"),
|
|
1377
|
+
],
|
|
1378
|
+
)
|
|
1171
1379
|
for lane_x in column_lanes:
|
|
1172
|
-
kind = "outer_column_join" if lane_x in outer_column_lanes else "column_join"
|
|
1173
1380
|
add_candidate(
|
|
1174
|
-
|
|
1381
|
+
"column_join",
|
|
1175
1382
|
source_right,
|
|
1176
1383
|
[
|
|
1177
1384
|
(Point(lane_x, source_right.y), "reserve"),
|
|
@@ -1179,12 +1386,47 @@ def _build_edge_join_candidates(
|
|
|
1179
1386
|
(join_point, "stub"),
|
|
1180
1387
|
],
|
|
1181
1388
|
)
|
|
1389
|
+
for lane_x in outer_column_lanes:
|
|
1390
|
+
add_candidate(
|
|
1391
|
+
"outer_column_join",
|
|
1392
|
+
source_right,
|
|
1393
|
+
[
|
|
1394
|
+
(Point(lane_x, source_right.y), "reserve"),
|
|
1395
|
+
(Point(lane_x, join_point.y), "reserve"),
|
|
1396
|
+
(join_point, "stub"),
|
|
1397
|
+
],
|
|
1398
|
+
)
|
|
1399
|
+
for lane_y in shared_row_lanes:
|
|
1400
|
+
source_vertical = _vertical_port(source_node, pair_offset, lane_y)
|
|
1401
|
+
for lane_x in shared_column_lanes:
|
|
1402
|
+
add_candidate(
|
|
1403
|
+
"shared_join",
|
|
1404
|
+
source_vertical,
|
|
1405
|
+
[
|
|
1406
|
+
(Point(source_vertical.x, lane_y), "stub"),
|
|
1407
|
+
(Point(lane_x, lane_y), "reserve"),
|
|
1408
|
+
(Point(lane_x, join_point.y), "reserve"),
|
|
1409
|
+
(join_point, "stub"),
|
|
1410
|
+
],
|
|
1411
|
+
)
|
|
1182
1412
|
for lane_y in row_lanes:
|
|
1183
1413
|
source_vertical = _vertical_port(source_node, pair_offset, lane_y)
|
|
1184
1414
|
for lane_x in column_lanes:
|
|
1185
|
-
outer_kind = "outer_join" if lane_x in outer_column_lanes or lane_y in outer_row_lanes else "lane_join"
|
|
1186
1415
|
add_candidate(
|
|
1187
|
-
|
|
1416
|
+
"lane_join",
|
|
1417
|
+
source_vertical,
|
|
1418
|
+
[
|
|
1419
|
+
(Point(source_vertical.x, lane_y), "stub"),
|
|
1420
|
+
(Point(lane_x, lane_y), "reserve"),
|
|
1421
|
+
(Point(lane_x, join_point.y), "reserve"),
|
|
1422
|
+
(join_point, "stub"),
|
|
1423
|
+
],
|
|
1424
|
+
)
|
|
1425
|
+
for lane_y in _unique_lanes(shared_row_lanes + row_lanes + outer_row_lanes):
|
|
1426
|
+
source_vertical = _vertical_port(source_node, pair_offset, lane_y)
|
|
1427
|
+
for lane_x in outer_column_lanes:
|
|
1428
|
+
add_candidate(
|
|
1429
|
+
"outer_join",
|
|
1188
1430
|
source_vertical,
|
|
1189
1431
|
[
|
|
1190
1432
|
(Point(source_vertical.x, lane_y), "stub"),
|
|
@@ -1221,15 +1463,26 @@ def _build_forward_candidates(
|
|
|
1221
1463
|
target_node: LayoutNode,
|
|
1222
1464
|
geometry: ComponentGeometry,
|
|
1223
1465
|
pair_offset: float,
|
|
1466
|
+
routing_groups: RoutingGroups | None,
|
|
1224
1467
|
theme: "Theme",
|
|
1225
1468
|
) -> list[tuple[str, CandidatePath]]:
|
|
1226
1469
|
candidates: list[tuple[str, CandidatePath]] = []
|
|
1470
|
+
shared_row_lanes = _shared_group_row_lane_positions(
|
|
1471
|
+
source_node.node.id,
|
|
1472
|
+
target_node.node.id,
|
|
1473
|
+
routing_groups,
|
|
1474
|
+
theme,
|
|
1475
|
+
)
|
|
1227
1476
|
|
|
1228
1477
|
if source_node.order == target_node.order:
|
|
1229
1478
|
candidates.append(("direct", _direct_same_row_candidate(source_node, target_node, geometry, pair_offset, theme)))
|
|
1230
1479
|
else:
|
|
1231
1480
|
candidates.append(("direct_elbow", _direct_elbow_candidate(source_node, target_node, pair_offset)))
|
|
1232
1481
|
|
|
1482
|
+
for lane_y in shared_row_lanes:
|
|
1483
|
+
candidates.append(("shared_row", _row_source_candidate(source_node, target_node, lane_y, pair_offset)))
|
|
1484
|
+
candidates.append(("shared_row", _row_target_candidate(source_node, target_node, lane_y, pair_offset)))
|
|
1485
|
+
|
|
1233
1486
|
if source_node.rank in geometry.gap_after_rank:
|
|
1234
1487
|
for lane_x in _ordered_gap_positions(geometry.gap_after_rank[source_node.rank], theme, "start"):
|
|
1235
1488
|
candidates.append(
|
|
@@ -1260,7 +1513,11 @@ def _build_forward_candidates(
|
|
|
1260
1513
|
for lane_x in _outer_column_lane_positions(geometry, theme):
|
|
1261
1514
|
candidates.append(("outer_column", _outer_column_candidate(source_node, target_node, lane_x, pair_offset)))
|
|
1262
1515
|
|
|
1263
|
-
|
|
1516
|
+
unique: dict[tuple[Point, ...], tuple[str, CandidatePath]] = {}
|
|
1517
|
+
for kind, candidate in candidates:
|
|
1518
|
+
unique.setdefault(candidate.points, (kind, candidate))
|
|
1519
|
+
|
|
1520
|
+
return list(unique.values())
|
|
1264
1521
|
|
|
1265
1522
|
|
|
1266
1523
|
def _direct_same_row_candidate(
|
|
@@ -1476,6 +1733,46 @@ def _target_row_lane_positions(
|
|
|
1476
1733
|
return tuple(lanes)
|
|
1477
1734
|
|
|
1478
1735
|
|
|
1736
|
+
def _shared_group_row_lane_positions(
|
|
1737
|
+
source_node_id: str,
|
|
1738
|
+
target_node_id: str,
|
|
1739
|
+
routing_groups: RoutingGroups | None,
|
|
1740
|
+
theme: "Theme",
|
|
1741
|
+
) -> tuple[float, ...]:
|
|
1742
|
+
frame = _shared_group_frame(source_node_id, target_node_id, routing_groups)
|
|
1743
|
+
if frame is None:
|
|
1744
|
+
return ()
|
|
1745
|
+
|
|
1746
|
+
lanes = tuple(_shared_group_back_edge_lane_y(frame, slot, theme) for slot in range(3))
|
|
1747
|
+
return _unique_lanes(lanes)
|
|
1748
|
+
|
|
1749
|
+
|
|
1750
|
+
def _shared_group_column_lane_positions(
|
|
1751
|
+
source_node: LayoutNode,
|
|
1752
|
+
target_node: LayoutNode,
|
|
1753
|
+
geometry: ComponentGeometry,
|
|
1754
|
+
routing_groups: RoutingGroups | None,
|
|
1755
|
+
theme: "Theme",
|
|
1756
|
+
) -> tuple[float, ...]:
|
|
1757
|
+
frame = _shared_group_frame(source_node.node.id, target_node.node.id, routing_groups)
|
|
1758
|
+
if frame is None:
|
|
1759
|
+
return ()
|
|
1760
|
+
|
|
1761
|
+
lanes: list[float] = []
|
|
1762
|
+
|
|
1763
|
+
if source_node.rank in geometry.gap_after_rank:
|
|
1764
|
+
clipped_gap = _clip_lane_gap(geometry.gap_after_rank[source_node.rank], frame.bounds.x, frame.bounds.right)
|
|
1765
|
+
if clipped_gap is not None:
|
|
1766
|
+
lanes.extend(_ordered_gap_positions(clipped_gap, theme, "start"))
|
|
1767
|
+
|
|
1768
|
+
if target_node.rank in geometry.gap_before_rank:
|
|
1769
|
+
clipped_gap = _clip_lane_gap(geometry.gap_before_rank[target_node.rank], frame.bounds.x, frame.bounds.right)
|
|
1770
|
+
if clipped_gap is not None:
|
|
1771
|
+
lanes.extend(_ordered_gap_positions(clipped_gap, theme, "end"))
|
|
1772
|
+
|
|
1773
|
+
return _unique_lanes(tuple(lanes))
|
|
1774
|
+
|
|
1775
|
+
|
|
1479
1776
|
def _outer_row_lane_positions(
|
|
1480
1777
|
geometry: ComponentGeometry,
|
|
1481
1778
|
theme: "Theme",
|
|
@@ -1516,6 +1813,18 @@ def _ordered_gap_positions(
|
|
|
1516
1813
|
)
|
|
1517
1814
|
|
|
1518
1815
|
|
|
1816
|
+
def _clip_lane_gap(
|
|
1817
|
+
gap: tuple[float, float],
|
|
1818
|
+
lower_bound: float,
|
|
1819
|
+
upper_bound: float,
|
|
1820
|
+
) -> tuple[float, float] | None:
|
|
1821
|
+
start = round(max(gap[0], lower_bound), 2)
|
|
1822
|
+
end = round(min(gap[1], upper_bound), 2)
|
|
1823
|
+
if end - start <= EPSILON:
|
|
1824
|
+
return None
|
|
1825
|
+
return (start, end)
|
|
1826
|
+
|
|
1827
|
+
|
|
1519
1828
|
def _lane_positions(start: float, end: float, step: float) -> tuple[float, ...]:
|
|
1520
1829
|
width = end - start
|
|
1521
1830
|
if width <= step:
|
|
@@ -1558,32 +1867,129 @@ def _vertical_port(node: LayoutNode, pair_offset: float, lane_y: float) -> Point
|
|
|
1558
1867
|
return Point(x, node.y)
|
|
1559
1868
|
|
|
1560
1869
|
|
|
1561
|
-
def
|
|
1562
|
-
evaluations: list[
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
CandidatePath,
|
|
1567
|
-
]
|
|
1568
|
-
],
|
|
1569
|
-
) -> CandidatePath | None:
|
|
1570
|
-
for clearance_required in (True, False):
|
|
1571
|
-
scoped = [item for item in evaluations if item[0] is clearance_required]
|
|
1572
|
-
if not scoped:
|
|
1573
|
-
continue
|
|
1574
|
-
return min(scoped, key=lambda item: item[1])[2]
|
|
1575
|
-
return None
|
|
1870
|
+
def _select_candidate_evaluation(
|
|
1871
|
+
evaluations: list[CandidateEvaluation],
|
|
1872
|
+
) -> CandidateEvaluation | None:
|
|
1873
|
+
if not evaluations:
|
|
1874
|
+
return None
|
|
1576
1875
|
|
|
1876
|
+
scoped = list(evaluations)
|
|
1877
|
+
scoped = _prefer_true(scoped, key=lambda evaluation: evaluation.clearance_ok)
|
|
1878
|
+
scoped = _prefer_true(scoped, key=lambda evaluation: evaluation.shared_local)
|
|
1879
|
+
scoped = _prefer_true(scoped, key=lambda evaluation: evaluation.clean_direct)
|
|
1880
|
+
scoped = _prefer_true(scoped, key=lambda evaluation: evaluation.endpoint_preferred)
|
|
1881
|
+
scoped = _prefer_zero_int(scoped, key=lambda evaluation: evaluation.collisions)
|
|
1882
|
+
scoped = _prefer_zero_int(scoped, key=lambda evaluation: evaluation.edge_crossings)
|
|
1883
|
+
scoped = _prefer_zero_float(scoped, key=lambda evaluation: evaluation.edge_overlap_length)
|
|
1884
|
+
scoped = _prefer_min_int(scoped, key=lambda evaluation: evaluation.kind_priority)
|
|
1885
|
+
scoped = _prefer_min_float(scoped, key=lambda evaluation: evaluation.backwards)
|
|
1886
|
+
scoped = _prefer_min_int(scoped, key=lambda evaluation: evaluation.bends)
|
|
1887
|
+
scoped = _prefer_min_float(scoped, key=lambda evaluation: evaluation.length)
|
|
1888
|
+
scoped = _prefer_min_int(scoped, key=lambda evaluation: evaluation.candidate_index)
|
|
1889
|
+
return scoped[0]
|
|
1890
|
+
|
|
1891
|
+
|
|
1892
|
+
def _select_point_route_evaluation(
|
|
1893
|
+
evaluations: list[PointRouteEvaluation],
|
|
1894
|
+
) -> PointRouteEvaluation | None:
|
|
1895
|
+
if not evaluations:
|
|
1896
|
+
return None
|
|
1897
|
+
|
|
1898
|
+
scoped = list(evaluations)
|
|
1899
|
+
scoped = _prefer_true(scoped, key=lambda evaluation: evaluation.clearance_ok)
|
|
1900
|
+
scoped = _prefer_true(scoped, key=lambda evaluation: evaluation.shared_local)
|
|
1901
|
+
scoped = _prefer_zero_int(scoped, key=lambda evaluation: evaluation.edge_crossings)
|
|
1902
|
+
scoped = _prefer_zero_float(scoped, key=lambda evaluation: evaluation.edge_overlap_length)
|
|
1903
|
+
scoped = _prefer_min_int(scoped, key=lambda evaluation: evaluation.bends)
|
|
1904
|
+
scoped = _prefer_min_float(scoped, key=lambda evaluation: evaluation.length)
|
|
1905
|
+
scoped = _prefer_min_int(scoped, key=lambda evaluation: evaluation.slot)
|
|
1906
|
+
return scoped[0]
|
|
1907
|
+
|
|
1908
|
+
|
|
1909
|
+
def _prefer_true(items: list[SelectionItem], *, key) -> list[SelectionItem]:
|
|
1910
|
+
if any(key(item) for item in items):
|
|
1911
|
+
return [item for item in items if key(item)]
|
|
1912
|
+
return items
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
def _prefer_zero_int(items: list[SelectionItem], *, key) -> list[SelectionItem]:
|
|
1916
|
+
zero_items = [item for item in items if key(item) == 0]
|
|
1917
|
+
if zero_items:
|
|
1918
|
+
return zero_items
|
|
1919
|
+
return _prefer_min_int(items, key=key)
|
|
1920
|
+
|
|
1921
|
+
|
|
1922
|
+
def _prefer_zero_float(items: list[SelectionItem], *, key) -> list[SelectionItem]:
|
|
1923
|
+
zero_items = [item for item in items if key(item) <= EPSILON]
|
|
1924
|
+
if zero_items:
|
|
1925
|
+
return zero_items
|
|
1926
|
+
return _prefer_min_float(items, key=key)
|
|
1577
1927
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
)
|
|
1581
|
-
for
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
return
|
|
1928
|
+
|
|
1929
|
+
def _prefer_min_int(items: list[SelectionItem], *, key) -> list[SelectionItem]:
|
|
1930
|
+
best = min(key(item) for item in items)
|
|
1931
|
+
return [item for item in items if key(item) == best]
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
def _prefer_min_float(items: list[SelectionItem], *, key) -> list[SelectionItem]:
|
|
1935
|
+
best = min(key(item) for item in items)
|
|
1936
|
+
return [item for item in items if key(item) <= best + EPSILON]
|
|
1937
|
+
|
|
1938
|
+
|
|
1939
|
+
def _candidate_uses_shared_local(kind: str) -> bool:
|
|
1940
|
+
return kind.startswith("shared_")
|
|
1941
|
+
|
|
1942
|
+
|
|
1943
|
+
def _forward_endpoint_preferred(
|
|
1944
|
+
candidate: CandidatePath,
|
|
1945
|
+
*,
|
|
1946
|
+
source_node: LayoutNode,
|
|
1947
|
+
target_node: LayoutNode,
|
|
1948
|
+
outgoing_count: int,
|
|
1949
|
+
incoming_count: int,
|
|
1950
|
+
routing_groups: RoutingGroups | None,
|
|
1951
|
+
) -> bool:
|
|
1952
|
+
if _relevant_group_bounds_for_optional_routing_groups(
|
|
1953
|
+
source_node.node.id,
|
|
1954
|
+
target_node.node.id,
|
|
1955
|
+
routing_groups,
|
|
1956
|
+
):
|
|
1957
|
+
return True
|
|
1958
|
+
|
|
1959
|
+
fanout_context = outgoing_count > 1 and incoming_count <= 1
|
|
1960
|
+
fanin_context = incoming_count > 1 and outgoing_count <= 1
|
|
1961
|
+
preferred = True
|
|
1962
|
+
|
|
1963
|
+
if fanout_context:
|
|
1964
|
+
preferred = preferred and candidate.points[0].x == source_node.right
|
|
1965
|
+
if fanin_context:
|
|
1966
|
+
preferred = preferred and candidate.points[-1].x == target_node.x
|
|
1967
|
+
|
|
1968
|
+
return preferred
|
|
1969
|
+
|
|
1970
|
+
|
|
1971
|
+
def _forward_clean_direct_preferred(
|
|
1972
|
+
kind: str,
|
|
1973
|
+
candidate: CandidatePath,
|
|
1974
|
+
*,
|
|
1975
|
+
source_node: LayoutNode,
|
|
1976
|
+
target_node: LayoutNode,
|
|
1977
|
+
collisions: int,
|
|
1978
|
+
interactions: InteractionMetrics,
|
|
1979
|
+
) -> bool:
|
|
1980
|
+
if kind != "direct_elbow":
|
|
1981
|
+
return False
|
|
1982
|
+
if source_node.order >= target_node.order:
|
|
1983
|
+
return False
|
|
1984
|
+
if collisions != 0:
|
|
1985
|
+
return False
|
|
1986
|
+
if interactions.edge_crossings != 0:
|
|
1987
|
+
return False
|
|
1988
|
+
if interactions.edge_overlap_length > EPSILON:
|
|
1989
|
+
return False
|
|
1990
|
+
if _backwards_distance(candidate.points) > EPSILON:
|
|
1991
|
+
return False
|
|
1992
|
+
return _bend_count(candidate.points) == 1
|
|
1587
1993
|
|
|
1588
1994
|
|
|
1589
1995
|
def _route_metrics(
|
|
@@ -1694,10 +2100,17 @@ def _obstacle_group_bounds(
|
|
|
1694
2100
|
source_node_id: str,
|
|
1695
2101
|
target_node_id: str,
|
|
1696
2102
|
routing_groups: RoutingGroups | None,
|
|
2103
|
+
*,
|
|
2104
|
+
self_loop: bool = False,
|
|
1697
2105
|
) -> tuple[Bounds, ...]:
|
|
1698
2106
|
if routing_groups is None:
|
|
1699
2107
|
return ()
|
|
1700
|
-
return
|
|
2108
|
+
return _relevant_group_bounds(
|
|
2109
|
+
source_node_id,
|
|
2110
|
+
target_node_id,
|
|
2111
|
+
routing_groups,
|
|
2112
|
+
self_loop=self_loop,
|
|
2113
|
+
)
|
|
1701
2114
|
|
|
1702
2115
|
|
|
1703
2116
|
def _segment_border_interactions(
|
|
@@ -1737,107 +2150,32 @@ def _segment_border_interactions(
|
|
|
1737
2150
|
return 0.0, 1
|
|
1738
2151
|
|
|
1739
2152
|
|
|
1740
|
-
def _forward_priority_key(
|
|
1741
|
-
*,
|
|
1742
|
-
kind: str,
|
|
1743
|
-
candidate: CandidatePath,
|
|
1744
|
-
candidate_index: int,
|
|
1745
|
-
source_node: LayoutNode,
|
|
1746
|
-
target_node: LayoutNode,
|
|
1747
|
-
nodes: dict[str, LayoutNode],
|
|
1748
|
-
outgoing_count: int,
|
|
1749
|
-
incoming_count: int,
|
|
1750
|
-
theme: "Theme",
|
|
1751
|
-
has_relevant_groups: bool,
|
|
1752
|
-
interactions: InteractionMetrics,
|
|
1753
|
-
) -> tuple[int, int, int, int, float, float, int, float, int, float, int]:
|
|
1754
|
-
collisions, backwards, bends, length = _route_metrics(
|
|
1755
|
-
candidate,
|
|
1756
|
-
nodes=nodes,
|
|
1757
|
-
ignored_node_ids={source_node.node.id, target_node.node.id},
|
|
1758
|
-
theme=theme,
|
|
1759
|
-
)
|
|
1760
|
-
return (
|
|
1761
|
-
interactions.edge_crossings,
|
|
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
|
-
),
|
|
1773
|
-
_forward_side_change_penalty(kind, has_relevant_groups=has_relevant_groups),
|
|
1774
|
-
interactions.edge_overlap_length,
|
|
1775
|
-
interactions.obstacle_overlap_length,
|
|
1776
|
-
interactions.obstacle_crossings,
|
|
1777
|
-
_forward_kind_priority(
|
|
1778
|
-
kind,
|
|
1779
|
-
outgoing_count=outgoing_count,
|
|
1780
|
-
incoming_count=incoming_count,
|
|
1781
|
-
has_relevant_groups=has_relevant_groups,
|
|
1782
|
-
),
|
|
1783
|
-
backwards,
|
|
1784
|
-
bends,
|
|
1785
|
-
length,
|
|
1786
|
-
candidate_index,
|
|
1787
|
-
)
|
|
1788
|
-
|
|
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
|
-
|
|
1814
2153
|
def _forward_kind_priority(
|
|
1815
2154
|
kind: str,
|
|
1816
2155
|
*,
|
|
1817
2156
|
outgoing_count: int,
|
|
1818
2157
|
incoming_count: int,
|
|
1819
|
-
has_relevant_groups: bool,
|
|
1820
2158
|
) -> int:
|
|
1821
2159
|
fanout_context = outgoing_count > 1 and incoming_count <= 1
|
|
1822
2160
|
fanin_context = incoming_count > 1 and outgoing_count <= 1
|
|
1823
2161
|
|
|
1824
2162
|
if kind == "direct":
|
|
1825
2163
|
return 0
|
|
1826
|
-
if kind == "
|
|
2164
|
+
if kind == "shared_row":
|
|
1827
2165
|
return 1
|
|
1828
|
-
if
|
|
2166
|
+
if kind == "direct_elbow":
|
|
1829
2167
|
return 2
|
|
1830
|
-
if fanout_context and kind == "
|
|
2168
|
+
if fanout_context and kind == "column_source":
|
|
1831
2169
|
return 3
|
|
2170
|
+
if fanout_context and kind == "row_source":
|
|
2171
|
+
return 4
|
|
1832
2172
|
if fanin_context and kind == "column_target":
|
|
1833
|
-
return 2
|
|
1834
|
-
if fanin_context and kind == "row_target":
|
|
1835
2173
|
return 3
|
|
1836
|
-
if kind
|
|
2174
|
+
if fanin_context and kind == "row_target":
|
|
1837
2175
|
return 4
|
|
1838
|
-
if kind in {"
|
|
2176
|
+
if kind in {"column_source", "column_target"}:
|
|
1839
2177
|
return 5
|
|
1840
|
-
if kind
|
|
2178
|
+
if kind in {"row_source", "row_target"}:
|
|
1841
2179
|
return 6
|
|
1842
2180
|
if kind == "outer_row":
|
|
1843
2181
|
return 7
|
|
@@ -1846,59 +2184,23 @@ def _forward_kind_priority(
|
|
|
1846
2184
|
return 9
|
|
1847
2185
|
|
|
1848
2186
|
|
|
1849
|
-
def _forward_side_change_penalty(kind: str, *, has_relevant_groups: bool) -> int:
|
|
1850
|
-
if kind in {"row_source", "row_target", "outer_row"}:
|
|
1851
|
-
return 1
|
|
1852
|
-
if kind == "direct_elbow" and not has_relevant_groups:
|
|
1853
|
-
return 1
|
|
1854
|
-
return 0
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
def _edge_join_priority_key(
|
|
1858
|
-
*,
|
|
1859
|
-
kind: str,
|
|
1860
|
-
candidate: CandidatePath,
|
|
1861
|
-
candidate_index: int,
|
|
1862
|
-
source_node: LayoutNode,
|
|
1863
|
-
target_node: LayoutNode,
|
|
1864
|
-
nodes: dict[str, LayoutNode],
|
|
1865
|
-
theme: "Theme",
|
|
1866
|
-
direction: str,
|
|
1867
|
-
interactions: InteractionMetrics,
|
|
1868
|
-
) -> tuple[int, int, int, float, float, int, float, int, float, int]:
|
|
1869
|
-
collisions, backwards, bends, length = _route_metrics(
|
|
1870
|
-
candidate,
|
|
1871
|
-
nodes=nodes,
|
|
1872
|
-
ignored_node_ids={source_node.node.id, target_node.node.id},
|
|
1873
|
-
theme=theme,
|
|
1874
|
-
)
|
|
1875
|
-
return (
|
|
1876
|
-
interactions.edge_crossings,
|
|
1877
|
-
collisions,
|
|
1878
|
-
_edge_join_kind_priority(kind, direction=direction),
|
|
1879
|
-
interactions.edge_overlap_length,
|
|
1880
|
-
interactions.obstacle_overlap_length,
|
|
1881
|
-
interactions.obstacle_crossings,
|
|
1882
|
-
backwards,
|
|
1883
|
-
bends,
|
|
1884
|
-
length,
|
|
1885
|
-
candidate_index,
|
|
1886
|
-
)
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
2187
|
def _edge_join_kind_priority(kind: str, *, direction: str) -> int:
|
|
1890
2188
|
if kind == "direct_join":
|
|
1891
2189
|
return 0
|
|
1892
|
-
if kind in {"
|
|
2190
|
+
if kind in {"shared_row_join", "shared_column_join"}:
|
|
1893
2191
|
return 1
|
|
1894
|
-
if kind == "
|
|
2192
|
+
if kind == "shared_join":
|
|
1895
2193
|
return 2
|
|
1896
|
-
if kind in {"
|
|
2194
|
+
if kind in {"row_join", "column_join"}:
|
|
1897
2195
|
return 3
|
|
1898
|
-
if kind == "
|
|
2196
|
+
if kind == "lane_join":
|
|
1899
2197
|
return 4
|
|
2198
|
+
if kind in {"outer_row_join", "outer_column_join"}:
|
|
2199
|
+
return 5
|
|
2200
|
+
if kind == "outer_join":
|
|
2201
|
+
return 6
|
|
1900
2202
|
# Keep residual ordering deterministic if new kinds are added later.
|
|
1901
|
-
return
|
|
2203
|
+
return 7 if direction == "forward" else 8
|
|
1902
2204
|
|
|
1903
2205
|
|
|
1904
2206
|
def _candidate_conflicts(candidate: CandidatePath, occupancy: Occupancy) -> bool:
|
|
@@ -2101,33 +2403,26 @@ def _repair_forward_conflicts(
|
|
|
2101
2403
|
current_key = (
|
|
2102
2404
|
current_interactions.edge_crossings,
|
|
2103
2405
|
current_interactions.edge_overlap_length,
|
|
2104
|
-
current_interactions.obstacle_overlap_length,
|
|
2105
|
-
current_interactions.obstacle_crossings,
|
|
2106
2406
|
_path_length(route.points),
|
|
2107
2407
|
)
|
|
2108
|
-
if current_key[:
|
|
2408
|
+
if current_key[:2] == (0, 0.0):
|
|
2109
2409
|
continue
|
|
2110
2410
|
|
|
2111
2411
|
current_source_side = _point_side_on_node(route.points[0], source_node)
|
|
2112
2412
|
current_target_side = _point_side_on_node(route.points[-1], target_node)
|
|
2113
2413
|
geometry = component_geometry[source_node.component_id]
|
|
2114
|
-
candidate_evaluations: list[
|
|
2414
|
+
candidate_evaluations: list[CandidateEvaluation] = []
|
|
2115
2415
|
|
|
2116
|
-
for candidate in _same_side_forward_candidates(
|
|
2416
|
+
for kind, candidate in _same_side_forward_candidates(
|
|
2117
2417
|
source_node=source_node,
|
|
2118
2418
|
target_node=target_node,
|
|
2119
2419
|
geometry=geometry,
|
|
2120
2420
|
pair_offset=pair_offsets.get(edge.id, 0.0),
|
|
2121
2421
|
current_source_side=current_source_side,
|
|
2122
2422
|
current_target_side=current_target_side,
|
|
2423
|
+
routing_groups=routing_groups,
|
|
2123
2424
|
theme=validated.theme,
|
|
2124
2425
|
):
|
|
2125
|
-
clearance_ok = _path_respects_group_clearance(
|
|
2126
|
-
candidate.points,
|
|
2127
|
-
source_node.node.id,
|
|
2128
|
-
target_node.node.id,
|
|
2129
|
-
routing_groups,
|
|
2130
|
-
)
|
|
2131
2426
|
interactions = _candidate_interaction_metrics(
|
|
2132
2427
|
candidate,
|
|
2133
2428
|
occupancy=occupancy,
|
|
@@ -2135,30 +2430,37 @@ def _repair_forward_conflicts(
|
|
|
2135
2430
|
target_node_id=target_node.node.id,
|
|
2136
2431
|
routing_groups=routing_groups,
|
|
2137
2432
|
)
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2433
|
+
candidate_evaluations.append(
|
|
2434
|
+
_build_forward_candidate_evaluation(
|
|
2435
|
+
kind=kind,
|
|
2436
|
+
candidate=candidate,
|
|
2437
|
+
candidate_index=len(candidate_evaluations),
|
|
2438
|
+
source_node=source_node,
|
|
2439
|
+
target_node=target_node,
|
|
2440
|
+
nodes=nodes,
|
|
2441
|
+
outgoing_count=0,
|
|
2442
|
+
incoming_count=0,
|
|
2443
|
+
theme=validated.theme,
|
|
2444
|
+
interactions=interactions,
|
|
2445
|
+
routing_groups=routing_groups,
|
|
2446
|
+
)
|
|
2144
2447
|
)
|
|
2145
|
-
candidate_evaluations.append((clearance_ok, candidate_key, candidate))
|
|
2146
2448
|
|
|
2147
|
-
replacement =
|
|
2449
|
+
replacement = _select_candidate_evaluation(candidate_evaluations)
|
|
2148
2450
|
if replacement is None:
|
|
2149
2451
|
continue
|
|
2150
2452
|
|
|
2151
|
-
replacement_key =
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2453
|
+
replacement_key = (
|
|
2454
|
+
replacement.edge_crossings,
|
|
2455
|
+
replacement.edge_overlap_length,
|
|
2456
|
+
replacement.length,
|
|
2155
2457
|
)
|
|
2156
2458
|
if replacement_key >= current_key:
|
|
2157
2459
|
continue
|
|
2158
2460
|
|
|
2159
|
-
route.points = replacement.points
|
|
2461
|
+
route.points = replacement.candidate.points
|
|
2160
2462
|
route.bounds = _bounds_for_points(
|
|
2161
|
-
replacement.points,
|
|
2463
|
+
replacement.candidate.points,
|
|
2162
2464
|
validated.theme.stroke_width,
|
|
2163
2465
|
validated.theme.arrow_size,
|
|
2164
2466
|
badge_radius=route.join_badge_radius,
|
|
@@ -2177,40 +2479,31 @@ def _same_side_forward_candidates(
|
|
|
2177
2479
|
pair_offset: float,
|
|
2178
2480
|
current_source_side: str,
|
|
2179
2481
|
current_target_side: str,
|
|
2482
|
+
routing_groups: RoutingGroups | None,
|
|
2180
2483
|
theme: "Theme",
|
|
2181
|
-
) -> tuple[CandidatePath, ...]:
|
|
2182
|
-
candidates: list[CandidatePath] = []
|
|
2484
|
+
) -> tuple[tuple[str, CandidatePath], ...]:
|
|
2485
|
+
candidates: list[tuple[str, CandidatePath]] = []
|
|
2183
2486
|
|
|
2184
|
-
for
|
|
2487
|
+
for kind, candidate in _build_forward_candidates(
|
|
2185
2488
|
source_node=source_node,
|
|
2186
2489
|
target_node=target_node,
|
|
2187
2490
|
geometry=geometry,
|
|
2188
2491
|
pair_offset=pair_offset,
|
|
2492
|
+
routing_groups=routing_groups,
|
|
2189
2493
|
theme=theme,
|
|
2190
2494
|
):
|
|
2191
2495
|
if _point_side_on_node(candidate.points[0], source_node) != current_source_side:
|
|
2192
2496
|
continue
|
|
2193
2497
|
if _point_side_on_node(candidate.points[-1], target_node) != current_target_side:
|
|
2194
2498
|
continue
|
|
2195
|
-
candidates.append(candidate)
|
|
2499
|
+
candidates.append((kind, candidate))
|
|
2196
2500
|
|
|
2197
|
-
unique: dict[tuple[Point, ...], CandidatePath] = {}
|
|
2198
|
-
for candidate in candidates:
|
|
2199
|
-
unique.setdefault(candidate.points, candidate)
|
|
2501
|
+
unique: dict[tuple[Point, ...], tuple[str, CandidatePath]] = {}
|
|
2502
|
+
for kind, candidate in candidates:
|
|
2503
|
+
unique.setdefault(candidate.points, (kind, candidate))
|
|
2200
2504
|
return tuple(unique.values())
|
|
2201
2505
|
|
|
2202
2506
|
|
|
2203
|
-
def _select_repair_candidate(
|
|
2204
|
-
evaluations: list[tuple[bool, tuple[int, float, float, int, float], CandidatePath]],
|
|
2205
|
-
) -> CandidatePath | None:
|
|
2206
|
-
for clearance_required in (True, False):
|
|
2207
|
-
scoped = [item for item in evaluations if item[0] is clearance_required]
|
|
2208
|
-
if not scoped:
|
|
2209
|
-
continue
|
|
2210
|
-
return min(scoped, key=lambda item: item[1])[2]
|
|
2211
|
-
return None
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
2507
|
def _separate_overlapping_endpoints(
|
|
2215
2508
|
validated: "ValidatedPipeline | ValidatedDetailPanel",
|
|
2216
2509
|
nodes: dict[str, LayoutNode],
|
|
@@ -2891,7 +3184,8 @@ def _select_back_edge_route(
|
|
|
2891
3184
|
routing_groups: RoutingGroups | None,
|
|
2892
3185
|
theme: "Theme",
|
|
2893
3186
|
) -> tuple[Point, ...]:
|
|
2894
|
-
evaluations: list[
|
|
3187
|
+
evaluations: list[PointRouteEvaluation] = []
|
|
3188
|
+
shared_local = _shared_group_frame(source_node.node.id, target_node.node.id, routing_groups) is not None
|
|
2895
3189
|
|
|
2896
3190
|
for slot in range(base_slot, base_slot + 8):
|
|
2897
3191
|
points = _route_back_edge(
|
|
@@ -2916,18 +3210,22 @@ def _select_back_edge_route(
|
|
|
2916
3210
|
target_node_id=target_node.node.id,
|
|
2917
3211
|
routing_groups=routing_groups,
|
|
2918
3212
|
)
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
3213
|
+
evaluations.append(
|
|
3214
|
+
PointRouteEvaluation(
|
|
3215
|
+
clearance_ok=clearance_ok,
|
|
3216
|
+
shared_local=shared_local,
|
|
3217
|
+
edge_crossings=interactions.edge_crossings,
|
|
3218
|
+
edge_overlap_length=interactions.edge_overlap_length,
|
|
3219
|
+
bends=_bend_count(points),
|
|
3220
|
+
length=_path_length(points),
|
|
3221
|
+
slot=slot,
|
|
3222
|
+
points=points,
|
|
3223
|
+
)
|
|
2925
3224
|
)
|
|
2926
|
-
evaluations.append((clearance_ok, priority_key, points))
|
|
2927
3225
|
|
|
2928
|
-
selected =
|
|
3226
|
+
selected = _select_point_route_evaluation(evaluations)
|
|
2929
3227
|
if selected is not None:
|
|
2930
|
-
return selected
|
|
3228
|
+
return selected.points
|
|
2931
3229
|
raise AssertionError("No back-edge route found.")
|
|
2932
3230
|
|
|
2933
3231
|
|
|
@@ -2941,7 +3239,8 @@ def _select_self_loop_route(
|
|
|
2941
3239
|
routing_groups: RoutingGroups | None,
|
|
2942
3240
|
theme: "Theme",
|
|
2943
3241
|
) -> tuple[Point, ...]:
|
|
2944
|
-
evaluations: list[
|
|
3242
|
+
evaluations: list[PointRouteEvaluation] = []
|
|
3243
|
+
shared_local = _shared_group_frame(source_node.node.id, source_node.node.id, routing_groups) is not None
|
|
2945
3244
|
|
|
2946
3245
|
for slot in range(base_slot, base_slot + 8):
|
|
2947
3246
|
points = _route_self_loop(
|
|
@@ -2966,18 +3265,22 @@ def _select_self_loop_route(
|
|
|
2966
3265
|
target_node_id=source_node.node.id,
|
|
2967
3266
|
routing_groups=routing_groups,
|
|
2968
3267
|
)
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
3268
|
+
evaluations.append(
|
|
3269
|
+
PointRouteEvaluation(
|
|
3270
|
+
clearance_ok=clearance_ok,
|
|
3271
|
+
shared_local=shared_local,
|
|
3272
|
+
edge_crossings=interactions.edge_crossings,
|
|
3273
|
+
edge_overlap_length=interactions.edge_overlap_length,
|
|
3274
|
+
bends=_bend_count(points),
|
|
3275
|
+
length=_path_length(points),
|
|
3276
|
+
slot=slot,
|
|
3277
|
+
points=points,
|
|
3278
|
+
)
|
|
2975
3279
|
)
|
|
2976
|
-
evaluations.append((clearance_ok, priority_key, points))
|
|
2977
3280
|
|
|
2978
|
-
selected =
|
|
3281
|
+
selected = _select_point_route_evaluation(evaluations)
|
|
2979
3282
|
if selected is not None:
|
|
2980
|
-
return selected
|
|
3283
|
+
return selected.points
|
|
2981
3284
|
|
|
2982
3285
|
raise AssertionError("No self-loop route found.")
|
|
2983
3286
|
|
|
@@ -176,6 +176,34 @@ def _build_generate_pipeline_fixture() -> Pipeline:
|
|
|
176
176
|
)
|
|
177
177
|
|
|
178
178
|
|
|
179
|
+
def _build_shared_group_join_fixture() -> Pipeline:
|
|
180
|
+
theme = Theme.research()
|
|
181
|
+
|
|
182
|
+
return Pipeline(
|
|
183
|
+
nodes=(
|
|
184
|
+
Node("feature", "Feature", width=220.0),
|
|
185
|
+
Node("gamma", "γ", fill="#E8F5E9", width=180.0),
|
|
186
|
+
Node("beta", "β", fill="#E8F5E9", width=180.0),
|
|
187
|
+
Node("output", "Output", width=180.0),
|
|
188
|
+
Node("density", "Cloud Density Estimation", fill="#FDECEC", width=220.0),
|
|
189
|
+
),
|
|
190
|
+
edges=(
|
|
191
|
+
Edge("e_main", "feature", "output"),
|
|
192
|
+
Edge("e_gamma", "gamma", "e_main", merge_symbol="x"),
|
|
193
|
+
Edge("e_beta", "beta", "e_main", merge_symbol="+"),
|
|
194
|
+
Edge("e_density_gamma", "density", "gamma", color="#DD8899", dashed=True),
|
|
195
|
+
Edge("e_density_beta", "density", "beta", color="#DD8899", dashed=True),
|
|
196
|
+
),
|
|
197
|
+
groups=(
|
|
198
|
+
Group("g_outer", "Density Modulation", ("feature", "gamma", "beta", "density", "output")),
|
|
199
|
+
Group("g_adain", "AdaIN", ("feature", "gamma", "beta", "output")),
|
|
200
|
+
Group("g_scale", "Scale / Shift", ("gamma", "beta")),
|
|
201
|
+
Group("g_density", "Density", ("density",)),
|
|
202
|
+
),
|
|
203
|
+
theme=theme,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
179
207
|
def _make_layout_node(
|
|
180
208
|
node_id: str,
|
|
181
209
|
*,
|
|
@@ -923,6 +951,27 @@ def test_generate_pipeline_script_prefers_clean_single_bend_guidance_elbow() ->
|
|
|
923
951
|
assert points[-1] == Point(target.center_x, target.y)
|
|
924
952
|
|
|
925
953
|
|
|
954
|
+
def test_shared_group_join_prefers_local_header_lane_over_global_detour() -> None:
|
|
955
|
+
layout = build_layout(_build_shared_group_join_fixture())
|
|
956
|
+
routed = {edge.edge.id: edge for edge in layout.main.edges}
|
|
957
|
+
overlays = {overlay.group.id: overlay.bounds for overlay in layout.main.groups}
|
|
958
|
+
adain = overlays["g_adain"]
|
|
959
|
+
outer = overlays["g_outer"]
|
|
960
|
+
member_top = min(layout.main.nodes[node_id].y for node_id in ("feature", "gamma", "beta", "output"))
|
|
961
|
+
|
|
962
|
+
main_lane_y = min(point.y for point in routed["e_main"].points)
|
|
963
|
+
assert adain.y < main_lane_y < member_top
|
|
964
|
+
assert outer.y <= main_lane_y
|
|
965
|
+
|
|
966
|
+
gamma_join = routed["e_gamma"]
|
|
967
|
+
beta_join = routed["e_beta"]
|
|
968
|
+
assert gamma_join.join_point is not None
|
|
969
|
+
assert beta_join.join_point is not None
|
|
970
|
+
assert adain.y < gamma_join.join_point.y < member_top
|
|
971
|
+
assert adain.y < beta_join.join_point.y < member_top
|
|
972
|
+
assert max(point.x for point in beta_join.points) <= adain.right + 0.01
|
|
973
|
+
|
|
974
|
+
|
|
926
975
|
def test_back_edge_leaves_group_before_bending() -> None:
|
|
927
976
|
pipeline = Pipeline(
|
|
928
977
|
nodes=[
|
|
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
|
|
File without changes
|