nodebpy 0.3.1__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nodebpy/builder.py +17 -13
- nodebpy/lib/nodearrange/__init__.py +2 -0
- nodebpy/lib/nodearrange/arrange/graph.py +583 -0
- nodebpy/lib/nodearrange/arrange/ordering.py +512 -0
- nodebpy/lib/nodearrange/arrange/ranking.py +256 -0
- nodebpy/lib/nodearrange/arrange/realize.py +231 -0
- nodebpy/lib/nodearrange/arrange/stacking.py +313 -0
- nodebpy/lib/nodearrange/arrange/structs.py +132 -0
- nodebpy/lib/nodearrange/arrange/sugiyama.py +256 -0
- nodebpy/lib/nodearrange/arrange/x_coords.py +211 -0
- nodebpy/lib/nodearrange/arrange/y_coords.py +339 -0
- nodebpy/lib/nodearrange/config.py +45 -0
- nodebpy/lib/nodearrange/utils.py +109 -0
- nodebpy/nodes/color.py +0 -1
- nodebpy/nodes/group.py +0 -1
- {nodebpy-0.3.1.dist-info → nodebpy-0.4.0.dist-info}/METADATA +6 -56
- nodebpy-0.4.0.dist-info/RECORD +38 -0
- nodebpy-0.3.1.dist-info/RECORD +0 -26
- {nodebpy-0.3.1.dist-info → nodebpy-0.4.0.dist-info}/WHEEL +0 -0
- {nodebpy-0.3.1.dist-info → nodebpy-0.4.0.dist-info}/entry_points.txt +0 -0
nodebpy/builder.py
CHANGED
|
@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Any, ClassVar, Iterable, Literal
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from .nodes import Math, VectorMath
|
|
7
7
|
|
|
8
|
-
import arrangebpy
|
|
9
8
|
import bpy
|
|
10
9
|
from bpy.types import (
|
|
11
10
|
GeometryNodeTree,
|
|
@@ -14,6 +13,7 @@ from bpy.types import (
|
|
|
14
13
|
NodeSocket,
|
|
15
14
|
)
|
|
16
15
|
|
|
16
|
+
from .lib.nodearrange import arrange
|
|
17
17
|
from .types import (
|
|
18
18
|
LINKABLE,
|
|
19
19
|
SOCKET_COMPATIBILITY,
|
|
@@ -117,12 +117,15 @@ class TreeBuilder:
|
|
|
117
117
|
"""Builder for creating Blender geometry node trees with a clean Python API."""
|
|
118
118
|
|
|
119
119
|
_tree_contexts: ClassVar["list[TreeBuilder]"] = []
|
|
120
|
-
# _active_tree: ClassVar["TreeBuilder | None"] = None
|
|
121
|
-
# _previous_tree: ClassVar["list[TreeBuilder]"] = list()
|
|
122
120
|
just_added: "Node | None" = None
|
|
121
|
+
collapse: bool = False
|
|
123
122
|
|
|
124
123
|
def __init__(
|
|
125
|
-
self,
|
|
124
|
+
self,
|
|
125
|
+
tree: GeometryNodeTree | str = "Geometry Nodes",
|
|
126
|
+
*,
|
|
127
|
+
collapse: bool = False,
|
|
128
|
+
arrange: bool = True,
|
|
126
129
|
):
|
|
127
130
|
if isinstance(tree, str):
|
|
128
131
|
self.tree = bpy.data.node_groups.new(tree, "GeometryNodeTree")
|
|
@@ -134,6 +137,11 @@ class TreeBuilder:
|
|
|
134
137
|
self.inputs = InputInterfaceContext(self)
|
|
135
138
|
self.outputs = OutputInterfaceContext(self)
|
|
136
139
|
self._arrange = arrange
|
|
140
|
+
self.collapse = collapse
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def nodes(self) -> Nodes:
|
|
144
|
+
return self.tree.nodes
|
|
137
145
|
|
|
138
146
|
def activate_tree(self) -> None:
|
|
139
147
|
"""Make this tree the active tree for all new node creation."""
|
|
@@ -152,18 +160,12 @@ class TreeBuilder:
|
|
|
152
160
|
self.arrange()
|
|
153
161
|
self.deactivate_tree()
|
|
154
162
|
|
|
155
|
-
@property
|
|
156
|
-
def nodes(self) -> Nodes:
|
|
157
|
-
return self.tree.nodes
|
|
158
|
-
|
|
159
163
|
def __len__(self) -> int:
|
|
160
164
|
return len(self.nodes)
|
|
161
165
|
|
|
162
166
|
def arrange(self):
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
)
|
|
166
|
-
arrangebpy.sugiyama_layout(self.tree, settings)
|
|
167
|
+
arrange.sugiyama.sugiyama_layout(self.tree)
|
|
168
|
+
arrange.sugiyama.config.reset()
|
|
167
169
|
|
|
168
170
|
def _repr_markdown_(self) -> str | None:
|
|
169
171
|
"""
|
|
@@ -230,7 +232,9 @@ class TreeBuilder:
|
|
|
230
232
|
return link
|
|
231
233
|
|
|
232
234
|
def add(self, name: str) -> Node:
|
|
233
|
-
|
|
235
|
+
node = self.tree.nodes.new(name)
|
|
236
|
+
node.hide = self.collapse
|
|
237
|
+
return node
|
|
234
238
|
|
|
235
239
|
|
|
236
240
|
class NodeBuilder:
|
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum, auto
|
|
8
|
+
from functools import cached_property
|
|
9
|
+
from itertools import chain, pairwise, product
|
|
10
|
+
from math import inf
|
|
11
|
+
from typing import Any, Literal, Sequence, TypeGuard
|
|
12
|
+
|
|
13
|
+
import bpy
|
|
14
|
+
import networkx as nx
|
|
15
|
+
from bpy.types import Node as BlenderNode
|
|
16
|
+
from bpy.types import NodeFrame, NodeSocket
|
|
17
|
+
|
|
18
|
+
from .. import config
|
|
19
|
+
from ..utils import (
|
|
20
|
+
REROUTE_DIM,
|
|
21
|
+
abs_loc,
|
|
22
|
+
dimensions,
|
|
23
|
+
frame_padding,
|
|
24
|
+
get_bottom,
|
|
25
|
+
get_ntree,
|
|
26
|
+
get_top,
|
|
27
|
+
group_by,
|
|
28
|
+
)
|
|
29
|
+
from .structs import bNodeSocket
|
|
30
|
+
|
|
31
|
+
# -------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Kind(Enum):
|
|
35
|
+
NODE = auto()
|
|
36
|
+
STACK = auto()
|
|
37
|
+
DUMMY = auto()
|
|
38
|
+
CLUSTER = auto()
|
|
39
|
+
HORIZONTAL_BORDER = auto()
|
|
40
|
+
VERTICAL_BORDER = auto()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_NonCluster = Literal[
|
|
44
|
+
Kind.NODE,
|
|
45
|
+
Kind.STACK,
|
|
46
|
+
Kind.DUMMY,
|
|
47
|
+
Kind.HORIZONTAL_BORDER,
|
|
48
|
+
Kind.VERTICAL_BORDER,
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(slots=True)
|
|
53
|
+
class CrossingReduction:
|
|
54
|
+
socket_ranks: dict[Socket, float] = field(default_factory=dict)
|
|
55
|
+
barycenter: float | None = None
|
|
56
|
+
|
|
57
|
+
def reset(self) -> None:
|
|
58
|
+
self.socket_ranks.clear()
|
|
59
|
+
self.barycenter = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Node:
|
|
63
|
+
node: BlenderNode | None
|
|
64
|
+
cluster: Cluster | None
|
|
65
|
+
type: _NonCluster
|
|
66
|
+
|
|
67
|
+
is_reroute: bool
|
|
68
|
+
width: float
|
|
69
|
+
height: float
|
|
70
|
+
|
|
71
|
+
rank: int
|
|
72
|
+
po_num: int
|
|
73
|
+
lowest_po_num: int
|
|
74
|
+
is_fill_dummy: bool
|
|
75
|
+
|
|
76
|
+
col: list[Node]
|
|
77
|
+
cr: CrossingReduction
|
|
78
|
+
|
|
79
|
+
x: float
|
|
80
|
+
y: float
|
|
81
|
+
|
|
82
|
+
root: Node
|
|
83
|
+
aligned: Node
|
|
84
|
+
inner_shift: float
|
|
85
|
+
sink: Node
|
|
86
|
+
shift: float
|
|
87
|
+
|
|
88
|
+
__slots__ = tuple(__annotations__)
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
node: BlenderNode | None = None,
|
|
93
|
+
cluster: Cluster | None = None,
|
|
94
|
+
type: _NonCluster = Kind.NODE,
|
|
95
|
+
rank: int | None = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
real = isinstance(node, BlenderNode)
|
|
98
|
+
|
|
99
|
+
self.node = node
|
|
100
|
+
self.cluster = cluster
|
|
101
|
+
self.type = type
|
|
102
|
+
self.rank = rank # type: ignore
|
|
103
|
+
|
|
104
|
+
if type == Kind.DUMMY or (real and node.bl_idname == "NodeReroute"):
|
|
105
|
+
self.is_reroute = True
|
|
106
|
+
self.width = REROUTE_DIM.x
|
|
107
|
+
self.height = REROUTE_DIM.y
|
|
108
|
+
elif real:
|
|
109
|
+
self.is_reroute = False
|
|
110
|
+
self.width = dimensions(node).x
|
|
111
|
+
self.height = get_top(node) - get_bottom(node)
|
|
112
|
+
else:
|
|
113
|
+
self.is_reroute = type == Kind.VERTICAL_BORDER
|
|
114
|
+
self.width = 0
|
|
115
|
+
self.height = 0
|
|
116
|
+
|
|
117
|
+
self.po_num = None # type: ignore
|
|
118
|
+
self.lowest_po_num = None # type: ignore
|
|
119
|
+
self.is_fill_dummy = False
|
|
120
|
+
|
|
121
|
+
self.col = None # type: ignore
|
|
122
|
+
self.cr = CrossingReduction()
|
|
123
|
+
|
|
124
|
+
self.x = None # type: ignore
|
|
125
|
+
self.bk_reset()
|
|
126
|
+
|
|
127
|
+
def __hash__(self) -> int:
|
|
128
|
+
return id(self)
|
|
129
|
+
|
|
130
|
+
def bk_reset(self) -> None:
|
|
131
|
+
self.root = self
|
|
132
|
+
self.aligned = self
|
|
133
|
+
self.inner_shift = 0
|
|
134
|
+
|
|
135
|
+
self.sink = self
|
|
136
|
+
self.shift = inf
|
|
137
|
+
|
|
138
|
+
self.y = None # type: ignore
|
|
139
|
+
|
|
140
|
+
def corrected_y(self) -> float:
|
|
141
|
+
assert is_real(self)
|
|
142
|
+
return self.y + (abs_loc(self.node).y - get_top(self.node))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class _RealNode(Node):
|
|
146
|
+
node: BlenderNode # type: ignore
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def is_real(v: Node | Cluster) -> TypeGuard[_RealNode]:
|
|
150
|
+
return isinstance(v.node, BlenderNode)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def node_name(v: Node) -> str:
|
|
154
|
+
return getattr(v.node, "name", "")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
Edge = tuple[Node, Node]
|
|
158
|
+
MultiEdge = tuple[Node, Node, int]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def opposite(v: Node, e: tuple[Node, Node] | tuple[Node, Node, ...]) -> Node:
|
|
162
|
+
return e[0] if v != e[0] else e[1]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(slots=True)
|
|
166
|
+
class Cluster:
|
|
167
|
+
node: NodeFrame | None
|
|
168
|
+
cluster: Cluster
|
|
169
|
+
nesting_level: int | None = None
|
|
170
|
+
cr: CrossingReduction = field(default_factory=CrossingReduction)
|
|
171
|
+
left: Node = field(init=False)
|
|
172
|
+
right: Node = field(init=False)
|
|
173
|
+
|
|
174
|
+
def __post_init__(self) -> None:
|
|
175
|
+
self.left = Node(None, self, Kind.HORIZONTAL_BORDER)
|
|
176
|
+
self.right = Node(None, self, Kind.HORIZONTAL_BORDER)
|
|
177
|
+
|
|
178
|
+
def __hash__(self) -> int:
|
|
179
|
+
return id(self)
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def type(self) -> Literal[Kind.CLUSTER]:
|
|
183
|
+
return Kind.CLUSTER
|
|
184
|
+
|
|
185
|
+
def label_height(self) -> float:
|
|
186
|
+
frame = self.node
|
|
187
|
+
if frame and frame.label:
|
|
188
|
+
return -(frame_padding() / 2 - frame.label_size * 1.25)
|
|
189
|
+
else:
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# -------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def get_nesting_relations(
|
|
197
|
+
v: Node | Cluster,
|
|
198
|
+
) -> Iterator[tuple[Cluster, Node | Cluster]]:
|
|
199
|
+
if c := v.cluster:
|
|
200
|
+
yield (c, v)
|
|
201
|
+
yield from get_nesting_relations(c)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def lowest_common_cluster(
|
|
205
|
+
T: nx.DiGraph[Node | Cluster],
|
|
206
|
+
edges: Iterable[tuple[Node, Node, Any]],
|
|
207
|
+
) -> dict[Edge, Cluster]:
|
|
208
|
+
pairs = {(u, v) for u, v, _ in edges if u.cluster != v.cluster}
|
|
209
|
+
return dict(nx.tree_all_pairs_lowest_common_ancestor(T, pairs=pairs))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def add_dummy_edge(G: nx.DiGraph[Node], u: Node, v: Node) -> None:
|
|
213
|
+
G.add_edge(u, v, from_socket=Socket(u, 0, True), to_socket=Socket(v, 0, False))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def add_dummy_nodes_to_edge(
|
|
217
|
+
G: nx.MultiDiGraph[Node],
|
|
218
|
+
edge: MultiEdge,
|
|
219
|
+
dummy_nodes: Sequence[Node],
|
|
220
|
+
) -> None:
|
|
221
|
+
if not dummy_nodes:
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
for pair in pairwise(dummy_nodes):
|
|
225
|
+
if pair not in G.edges:
|
|
226
|
+
add_dummy_edge(G, *pair)
|
|
227
|
+
|
|
228
|
+
u, v, _ = edge
|
|
229
|
+
d = G.edges[edge] # type: ignore
|
|
230
|
+
|
|
231
|
+
w = dummy_nodes[0]
|
|
232
|
+
if w not in G[u]:
|
|
233
|
+
G.add_edge(u, w, from_socket=d[FROM_SOCKET], to_socket=Socket(w, 0, False))
|
|
234
|
+
|
|
235
|
+
z = dummy_nodes[-1]
|
|
236
|
+
G.add_edge(z, v, from_socket=Socket(z, 0, True), to_socket=d[TO_SOCKET])
|
|
237
|
+
|
|
238
|
+
G.remove_edge(*edge)
|
|
239
|
+
|
|
240
|
+
if not is_real(u) or not is_real(v):
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
links = get_ntree().links
|
|
244
|
+
if d[TO_SOCKET].bpy.is_multi_input:
|
|
245
|
+
target_link = (d[FROM_SOCKET].bpy, d[TO_SOCKET].bpy)
|
|
246
|
+
links.remove(
|
|
247
|
+
next(l for l in links if (l.from_socket, l.to_socket) == target_link)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def assign_clusters(
|
|
252
|
+
dummy_nodes: Iterable[Node],
|
|
253
|
+
start: Cluster,
|
|
254
|
+
stop: Cluster,
|
|
255
|
+
is_within_cluster: Callable[[Node, Cluster], bool],
|
|
256
|
+
) -> None:
|
|
257
|
+
c = start
|
|
258
|
+
for w in dummy_nodes:
|
|
259
|
+
while c != stop and not is_within_cluster(w, c):
|
|
260
|
+
c = c.cluster
|
|
261
|
+
|
|
262
|
+
if c == stop:
|
|
263
|
+
break
|
|
264
|
+
|
|
265
|
+
w.cluster = c
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def improve_cluster_assignment(e: Edge, dummy_nodes: Sequence[Node]) -> None:
|
|
269
|
+
if config.SETTINGS.keep_reroutes_outside_frames:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
u, v = e
|
|
273
|
+
assert u.cluster and v.cluster
|
|
274
|
+
c1 = u.cluster
|
|
275
|
+
c2 = v.cluster
|
|
276
|
+
|
|
277
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
278
|
+
|
|
279
|
+
if not (c1.node and c2.node) or c1.right.rank >= c2.left.rank:
|
|
280
|
+
if c2.node and u.rank < c2.left.rank:
|
|
281
|
+
c1 = None
|
|
282
|
+
while c2.cluster.node and u.rank < c2.cluster.left.rank:
|
|
283
|
+
c2 = c2.cluster
|
|
284
|
+
elif c1.node and v.rank > c1.right.rank:
|
|
285
|
+
c2 = None
|
|
286
|
+
while c1.cluster.node and v.rank > c1.cluster.right.rank:
|
|
287
|
+
c1 = c1.cluster
|
|
288
|
+
else:
|
|
289
|
+
return
|
|
290
|
+
else:
|
|
291
|
+
while True:
|
|
292
|
+
parent1 = c1.cluster
|
|
293
|
+
if parent1.node and parent1.right.rank < c2.left.rank:
|
|
294
|
+
c1 = parent1
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
parent2 = c2.cluster
|
|
298
|
+
if parent2.node and c1.right.rank < parent2.left.rank:
|
|
299
|
+
c2 = parent2
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
305
|
+
|
|
306
|
+
if c1:
|
|
307
|
+
assign_clusters(
|
|
308
|
+
dummy_nodes,
|
|
309
|
+
u.cluster,
|
|
310
|
+
c1.cluster,
|
|
311
|
+
lambda w, c: w.rank <= c.right.rank,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if c2:
|
|
315
|
+
assign_clusters(
|
|
316
|
+
reversed(dummy_nodes),
|
|
317
|
+
v.cluster,
|
|
318
|
+
c2.cluster,
|
|
319
|
+
lambda w, c: w.rank >= c.left.rank,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# https://api.semanticscholar.org/CorpusID:14932050
|
|
324
|
+
class ClusterGraph:
|
|
325
|
+
G: nx.MultiDiGraph[Node]
|
|
326
|
+
T: nx.DiGraph[Node | Cluster]
|
|
327
|
+
S: set[Cluster]
|
|
328
|
+
__slots__ = tuple(__annotations__)
|
|
329
|
+
|
|
330
|
+
def __init__(self, G: nx.MultiDiGraph[Node]) -> None:
|
|
331
|
+
self.G = G
|
|
332
|
+
self.T = nx.DiGraph(chain(*map(get_nesting_relations, G)))
|
|
333
|
+
self.S = {v for v in self.T if v.type == Kind.CLUSTER}
|
|
334
|
+
|
|
335
|
+
def remove_nodes_from(self, nodes: Iterable[Node]) -> None:
|
|
336
|
+
ntree = get_ntree()
|
|
337
|
+
for v in nodes:
|
|
338
|
+
self.G.remove_node(v)
|
|
339
|
+
self.T.remove_node(v)
|
|
340
|
+
if v.col:
|
|
341
|
+
v.col.remove(v)
|
|
342
|
+
|
|
343
|
+
if not is_real(v):
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
sockets = {*v.node.inputs, *v.node.outputs}
|
|
347
|
+
|
|
348
|
+
for socket in sockets:
|
|
349
|
+
config.linked_sockets.pop(socket, None)
|
|
350
|
+
|
|
351
|
+
for val in config.linked_sockets.values():
|
|
352
|
+
val -= sockets
|
|
353
|
+
|
|
354
|
+
config.selected.remove(v.node)
|
|
355
|
+
ntree.nodes.remove(v.node)
|
|
356
|
+
|
|
357
|
+
def merge_edges(self) -> None:
|
|
358
|
+
G = self.G
|
|
359
|
+
T = self.T
|
|
360
|
+
groups = group_by(G.edges, key=lambda e: G.edges[e][FROM_SOCKET])
|
|
361
|
+
edges: tuple[MultiEdge, ...]
|
|
362
|
+
for edges, from_socket in groups.items():
|
|
363
|
+
long_edges = [(u, v, k) for u, v, k in edges if v.rank - u.rank > 1]
|
|
364
|
+
|
|
365
|
+
if len(long_edges) < 2:
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
long_edges.sort(key=lambda e: e[1].rank)
|
|
369
|
+
lca = lowest_common_cluster(T, long_edges)
|
|
370
|
+
dummy_nodes = []
|
|
371
|
+
for u, v, k in long_edges:
|
|
372
|
+
if dummy_nodes and dummy_nodes[-1].rank == v.rank - 1:
|
|
373
|
+
w = dummy_nodes[-1]
|
|
374
|
+
else:
|
|
375
|
+
assert u.cluster
|
|
376
|
+
c = lca.get((u, v), u.cluster)
|
|
377
|
+
w = Node(None, c, Kind.DUMMY, v.rank - 1)
|
|
378
|
+
dummy_nodes.append(w)
|
|
379
|
+
|
|
380
|
+
add_dummy_nodes_to_edge(G, (u, v, k), [w])
|
|
381
|
+
G.remove_edge(u, w)
|
|
382
|
+
|
|
383
|
+
for pair in pairwise(dummy_nodes):
|
|
384
|
+
add_dummy_edge(G, *pair)
|
|
385
|
+
|
|
386
|
+
w = dummy_nodes[0]
|
|
387
|
+
G.add_edge(u, w, from_socket=from_socket, to_socket=Socket(w, 0, False))
|
|
388
|
+
|
|
389
|
+
improve_cluster_assignment((u, v), dummy_nodes)
|
|
390
|
+
for w in dummy_nodes:
|
|
391
|
+
T.add_edge(w.cluster, w)
|
|
392
|
+
|
|
393
|
+
def insert_dummy_nodes(self) -> None:
|
|
394
|
+
G = self.G
|
|
395
|
+
T = self.T
|
|
396
|
+
|
|
397
|
+
# -------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
for c in self.S:
|
|
400
|
+
descendants = [v for v in nx.descendants(T, c) if v.type != Kind.CLUSTER]
|
|
401
|
+
c.left = min(descendants, key=lambda v: v.rank)
|
|
402
|
+
c.right = max(descendants, key=lambda v: v.rank)
|
|
403
|
+
|
|
404
|
+
# -------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
long_edges = [
|
|
407
|
+
(u, v, k) for u, v, k in G.edges(keys=True) if v.rank - u.rank > 1
|
|
408
|
+
]
|
|
409
|
+
lca = lowest_common_cluster(T, long_edges)
|
|
410
|
+
for u, v, k in long_edges:
|
|
411
|
+
assert u.cluster
|
|
412
|
+
c = lca.get((u, v), u.cluster)
|
|
413
|
+
dummy_nodes = []
|
|
414
|
+
for i in range(u.rank + 1, v.rank):
|
|
415
|
+
w = Node(None, c, Kind.DUMMY, i)
|
|
416
|
+
dummy_nodes.append(w)
|
|
417
|
+
|
|
418
|
+
improve_cluster_assignment((u, v), dummy_nodes)
|
|
419
|
+
add_dummy_nodes_to_edge(G, (u, v, k), dummy_nodes)
|
|
420
|
+
|
|
421
|
+
for w in G.nodes - T.nodes:
|
|
422
|
+
assert w.cluster
|
|
423
|
+
T.add_edge(w.cluster, w)
|
|
424
|
+
|
|
425
|
+
# -------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
for c in self.S:
|
|
428
|
+
if not c.node:
|
|
429
|
+
continue
|
|
430
|
+
|
|
431
|
+
ranks = sorted(
|
|
432
|
+
{v.rank for v in nx.descendants(T, c) if v.type != Kind.CLUSTER}
|
|
433
|
+
)
|
|
434
|
+
for i, j in pairwise(ranks):
|
|
435
|
+
for k in range(i + 1, j):
|
|
436
|
+
v = Node(None, c, Kind.DUMMY, k)
|
|
437
|
+
v.is_fill_dummy = True
|
|
438
|
+
G.add_node(v)
|
|
439
|
+
T.add_edge(c, v)
|
|
440
|
+
|
|
441
|
+
def add_vertical_border_nodes(self) -> None:
|
|
442
|
+
T = self.T
|
|
443
|
+
G = self.G
|
|
444
|
+
columns = G.graph["columns"]
|
|
445
|
+
for c in self.S:
|
|
446
|
+
if not c.node:
|
|
447
|
+
continue
|
|
448
|
+
|
|
449
|
+
descendants = [v for v in nx.descendants(T, c) if v.type != Kind.CLUSTER]
|
|
450
|
+
lower_border_nodes = []
|
|
451
|
+
upper_border_nodes = []
|
|
452
|
+
for subcol in group_by(
|
|
453
|
+
descendants, key=lambda v: columns.index(v.col), sort=True
|
|
454
|
+
):
|
|
455
|
+
col = subcol[0].col
|
|
456
|
+
indices = [col.index(v) for v in subcol]
|
|
457
|
+
|
|
458
|
+
lower_v = Node(None, c, Kind.VERTICAL_BORDER)
|
|
459
|
+
col.insert(max(indices) + 1, lower_v)
|
|
460
|
+
lower_v.col = col
|
|
461
|
+
T.add_edge(c, lower_v)
|
|
462
|
+
lower_border_nodes.append(lower_v)
|
|
463
|
+
|
|
464
|
+
upper_v = Node(None, c, Kind.VERTICAL_BORDER)
|
|
465
|
+
upper_v.height += c.label_height()
|
|
466
|
+
col.insert(min(indices), upper_v)
|
|
467
|
+
upper_v.col = col
|
|
468
|
+
T.add_edge(c, upper_v)
|
|
469
|
+
upper_border_nodes.append(upper_v)
|
|
470
|
+
|
|
471
|
+
G.add_nodes_from(lower_border_nodes + upper_border_nodes)
|
|
472
|
+
for p in *pairwise(lower_border_nodes), *pairwise(upper_border_nodes):
|
|
473
|
+
add_dummy_edge(G, *p)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# -------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def get_socket_y(socket: NodeSocket) -> float:
|
|
480
|
+
b_socket = bNodeSocket.from_address(socket.as_pointer())
|
|
481
|
+
ui_scale = 1.0 # type: ignore
|
|
482
|
+
return b_socket.runtime.contents.location[1] / ui_scale
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@dataclass(frozen=True)
|
|
486
|
+
class Socket:
|
|
487
|
+
owner: Node
|
|
488
|
+
idx: int
|
|
489
|
+
is_output: bool
|
|
490
|
+
prescribed_offset_y: float | None = field(default=None, hash=False, compare=False)
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def bpy(self) -> NodeSocket | None:
|
|
494
|
+
v = self.owner
|
|
495
|
+
|
|
496
|
+
if not is_real(v):
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
sockets = v.node.outputs if self.is_output else v.node.inputs
|
|
500
|
+
if self.idx >= len(sockets):
|
|
501
|
+
return None
|
|
502
|
+
return sockets[self.idx]
|
|
503
|
+
|
|
504
|
+
@property
|
|
505
|
+
def x(self) -> float:
|
|
506
|
+
v = self.owner
|
|
507
|
+
return v.x + v.width if self.is_output else v.x
|
|
508
|
+
|
|
509
|
+
@cached_property
|
|
510
|
+
def _offset_y(self) -> float:
|
|
511
|
+
if self.prescribed_offset_y is not None:
|
|
512
|
+
return self.prescribed_offset_y
|
|
513
|
+
|
|
514
|
+
v = self.owner
|
|
515
|
+
|
|
516
|
+
if v.is_reroute or not is_real(v):
|
|
517
|
+
return 0
|
|
518
|
+
|
|
519
|
+
assert self.bpy
|
|
520
|
+
return get_socket_y(self.bpy) - get_top(v.node)
|
|
521
|
+
|
|
522
|
+
@property
|
|
523
|
+
def y(self) -> float:
|
|
524
|
+
return self.owner.y + self._offset_y
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
FROM_SOCKET = "from_socket"
|
|
528
|
+
TO_SOCKET = "to_socket"
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def socket_graph(G: nx.MultiDiGraph[Node]) -> nx.DiGraph[Socket]:
|
|
532
|
+
H = nx.DiGraph()
|
|
533
|
+
H.add_edges_from([(d[FROM_SOCKET], d[TO_SOCKET]) for *_, d in G.edges.data()])
|
|
534
|
+
for sockets in group_by(H, key=lambda s: s.owner):
|
|
535
|
+
outputs = {s for s in sockets if s.is_output}
|
|
536
|
+
H.add_edges_from(product(set(sockets) - outputs, outputs))
|
|
537
|
+
|
|
538
|
+
return H
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# -------------------------------------------------------------------
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def get_reroute_paths(
|
|
545
|
+
CG: ClusterGraph,
|
|
546
|
+
function: Callable | None = None,
|
|
547
|
+
*,
|
|
548
|
+
preserve_reroute_clusters: bool = True,
|
|
549
|
+
aligned: bool = False,
|
|
550
|
+
linear: bool = True,
|
|
551
|
+
) -> list[list[Node]]:
|
|
552
|
+
G = CG.G
|
|
553
|
+
reroutes = {v for v in G if v.is_reroute and (not function or function(v))}
|
|
554
|
+
H = nx.DiGraph(G.subgraph(reroutes))
|
|
555
|
+
|
|
556
|
+
K = G if linear else H
|
|
557
|
+
for v in H:
|
|
558
|
+
if K.out_degree[v] > 1:
|
|
559
|
+
H.remove_edges_from(tuple(H.out_edges(v)))
|
|
560
|
+
|
|
561
|
+
if preserve_reroute_clusters:
|
|
562
|
+
reroute_clusters = { #
|
|
563
|
+
c
|
|
564
|
+
for c in CG.S
|
|
565
|
+
if all(v.is_reroute for v in CG.T[c] if v.type != Kind.CLUSTER)
|
|
566
|
+
}
|
|
567
|
+
H.remove_edges_from(
|
|
568
|
+
[ #
|
|
569
|
+
(u, v)
|
|
570
|
+
for u, v in H.edges
|
|
571
|
+
if u.cluster != v.cluster and {u.cluster, v.cluster} & reroute_clusters
|
|
572
|
+
]
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
if aligned:
|
|
576
|
+
H.remove_edges_from([(u, v) for u, v in H.edges if u.y != v.y])
|
|
577
|
+
|
|
578
|
+
indicies = {v: i for i, v in enumerate(nx.topological_sort(G)) if v in reroutes}
|
|
579
|
+
paths = [
|
|
580
|
+
sorted(c, key=lambda v: indicies[v]) for c in nx.weakly_connected_components(H)
|
|
581
|
+
]
|
|
582
|
+
paths.sort(key=lambda p: sum([indicies[v] for v in p]))
|
|
583
|
+
return paths
|