nodebpy 520.25.0__tar.gz → 520.26.0__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.
- {nodebpy-520.25.0 → nodebpy-520.26.0}/PKG-INFO +1 -1
- {nodebpy-520.25.0 → nodebpy-520.26.0}/pyproject.toml +1 -1
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/assets/_library.py +41 -1
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/assets/_pipeline.py +12 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/builder/layout.py +29 -1
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/export/plot.py +81 -32
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/VENDORED.md +17 -0
- nodebpy-520.26.0/src/nodebpy/lib/nodearrange/arrange/balancing.py +182 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +66 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +7 -1
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +4 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +15 -2
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/config.py +17 -1
- {nodebpy-520.25.0 → nodebpy-520.26.0}/README.md +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/__init__.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/assets/__init__.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/assets/__main__.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/assets/_codegen.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/builder/__init__.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/builder/_registry.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/builder/_socket_order.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/builder/_utils.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/builder/accessor.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/builder/asset.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/builder/items.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/builder/mixins.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/builder/node.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/builder/socket.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/builder/tree.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/export/__init__.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/export/codegen.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/export/diagram.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/export/parity.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/export/web_render.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/__init__.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/_mixins.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/compositor/assets.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/compositor/color.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/compositor/group.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/compositor/input.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/compositor/output.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/assets.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/color.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/group.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/input.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/manual.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/output.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/shader/assets.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/shader/color.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/shader/converter.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/shader/grid.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/shader/group.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/shader/input.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/shader/manual.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/shader/output.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/shader/script.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/shader/shader.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/shader/texture.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/nodes/shader/vector.py +0 -0
- {nodebpy-520.25.0 → nodebpy-520.26.0}/src/nodebpy/types.py +0 -0
|
@@ -1406,7 +1406,7 @@ def _add_arrangement_flags(parser, description: str) -> None: # pragma: no cove
|
|
|
1406
1406
|
choices=["LEFT_DOWN", "RIGHT_DOWN", "LEFT_UP", "RIGHT_UP", "BALANCED"],
|
|
1407
1407
|
help=(
|
|
1408
1408
|
"Direction of layout — which corner nodes align towards, or "
|
|
1409
|
-
"'balanced' to even out the four extremes (default:
|
|
1409
|
+
"'balanced' to even out the four extremes (default: balanced)."
|
|
1410
1410
|
),
|
|
1411
1411
|
)
|
|
1412
1412
|
layout.add_argument(
|
|
@@ -1442,6 +1442,38 @@ def _add_arrangement_flags(parser, description: str) -> None: # pragma: no cove
|
|
|
1442
1442
|
action="store_true",
|
|
1443
1443
|
help="Fit the widths of collapsed nodes to their display name.",
|
|
1444
1444
|
)
|
|
1445
|
+
layout.add_argument(
|
|
1446
|
+
"--no-sequential-frames",
|
|
1447
|
+
dest="sequential_frames",
|
|
1448
|
+
action="store_false",
|
|
1449
|
+
help=(
|
|
1450
|
+
"Do not rank frames as stages of the flow (by default every node "
|
|
1451
|
+
"of a frame comes after every node of the frame feeding it, so "
|
|
1452
|
+
"frames line up left to right instead of stacking)."
|
|
1453
|
+
),
|
|
1454
|
+
)
|
|
1455
|
+
layout.add_argument(
|
|
1456
|
+
"--no-balance-heights",
|
|
1457
|
+
dest="balance_heights",
|
|
1458
|
+
action="store_false",
|
|
1459
|
+
help=(
|
|
1460
|
+
"Do not promote feeder chains into emptier columns to shorten the "
|
|
1461
|
+
"tallest column."
|
|
1462
|
+
),
|
|
1463
|
+
)
|
|
1464
|
+
layout.add_argument(
|
|
1465
|
+
"--balance-aspect",
|
|
1466
|
+
type=float,
|
|
1467
|
+
help=("Width-to-height ratio the height balancing aims for (default: 1.6)."),
|
|
1468
|
+
)
|
|
1469
|
+
layout.add_argument(
|
|
1470
|
+
"--reroute-margin-y-fac",
|
|
1471
|
+
type=float,
|
|
1472
|
+
help=(
|
|
1473
|
+
"Fraction of the vertical spacing kept between consecutive "
|
|
1474
|
+
"reroutes in a column (default: 0.35)."
|
|
1475
|
+
),
|
|
1476
|
+
)
|
|
1445
1477
|
|
|
1446
1478
|
|
|
1447
1479
|
def _arrange_options_from_args(args) -> SugiyamaOptions | None:
|
|
@@ -1466,6 +1498,14 @@ def _arrange_options_from_args(args) -> SugiyamaOptions | None:
|
|
|
1466
1498
|
overrides["stack_margin_y_fac"] = args.stack_margin_y_fac
|
|
1467
1499
|
if args.optimize_sizes:
|
|
1468
1500
|
overrides["optimize_sizes"] = True
|
|
1501
|
+
if not args.sequential_frames:
|
|
1502
|
+
overrides["sequential_frames"] = False
|
|
1503
|
+
if not args.balance_heights:
|
|
1504
|
+
overrides["balance_heights"] = False
|
|
1505
|
+
if args.balance_aspect is not None:
|
|
1506
|
+
overrides["balance_aspect"] = args.balance_aspect
|
|
1507
|
+
if args.reroute_margin_y_fac is not None:
|
|
1508
|
+
overrides["reroute_margin_y_fac"] = args.reroute_margin_y_fac
|
|
1469
1509
|
return SugiyamaOptions(**overrides) if overrides else None
|
|
1470
1510
|
|
|
1471
1511
|
|
|
@@ -97,6 +97,10 @@ _CONFIG_KEYS: dict[str, tuple[str | None, str]] = {
|
|
|
97
97
|
"no-stack-collapsed": ("stack_collapsed", "off"),
|
|
98
98
|
"stack-margin-y-fac": ("stack_margin_y_fac", "value"),
|
|
99
99
|
"optimize-sizes": ("optimize_sizes", "flag"),
|
|
100
|
+
"no-sequential-frames": ("sequential_frames", "off"),
|
|
101
|
+
"no-balance-heights": ("balance_heights", "off"),
|
|
102
|
+
"balance-aspect": ("balance_aspect", "value"),
|
|
103
|
+
"reroute-margin-y-fac": ("reroute_margin_y_fac", "value"),
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
# Which dest each positional config key fills, per subcommand: the dump
|
|
@@ -132,6 +136,10 @@ _OPTION_DEFAULTS: dict[str, object] = {
|
|
|
132
136
|
"stack_collapsed": True,
|
|
133
137
|
"stack_margin_y_fac": None,
|
|
134
138
|
"optimize_sizes": False,
|
|
139
|
+
"sequential_frames": True,
|
|
140
|
+
"balance_heights": True,
|
|
141
|
+
"balance_aspect": None,
|
|
142
|
+
"reroute_margin_y_fac": None,
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
# The resolved options the stamp fingerprint covers: everything, beyond the
|
|
@@ -140,6 +148,8 @@ _OPTION_DEFAULTS: dict[str, object] = {
|
|
|
140
148
|
# it gates the session check, not the output.
|
|
141
149
|
_STAMP_DESTS = (
|
|
142
150
|
"add_reroutes",
|
|
151
|
+
"balance_aspect",
|
|
152
|
+
"balance_heights",
|
|
143
153
|
"compress",
|
|
144
154
|
"direction",
|
|
145
155
|
"iterations",
|
|
@@ -149,6 +159,8 @@ _STAMP_DESTS = (
|
|
|
149
159
|
"nodebpy_pkg",
|
|
150
160
|
"on_missing",
|
|
151
161
|
"optimize_sizes",
|
|
162
|
+
"reroute_margin_y_fac",
|
|
163
|
+
"sequential_frames",
|
|
152
164
|
"snapshot_positions",
|
|
153
165
|
"socket_alignment",
|
|
154
166
|
"spacing",
|
|
@@ -871,13 +871,33 @@ class SugiyamaOptions:
|
|
|
871
871
|
Fit the widths of collapsed nodes to their display name.
|
|
872
872
|
iterations : int
|
|
873
873
|
Number of crossing-minimization iterations.
|
|
874
|
+
sequential_frames : bool
|
|
875
|
+
Rank frames as stages of the flow: every node of a frame comes
|
|
876
|
+
after every node of the frame (or intermediate node) feeding it, so
|
|
877
|
+
successive frames line up left to right instead of stacking into a
|
|
878
|
+
staircase. Frames with no links between them (parallel branches)
|
|
879
|
+
still share columns and stack vertically.
|
|
880
|
+
balance_heights : bool
|
|
881
|
+
Shorten the tallest column by moving nodes whose feeders serve only
|
|
882
|
+
them (a private upstream chain) one column left, while that makes
|
|
883
|
+
the drawing smaller overall. Counters the tall sliver a node with
|
|
884
|
+
many inputs otherwise produces, at the price of slightly longer
|
|
885
|
+
links routed through reroutes / dummy nodes.
|
|
886
|
+
balance_aspect : float
|
|
887
|
+
Width-to-height ratio the balancing aims for: it keeps promoting
|
|
888
|
+
feeders left while the drawing's bounding box (height, or width
|
|
889
|
+
divided by this ratio, whichever is larger) shrinks.
|
|
890
|
+
reroute_margin_y_fac : float
|
|
891
|
+
Fraction of the vertical margin kept between consecutive reroutes
|
|
892
|
+
(and the dummy nodes long links are routed through) in a column;
|
|
893
|
+
bundles of long links pack tighter than nodes.
|
|
874
894
|
"""
|
|
875
895
|
|
|
876
896
|
# Defaults calibrated against hand-approved node-arrange addon output
|
|
877
897
|
# ("30" x/y spacing, no socket alignment, top-right node alignment).
|
|
878
898
|
margin: tuple[float, float] = (30.0, 30.0)
|
|
879
899
|
direction: Literal["LEFT_DOWN", "RIGHT_DOWN", "BALANCED", "LEFT_UP", "RIGHT_UP"] = (
|
|
880
|
-
"
|
|
900
|
+
"BALANCED"
|
|
881
901
|
)
|
|
882
902
|
socket_alignment: Literal["NONE", "MODERATE", "FULL"] = "NONE"
|
|
883
903
|
add_reroutes: bool = False
|
|
@@ -886,6 +906,10 @@ class SugiyamaOptions:
|
|
|
886
906
|
stack_margin_y_fac: float = 0.5
|
|
887
907
|
optimize_sizes: bool = False
|
|
888
908
|
iterations: int = 50
|
|
909
|
+
sequential_frames: bool = True
|
|
910
|
+
balance_heights: bool = True
|
|
911
|
+
balance_aspect: float = 1.6
|
|
912
|
+
reroute_margin_y_fac: float = 0.35
|
|
889
913
|
|
|
890
914
|
|
|
891
915
|
type ArrangeMethod = (
|
|
@@ -959,6 +983,10 @@ def _arrange_sugiyama(tree: bpy.types.NodeTree, options: SugiyamaOptions) -> Non
|
|
|
959
983
|
stack_collapsed=options.stack_collapsed,
|
|
960
984
|
optimize_sizes=options.optimize_sizes,
|
|
961
985
|
stack_margin_y_fac=options.stack_margin_y_fac,
|
|
986
|
+
sequential_frames=options.sequential_frames,
|
|
987
|
+
balance_heights=options.balance_heights,
|
|
988
|
+
balance_aspect=options.balance_aspect,
|
|
989
|
+
reroute_margin_y_fac=options.reroute_margin_y_fac,
|
|
962
990
|
)
|
|
963
991
|
sugiyama.sugiyama_layout(tree, settings=settings, margin=Vector(options.margin))
|
|
964
992
|
|
|
@@ -1069,31 +1069,81 @@ def _draw_frames(cv: _Canvas, tree: bpy.types.NodeTree) -> None:
|
|
|
1069
1069
|
def _zone_members(
|
|
1070
1070
|
tree: bpy.types.NodeTree, start: bpy.types.Node, end: bpy.types.Node
|
|
1071
1071
|
) -> set[bpy.types.Node]:
|
|
1072
|
-
"""
|
|
1073
|
-
|
|
1072
|
+
"""The nodes Blender draws inside a zone: the zone's input and output
|
|
1073
|
+
nodes and every node fed, directly or through other nodes, from the
|
|
1074
|
+
zone input. A node that only feeds into the zone from outside (a group
|
|
1075
|
+
input, a constant, a value computed elsewhere) stays outside."""
|
|
1074
1076
|
forward: dict[bpy.types.Node, set[bpy.types.Node]] = {}
|
|
1075
|
-
backward: dict[bpy.types.Node, set[bpy.types.Node]] = {}
|
|
1076
1077
|
for link in tree.links:
|
|
1077
1078
|
if link.from_node is None or link.to_node is None:
|
|
1078
1079
|
continue
|
|
1079
1080
|
forward.setdefault(link.from_node, set()).add(link.to_node)
|
|
1080
|
-
backward.setdefault(link.to_node, set()).add(link.from_node)
|
|
1081
1081
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
return
|
|
1082
|
+
seen = {start, end}
|
|
1083
|
+
queue = deque([start])
|
|
1084
|
+
while queue:
|
|
1085
|
+
node = queue.popleft()
|
|
1086
|
+
# bpy hands out a fresh wrapper per access, so compare by value.
|
|
1087
|
+
if node == end:
|
|
1088
|
+
continue
|
|
1089
|
+
for nxt in forward.get(node, ()):
|
|
1090
|
+
if nxt not in seen:
|
|
1091
|
+
seen.add(nxt)
|
|
1092
|
+
queue.append(nxt)
|
|
1093
|
+
return seen
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _convex_hull(points: list[tuple[float, float]]) -> list[tuple[float, float]]:
|
|
1097
|
+
"""Counter-clockwise convex hull (monotone chain)."""
|
|
1098
|
+
pts = sorted(set(points))
|
|
1099
|
+
if len(pts) <= 2:
|
|
1100
|
+
return pts
|
|
1101
|
+
|
|
1102
|
+
def cross(o: tuple[float, float], a: tuple[float, float], b: tuple[float, float]):
|
|
1103
|
+
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
|
|
1104
|
+
|
|
1105
|
+
lower: list[tuple[float, float]] = []
|
|
1106
|
+
for pt in pts:
|
|
1107
|
+
while len(lower) >= 2 and cross(lower[-2], lower[-1], pt) <= 0:
|
|
1108
|
+
lower.pop()
|
|
1109
|
+
lower.append(pt)
|
|
1110
|
+
upper: list[tuple[float, float]] = []
|
|
1111
|
+
for pt in reversed(pts):
|
|
1112
|
+
while len(upper) >= 2 and cross(upper[-2], upper[-1], pt) <= 0:
|
|
1113
|
+
upper.pop()
|
|
1114
|
+
upper.append(pt)
|
|
1115
|
+
return lower[:-1] + upper[:-1]
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def _rounded_offset(
|
|
1119
|
+
hull: list[tuple[float, float]], radius: float, steps: int = 6
|
|
1120
|
+
) -> list[tuple[float, float]]:
|
|
1121
|
+
"""Outline of a convex polygon grown by *radius*: its edges pushed
|
|
1122
|
+
outwards, its corners rounded (the Minkowski sum with a disc)."""
|
|
1123
|
+
n = len(hull)
|
|
1124
|
+
if n == 1:
|
|
1125
|
+
cx, cy = hull[0]
|
|
1126
|
+
return [
|
|
1127
|
+
(cx + radius * math.cos(a), cy + radius * math.sin(a))
|
|
1128
|
+
for a in (2 * math.pi * k / (4 * steps) for k in range(4 * steps))
|
|
1129
|
+
]
|
|
1130
|
+
outline: list[tuple[float, float]] = []
|
|
1131
|
+
for i, (x, y) in enumerate(hull):
|
|
1132
|
+
(px, py), (nx, ny) = hull[i - 1], hull[(i + 1) % n]
|
|
1133
|
+
# Outward normals of the incoming and outgoing edges (hull is CCW).
|
|
1134
|
+
a_in = math.atan2(-(x - px), (y - py))
|
|
1135
|
+
a_out = math.atan2(-(nx - x), (ny - y))
|
|
1136
|
+
while a_out < a_in:
|
|
1137
|
+
a_out += 2 * math.pi
|
|
1138
|
+
for k in range(steps + 1):
|
|
1139
|
+
a = a_in + (a_out - a_in) * k / steps
|
|
1140
|
+
outline.append((x + radius * math.cos(a), y + radius * math.sin(a)))
|
|
1141
|
+
return outline
|
|
1094
1142
|
|
|
1095
1143
|
|
|
1096
1144
|
def _draw_zones(cv: _Canvas, tree: bpy.types.NodeTree) -> None:
|
|
1145
|
+
from matplotlib.patches import Polygon
|
|
1146
|
+
|
|
1097
1147
|
for node in tree.nodes:
|
|
1098
1148
|
attr = _ZONE_THEME_ATTR.get(node.bl_idname)
|
|
1099
1149
|
if attr is None:
|
|
@@ -1101,24 +1151,23 @@ def _draw_zones(cv: _Canvas, tree: bpy.types.NodeTree) -> None:
|
|
|
1101
1151
|
paired = getattr(node, "paired_output", None)
|
|
1102
1152
|
if paired is None:
|
|
1103
1153
|
continue
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
y1 = max(b[3] for b in bounds) + _ZONE_PADDING
|
|
1154
|
+
corners: list[tuple[float, float]] = []
|
|
1155
|
+
for member in _zone_members(tree, node, paired):
|
|
1156
|
+
x0, y0, x1, y1 = _node_bounds(member)
|
|
1157
|
+
corners += [(x0, y0), (x0, y1), (x1, y0), (x1, y1)]
|
|
1158
|
+
outline = _rounded_offset(_convex_hull(corners), _ZONE_PADDING)
|
|
1110
1159
|
color = cv.theme.zones[attr]
|
|
1111
1160
|
border = (color[0], color[1], color[2], 0.9)
|
|
1112
|
-
cv.
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1161
|
+
cv.ax.add_patch(
|
|
1162
|
+
Polygon(
|
|
1163
|
+
outline,
|
|
1164
|
+
closed=True,
|
|
1165
|
+
facecolor=color,
|
|
1166
|
+
edgecolor=border,
|
|
1167
|
+
lw=1.0,
|
|
1168
|
+
joinstyle="round",
|
|
1169
|
+
zorder=1.5,
|
|
1170
|
+
)
|
|
1122
1171
|
)
|
|
1123
1172
|
|
|
1124
1173
|
|
|
@@ -44,6 +44,23 @@ break headless operation.
|
|
|
44
44
|
working set is `list(ntree.nodes)` and the select gates are membership /
|
|
45
45
|
always-true checks. Upstream patches touching `.select` need the same
|
|
46
46
|
translation.
|
|
47
|
+
- **Layout readability additions** (nodebpy-only, each behind a `Settings`
|
|
48
|
+
flag mirrored on `SugiyamaOptions`):
|
|
49
|
+
- `ranking.add_frame_sequence_edges()` (`sequential_frames`): ranks
|
|
50
|
+
frames as stages of the flow by constraining every node of a frame to
|
|
51
|
+
come after every node of the frame (or intermediate node) feeding it, so
|
|
52
|
+
successive frames line up left to right instead of stacking into a
|
|
53
|
+
staircase.
|
|
54
|
+
- `balancing.balance_column_heights()` (`balance_heights`,
|
|
55
|
+
`balance_aspect`): after ranking, promotes nodes of the tallest columns
|
|
56
|
+
together with their upstream into emptier columns while the drawing
|
|
57
|
+
gets closer to a screen-shaped box.
|
|
58
|
+
- `y_coords.vertical_gap()` (`reroute_margin_y_fac`): consecutive
|
|
59
|
+
reroutes / dummy nodes in a column pack at a fraction of the margin.
|
|
60
|
+
- `sugiyama.precompute_links()` keeps `is_hidden` links (links into a
|
|
61
|
+
collapsed panel's sockets), which still order the nodes; and
|
|
62
|
+
`x_coords.assign_x_coords()` skips a column left empty by dissolved
|
|
63
|
+
dummies.
|
|
47
64
|
- `structs.py` uses explicit `_fields_` lists (upstream builds them from
|
|
48
65
|
annotations, formerly via `eval`) and additionally binds `bNode` /
|
|
49
66
|
`bNodeRuntime` / `rctf`, which upstream does not have.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
2
|
+
"""Height-balanced ranking (nodebpy addition, not in upstream node-arrange).
|
|
3
|
+
|
|
4
|
+
Network-simplex ranking minimises total edge length, so every feeder sits in
|
|
5
|
+
the column right before its consumer. A node with many inputs — a big group
|
|
6
|
+
node taking a dozen values, switches and math results — therefore gets all
|
|
7
|
+
of its feeders stacked in one column, which grows far taller than any other
|
|
8
|
+
column while the columns further left stay nearly empty: the tree becomes a
|
|
9
|
+
tall sliver with dead space everywhere else.
|
|
10
|
+
|
|
11
|
+
:func:`balance_column_heights` post-processes the ranks: while the tallest
|
|
12
|
+
column can be made shorter without making the drawing larger overall, it
|
|
13
|
+
moves a node of that column one column to the left together with
|
|
14
|
+
everything upstream of it, which is always feasible (the moved set has no
|
|
15
|
+
predecessor outside itself). Links from the moved set to the nodes left
|
|
16
|
+
behind become one column longer and are routed through dummy nodes /
|
|
17
|
+
reroutes, which the height estimate charges for.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from collections import defaultdict
|
|
23
|
+
from collections.abc import Iterable
|
|
24
|
+
|
|
25
|
+
import networkx as nx
|
|
26
|
+
|
|
27
|
+
from ..config import LayoutState
|
|
28
|
+
from ..utils import REROUTE_DIM
|
|
29
|
+
from .graph import FROM_SOCKET, Cluster, Kind, Node, Socket
|
|
30
|
+
|
|
31
|
+
_MAX_MOVES = 500
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _upstream(G: nx.DiGraph[Node], v: Node) -> set[Node]:
|
|
35
|
+
"""Every node *v* depends on. Moving *v* together with its upstream one
|
|
36
|
+
column to the left is always feasible: the set has no predecessor
|
|
37
|
+
outside itself, and links from it to nodes left behind only get
|
|
38
|
+
longer."""
|
|
39
|
+
return nx.ancestors(G, v)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _column_heights(
|
|
43
|
+
G: nx.DiGraph[Node], ranks: dict[Node, int], state: LayoutState
|
|
44
|
+
) -> dict[int, float]:
|
|
45
|
+
"""Estimated drawn height per rank: real nodes plus the dummy nodes of
|
|
46
|
+
the long edges passing through, each separated by the vertical margin.
|
|
47
|
+
Long edges leaving the same socket merge into one dummy chain."""
|
|
48
|
+
margin = state.margin.y
|
|
49
|
+
heights: defaultdict[int, float] = defaultdict(float)
|
|
50
|
+
counts: defaultdict[int, int] = defaultdict(int)
|
|
51
|
+
for v, r in ranks.items():
|
|
52
|
+
heights[r] += v.height
|
|
53
|
+
counts[r] += 1
|
|
54
|
+
crossings: defaultdict[int, set[Socket]] = defaultdict(set)
|
|
55
|
+
for u, w, from_socket in G.edges(data=FROM_SOCKET):
|
|
56
|
+
for r in range(ranks[u] + 1, ranks[w]):
|
|
57
|
+
crossings[r].add(from_socket)
|
|
58
|
+
dummy_gap = margin * state.settings.reroute_margin_y_fac
|
|
59
|
+
for r, sources in crossings.items():
|
|
60
|
+
# Dummy chains pack at the reduced reroute gap (see y_coords).
|
|
61
|
+
heights[r] += len(sources) * (REROUTE_DIM.y + dummy_gap)
|
|
62
|
+
return {r: heights[r] + margin * max(counts[r] - 1, 0) for r in heights}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _column_widths(ranks: dict[Node, int]) -> dict[int, float]:
|
|
66
|
+
widths: defaultdict[int, float] = defaultdict(float)
|
|
67
|
+
for v, r in ranks.items():
|
|
68
|
+
widths[r] = max(widths[r], v.width)
|
|
69
|
+
return widths
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _frame_sequence_ok(
|
|
73
|
+
ranks: dict[Node, int],
|
|
74
|
+
sequence: Iterable[tuple[frozenset[Node], frozenset[Node]]],
|
|
75
|
+
) -> bool:
|
|
76
|
+
"""Whether every recorded frame-sequence constraint (all of unit *a*
|
|
77
|
+
before all of unit *b*) still holds under *ranks*."""
|
|
78
|
+
for before, after in sequence:
|
|
79
|
+
if max(ranks[v] for v in before) >= min(ranks[v] for v in after):
|
|
80
|
+
return False
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _overshoot(heights: dict[int, float], target: float) -> float:
|
|
85
|
+
return sum(max(0.0, h - target) for h in heights.values())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _fit_to_height(
|
|
89
|
+
G: nx.DiGraph[Node],
|
|
90
|
+
ranks: dict[Node, int],
|
|
91
|
+
target: float,
|
|
92
|
+
state: LayoutState,
|
|
93
|
+
upstream_of: dict[Node, set[Node]],
|
|
94
|
+
) -> dict[Node, int] | None:
|
|
95
|
+
"""Greedily move nodes (with their upstream) left until no column is
|
|
96
|
+
taller than *target*; None when the greedy gets stuck first."""
|
|
97
|
+
ranks = dict(ranks)
|
|
98
|
+
sequence = state.frame_sequence
|
|
99
|
+
for _ in range(_MAX_MOVES):
|
|
100
|
+
heights = _column_heights(G, ranks, state)
|
|
101
|
+
current = _overshoot(heights, target)
|
|
102
|
+
if current <= 0:
|
|
103
|
+
return ranks
|
|
104
|
+
best: tuple[float, dict[Node, int]] | None = None
|
|
105
|
+
for v, upstream in upstream_of.items():
|
|
106
|
+
if not upstream or heights.get(ranks[v], 0.0) <= target:
|
|
107
|
+
continue
|
|
108
|
+
trial = dict(ranks)
|
|
109
|
+
for w in upstream | {v}:
|
|
110
|
+
trial[w] -= 1
|
|
111
|
+
if not _frame_sequence_ok(trial, sequence):
|
|
112
|
+
continue
|
|
113
|
+
shoot = _overshoot(_column_heights(G, trial, state), target)
|
|
114
|
+
if shoot < current and (best is None or shoot < best[0]):
|
|
115
|
+
best = (shoot, trial)
|
|
116
|
+
if best is None:
|
|
117
|
+
return None
|
|
118
|
+
ranks = best[1]
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
_TARGET_STEP = 0.9
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def balance_column_heights(
|
|
126
|
+
G: nx.DiGraph[Node],
|
|
127
|
+
clusters: Iterable[Cluster],
|
|
128
|
+
state: LayoutState,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Shorten the tallest columns by promoting feeder chains left.
|
|
131
|
+
|
|
132
|
+
Lowers a target height step by step from the current tallest column and
|
|
133
|
+
for each target greedily moves nodes of over-tall columns — together
|
|
134
|
+
with their upstream — one column to the left until every column
|
|
135
|
+
fits (or gives up). Of the layouts found, the one needing the smallest
|
|
136
|
+
screen-shaped box (``balance_aspect`` wide for every unit of height) is
|
|
137
|
+
kept, so height is traded for width only while the drawing gets closer
|
|
138
|
+
to that shape; the descent stops once fitting fails or the box starts
|
|
139
|
+
growing again. Frame-sequence constraints are kept throughout.
|
|
140
|
+
"""
|
|
141
|
+
nodes = [v for v in G if v.type != Kind.HORIZONTAL_BORDER]
|
|
142
|
+
if not nodes:
|
|
143
|
+
return
|
|
144
|
+
ranks = {v: v.rank for v in nodes}
|
|
145
|
+
margin_x = state.margin.x
|
|
146
|
+
upstream_of = {v: _upstream(G, v) for v in nodes}
|
|
147
|
+
|
|
148
|
+
aspect = state.settings.balance_aspect
|
|
149
|
+
|
|
150
|
+
def area(r: dict[Node, int]) -> float:
|
|
151
|
+
"""Size of the drawing as the side of the screen-shaped box (of the
|
|
152
|
+
target aspect ratio) it needs: the taller of its height and its
|
|
153
|
+
width scaled down by the aspect ratio."""
|
|
154
|
+
heights = _column_heights(G, r, state)
|
|
155
|
+
widths = _column_widths(r)
|
|
156
|
+
width = sum(widths.values()) + margin_x * max(len(widths) - 1, 0)
|
|
157
|
+
return max(max(heights.values()), width / aspect)
|
|
158
|
+
|
|
159
|
+
best_ranks = ranks
|
|
160
|
+
best_area = area(ranks)
|
|
161
|
+
target = max(_column_heights(G, ranks, state).values()) * _TARGET_STEP
|
|
162
|
+
floor = max(v.height for v in nodes)
|
|
163
|
+
while target >= floor:
|
|
164
|
+
fitted = _fit_to_height(G, best_ranks, target, state, upstream_of)
|
|
165
|
+
if fitted is None:
|
|
166
|
+
break
|
|
167
|
+
fitted_area = area(fitted)
|
|
168
|
+
if fitted_area > best_area:
|
|
169
|
+
break
|
|
170
|
+
best_ranks, best_area = fitted, fitted_area
|
|
171
|
+
target = max(_column_heights(G, fitted, state).values()) * _TARGET_STEP
|
|
172
|
+
|
|
173
|
+
offset = min(best_ranks.values())
|
|
174
|
+
for v in nodes:
|
|
175
|
+
v.rank = best_ranks[v] - offset
|
|
176
|
+
# Frame borders follow their members (recomputed by insert_dummy_nodes);
|
|
177
|
+
# keep them consistent for anything reading them before that.
|
|
178
|
+
for c in clusters:
|
|
179
|
+
members = [v for v in nodes if v.cluster is c]
|
|
180
|
+
if members:
|
|
181
|
+
c.left.rank = min(v.rank for v in members) - 1
|
|
182
|
+
c.right.rank = max(v.rank for v in members) + 1
|
|
@@ -27,9 +27,75 @@ def get_nesting_graph(CG: ClusterGraph) -> nx.MultiDiGraph[Node]:
|
|
|
27
27
|
else:
|
|
28
28
|
H.add_edges_from(((u.left, v.left), (v.right, u.right)))
|
|
29
29
|
|
|
30
|
+
if CG.state.settings.sequential_frames:
|
|
31
|
+
add_frame_sequence_edges(CG, H)
|
|
32
|
+
|
|
30
33
|
return H
|
|
31
34
|
|
|
32
35
|
|
|
36
|
+
def _top_level_unit(v: Node, root: Cluster) -> Node | Cluster:
|
|
37
|
+
"""The outermost frame (a child of *root*) containing *v*, or *v* itself
|
|
38
|
+
when it sits directly in the root."""
|
|
39
|
+
unit: Node | Cluster = v
|
|
40
|
+
c = v.cluster
|
|
41
|
+
while c is not None and c is not root:
|
|
42
|
+
unit = c
|
|
43
|
+
c = c.cluster
|
|
44
|
+
return unit
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def add_frame_sequence_edges(CG: ClusterGraph, H: nx.MultiDiGraph[Node]) -> None:
|
|
48
|
+
"""Rank frames as stages of the flow (nodebpy divergence).
|
|
49
|
+
|
|
50
|
+
With plain nesting constraints a frame only has to enclose its own
|
|
51
|
+
members, so a downstream frame's first nodes are ranked right next to the
|
|
52
|
+
upstream frame's last ones and the two frames share columns — which
|
|
53
|
+
forces them to be stacked vertically, producing a staircase of frames
|
|
54
|
+
instead of a left-to-right flow. This adds, for every link between two
|
|
55
|
+
top-level units (a frame, or a node outside every frame), a constraint
|
|
56
|
+
from the source unit's right border to the target unit's left border:
|
|
57
|
+
every node of a frame comes after every node of the frame (or the
|
|
58
|
+
intermediate node) feeding it. Nodes without predecessors (inputs and
|
|
59
|
+
values serving one consumer) and without successors are exempt so they
|
|
60
|
+
stay next to their consumer / producer; units on a cycle of the quotient
|
|
61
|
+
graph are left to the plain nesting constraints.
|
|
62
|
+
"""
|
|
63
|
+
G = CG.G
|
|
64
|
+
root = next(c for c in CG.S if not CG.T.pred[c])
|
|
65
|
+
unit_of = {v: _top_level_unit(v, root) for v in G}
|
|
66
|
+
|
|
67
|
+
Q: nx.DiGraph[Node | Cluster] = nx.DiGraph()
|
|
68
|
+
Q.add_nodes_from(set(unit_of.values()))
|
|
69
|
+
for u, v in G.edges():
|
|
70
|
+
a, b = unit_of[u], unit_of[v]
|
|
71
|
+
if a is not b:
|
|
72
|
+
Q.add_edge(a, b)
|
|
73
|
+
|
|
74
|
+
scc_of = {
|
|
75
|
+
n: i for i, comp in enumerate(nx.strongly_connected_components(Q)) for n in comp
|
|
76
|
+
}
|
|
77
|
+
for a, b in Q.edges():
|
|
78
|
+
if scc_of[a] == scc_of[b]:
|
|
79
|
+
continue
|
|
80
|
+
a_is_frame = isinstance(a, Cluster)
|
|
81
|
+
b_is_frame = isinstance(b, Cluster)
|
|
82
|
+
if not a_is_frame and not b_is_frame:
|
|
83
|
+
continue
|
|
84
|
+
if not a_is_frame and not G.pred[a]:
|
|
85
|
+
continue
|
|
86
|
+
if not b_is_frame and not G.succ[b]:
|
|
87
|
+
continue
|
|
88
|
+
tail = a.right if isinstance(a, Cluster) else a
|
|
89
|
+
head = b.left if isinstance(b, Cluster) else b
|
|
90
|
+
H.add_edge(tail, head)
|
|
91
|
+
CG.state.frame_sequence.append(
|
|
92
|
+
(
|
|
93
|
+
frozenset(v for v in G if unit_of[v] is a),
|
|
94
|
+
frozenset(v for v in G if unit_of[v] is b),
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
33
99
|
@cache
|
|
34
100
|
def get_adj_edges_H(H: nx.MultiDiGraph[Node], v: Node) -> tuple[MultiEdge, ...]:
|
|
35
101
|
return (*H.in_edges(v, keys=True), *H.out_edges(v, keys=True))
|
|
@@ -14,6 +14,7 @@ from mathutils import Vector
|
|
|
14
14
|
|
|
15
15
|
from ..config import LayoutState, Settings
|
|
16
16
|
from ..utils import abs_loc, group_by
|
|
17
|
+
from .balancing import balance_column_heights
|
|
17
18
|
from .graph import (
|
|
18
19
|
FROM_SOCKET,
|
|
19
20
|
TO_SOCKET,
|
|
@@ -102,8 +103,11 @@ def optimize_sizes(nodes: Iterable[BlenderNode]) -> None:
|
|
|
102
103
|
def precompute_links(state: LayoutState) -> None:
|
|
103
104
|
# Precompute links to ignore invalid/hidden links, and avoid `O(len(ntree.links))` time
|
|
104
105
|
|
|
106
|
+
# Headless divergence: links into a collapsed panel's sockets report
|
|
107
|
+
# ``is_hidden`` (Blender draws them to the panel header); they still
|
|
108
|
+
# carry data, so they still order the nodes.
|
|
105
109
|
for link in state.ntree.links:
|
|
106
|
-
if
|
|
110
|
+
if link.is_valid:
|
|
107
111
|
assert link.from_socket
|
|
108
112
|
assert link.to_socket
|
|
109
113
|
state.linked_sockets[link.to_socket].add(link.from_socket)
|
|
@@ -313,6 +317,8 @@ def sugiyama_layout(
|
|
|
313
317
|
node_stacks = contracted_node_stacks(CG)
|
|
314
318
|
|
|
315
319
|
compute_ranks(CG)
|
|
320
|
+
if state.settings.balance_heights:
|
|
321
|
+
balance_column_heights(G, CG.S, state)
|
|
316
322
|
CG.merge_edges()
|
|
317
323
|
CG.insert_dummy_nodes()
|
|
318
324
|
|
|
@@ -60,6 +60,10 @@ def assign_x_coords(
|
|
|
60
60
|
columns: list[list[Node]] = G.graph["columns"]
|
|
61
61
|
x = 0
|
|
62
62
|
for i, col in enumerate(columns):
|
|
63
|
+
if not col:
|
|
64
|
+
# nodebpy divergence: a rank whose only occupants were dummy
|
|
65
|
+
# nodes (dissolved when reroutes are not added) takes no space.
|
|
66
|
+
continue
|
|
63
67
|
max_width = max([v.width for v in col])
|
|
64
68
|
|
|
65
69
|
for v in col:
|
|
@@ -135,6 +135,17 @@ def inner_shift(
|
|
|
135
135
|
w.inner_shift = fmean(inner_shifts)
|
|
136
136
|
|
|
137
137
|
|
|
138
|
+
def vertical_gap(u: Node, w: Node, state: LayoutState) -> float:
|
|
139
|
+
"""Margin between two vertically adjacent nodes of a column (nodebpy
|
|
140
|
+
divergence): consecutive reroutes / dummy nodes — the bundles of long
|
|
141
|
+
links routed past a column — pack much tighter than nodes, as reroute
|
|
142
|
+
dots do in hand-made trees, so a fan-in of many long links no longer
|
|
143
|
+
costs a node's height per link."""
|
|
144
|
+
if u.is_reroute and w.is_reroute:
|
|
145
|
+
return state.margin.y * state.settings.reroute_margin_y_fac
|
|
146
|
+
return state.margin.y
|
|
147
|
+
|
|
148
|
+
|
|
138
149
|
def place_block(v: Node, is_up: bool, state: LayoutState) -> None:
|
|
139
150
|
if cast(float | None, v.y) is not None:
|
|
140
151
|
return
|
|
@@ -155,7 +166,8 @@ def place_block(v: Node, is_up: bool, state: LayoutState) -> None:
|
|
|
155
166
|
v.sink = u.sink
|
|
156
167
|
|
|
157
168
|
if v.sink == u.sink:
|
|
158
|
-
|
|
169
|
+
gap = vertical_gap(n, w, state)
|
|
170
|
+
delta_l = n.height + gap if is_up else w.height + gap
|
|
159
171
|
s_b = u.y + n.inner_shift - w.inner_shift + delta_l
|
|
160
172
|
v.y = s_b if initial else max(v.y, s_b)
|
|
161
173
|
initial = False
|
|
@@ -183,7 +195,8 @@ def vertical_compaction(G: nx.DiGraph[Node], is_up: bool, state: LayoutState) ->
|
|
|
183
195
|
col[0].sink.shift = 0
|
|
184
196
|
|
|
185
197
|
for u, v in neighborings[tuple(col)]:
|
|
186
|
-
|
|
198
|
+
gap = vertical_gap(u, v, state)
|
|
199
|
+
delta_l = u.height + gap if is_up else v.height + gap
|
|
187
200
|
s_c = v.y + v.inner_shift - u.y - u.inner_shift - delta_l
|
|
188
201
|
u.sink.shift = min(u.sink.shift, v.sink.shift + s_c)
|
|
189
202
|
|
|
@@ -11,7 +11,7 @@ from bpy.types import NodeSocket, NodeTree
|
|
|
11
11
|
from mathutils import Vector
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
-
from .arrange.graph import Socket
|
|
14
|
+
from .arrange.graph import Node, Socket
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
@dataclass
|
|
@@ -30,6 +30,17 @@ class Settings:
|
|
|
30
30
|
recenter_mode = "NODES"
|
|
31
31
|
origin: Literal["CENTER", "ACTIVE_OUTPUT", "ACTIVE_NODE"] = "CENTER"
|
|
32
32
|
stack_margin_y_fac: float = 0.5
|
|
33
|
+
# nodebpy divergence: rank frames as sequential stages (see
|
|
34
|
+
# ranking.add_frame_sequence_edges).
|
|
35
|
+
sequential_frames: bool = True
|
|
36
|
+
# nodebpy divergence: promote private feeder chains out of the tallest
|
|
37
|
+
# column (see arrange.balancing).
|
|
38
|
+
balance_heights: bool = True
|
|
39
|
+
balance_aspect: float = 1.6
|
|
40
|
+
# nodebpy divergence: fraction of the vertical margin kept between
|
|
41
|
+
# consecutive reroutes / dummy nodes in a column (see
|
|
42
|
+
# y_coords.vertical_gap).
|
|
43
|
+
reroute_margin_y_fac: float = 0.35
|
|
33
44
|
|
|
34
45
|
|
|
35
46
|
DEFAULT_MARGIN = (200.0, 20.0)
|
|
@@ -55,3 +66,8 @@ class LayoutState:
|
|
|
55
66
|
multi_input_sort_ids: defaultdict[Socket, list[tuple[Socket, int]]] = field(
|
|
56
67
|
default_factory=lambda: defaultdict(list)
|
|
57
68
|
)
|
|
69
|
+
# Frame-sequence constraints applied by ranking.add_frame_sequence_edges:
|
|
70
|
+
# every node of the first set is ranked before every node of the second.
|
|
71
|
+
frame_sequence: list[tuple[frozenset[Node], frozenset[Node]]] = field(
|
|
72
|
+
default_factory=list
|
|
73
|
+
)
|
|
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
|
|
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
|
|
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
|
|
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
|