nodebpy 0.3.0__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 +52 -36
- 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/__init__.py +6 -0
- nodebpy/nodes/attribute.py +98 -4
- nodebpy/nodes/color.py +6 -3
- nodebpy/nodes/converter.py +357 -54
- nodebpy/nodes/experimental.py +9 -3
- nodebpy/nodes/geometry.py +390 -94
- nodebpy/nodes/grid.py +90 -30
- nodebpy/nodes/group.py +3 -2
- nodebpy/nodes/input.py +239 -78
- nodebpy/nodes/interface.py +21 -7
- nodebpy/nodes/manual.py +6 -6
- nodebpy/nodes/output.py +8 -1
- nodebpy/nodes/texture.py +30 -10
- nodebpy/nodes/vector.py +41 -4
- nodebpy/nodes/zone.py +125 -99
- {nodebpy-0.3.0.dist-info → nodebpy-0.4.0.dist-info}/METADATA +16 -56
- nodebpy-0.4.0.dist-info/RECORD +38 -0
- nodebpy-0.3.0.dist-info/RECORD +0 -26
- {nodebpy-0.3.0.dist-info → nodebpy-0.4.0.dist-info}/WHEEL +0 -0
- {nodebpy-0.3.0.dist-info → nodebpy-0.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
2
|
+
|
|
3
|
+
# http://dx.doi.org/10.1109/32.221135
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from functools import cache
|
|
8
|
+
from math import sqrt
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
import networkx as nx
|
|
12
|
+
|
|
13
|
+
from ..utils import group_by
|
|
14
|
+
from .graph import Kind, MultiEdge, Node, opposite
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .sugiyama import ClusterGraph
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# https://api.semanticscholar.org/CorpusID:14932050
|
|
21
|
+
def get_nesting_graph(CG: ClusterGraph) -> nx.MultiDiGraph[Node]:
|
|
22
|
+
H = CG.G.copy()
|
|
23
|
+
for u, v in CG.T.edges:
|
|
24
|
+
if u.type == Kind.CLUSTER:
|
|
25
|
+
if v.type != Kind.CLUSTER:
|
|
26
|
+
H.add_edges_from(((u.left, v), (v, u.right)))
|
|
27
|
+
else:
|
|
28
|
+
H.add_edges_from(((u.left, v.left), (v.right, u.right)))
|
|
29
|
+
|
|
30
|
+
return H
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@cache
|
|
34
|
+
def get_adj_edges_H(H: nx.MultiDiGraph[Node], v: Node) -> tuple[MultiEdge, ...]:
|
|
35
|
+
return (*H.in_edges(v, keys=True), *H.out_edges(v, keys=True))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@cache
|
|
39
|
+
def get_adj_edges_T(T: nx.MultiDiGraph[Node], v: Node) -> tuple[MultiEdge, ...]:
|
|
40
|
+
return (*T.in_edges(v, keys=True), *T.out_edges(v, keys=True))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_slack(e: MultiEdge) -> int:
|
|
44
|
+
u, v, _ = e
|
|
45
|
+
min_length = 1
|
|
46
|
+
return v.rank - u.rank - min_length
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def tight_tree(
|
|
50
|
+
H: nx.MultiDiGraph[Node],
|
|
51
|
+
T: nx.MultiDiGraph[Node],
|
|
52
|
+
v: Node,
|
|
53
|
+
visited: set[MultiEdge] | None = None,
|
|
54
|
+
) -> int:
|
|
55
|
+
if visited is None:
|
|
56
|
+
visited = set()
|
|
57
|
+
|
|
58
|
+
T.add_node(v)
|
|
59
|
+
|
|
60
|
+
for e in get_adj_edges_H(H, v):
|
|
61
|
+
if e in visited:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
visited.add(e)
|
|
65
|
+
|
|
66
|
+
u, w, _ = e
|
|
67
|
+
other = u if v != u else w
|
|
68
|
+
if e in T.edges:
|
|
69
|
+
tight_tree(H, T, other, visited)
|
|
70
|
+
elif other not in T and get_slack(e) == 0:
|
|
71
|
+
T.add_edge(*e)
|
|
72
|
+
tight_tree(H, T, other, visited)
|
|
73
|
+
|
|
74
|
+
return len(T)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def set_post_order_numbers(v: Node, T: nx.MultiDiGraph[Node]) -> None:
|
|
78
|
+
visited = set()
|
|
79
|
+
num = 0
|
|
80
|
+
|
|
81
|
+
def recurse(w: Node) -> int:
|
|
82
|
+
nums = []
|
|
83
|
+
for e in get_adj_edges_T(T, w):
|
|
84
|
+
if e in visited:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
visited.add(e)
|
|
88
|
+
nums.append(recurse(opposite(w, e)))
|
|
89
|
+
|
|
90
|
+
nonlocal num
|
|
91
|
+
w.po_num = num
|
|
92
|
+
w.lowest_po_num = min(nums + [num])
|
|
93
|
+
num += 1
|
|
94
|
+
return w.lowest_po_num
|
|
95
|
+
|
|
96
|
+
recurse(v)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def compute_cut_values(H: nx.MultiDiGraph[Node], T: nx.MultiDiGraph[Node]) -> None:
|
|
100
|
+
unknown_cut_values = {}
|
|
101
|
+
leaves = []
|
|
102
|
+
for v in H:
|
|
103
|
+
adj_edges = get_adj_edges_T(T, v)
|
|
104
|
+
unknown_cut_values[v] = list(adj_edges)
|
|
105
|
+
if len(adj_edges) == 1:
|
|
106
|
+
leaves.append(v)
|
|
107
|
+
|
|
108
|
+
for v in leaves:
|
|
109
|
+
while len(unknown_cut_values[v]) == 1:
|
|
110
|
+
to_determine = unknown_cut_values[v][0]
|
|
111
|
+
d = T.edges[to_determine]
|
|
112
|
+
d['cut_value'] = H.edges[to_determine]['weight']
|
|
113
|
+
u, w, _ = to_determine
|
|
114
|
+
for e in get_adj_edges_H(H, v):
|
|
115
|
+
if e == to_determine:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
weight = H.edges[e]['weight']
|
|
119
|
+
if e in T.edges:
|
|
120
|
+
if u == e[0] or w == e[1]:
|
|
121
|
+
d['cut_value'] -= T.edges[e]['cut_value'] - weight
|
|
122
|
+
else:
|
|
123
|
+
d['cut_value'] += T.edges[e]['cut_value'] - weight
|
|
124
|
+
else:
|
|
125
|
+
if (v == u and e[0] != v) or (v != u and e[0] == v):
|
|
126
|
+
weight = -weight
|
|
127
|
+
d['cut_value'] += weight
|
|
128
|
+
|
|
129
|
+
unknown_cut_values[u].remove(to_determine)
|
|
130
|
+
unknown_cut_values[w].remove(to_determine)
|
|
131
|
+
v = w if u == v else u
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def feasible_tree(H: nx.MultiDiGraph[Node]) -> nx.MultiDiGraph[Node]:
|
|
135
|
+
generations = nx.topological_generations(nx.reverse_view(H)) # type: ignore
|
|
136
|
+
for i, col in enumerate(reversed(tuple(generations))):
|
|
137
|
+
for v in col:
|
|
138
|
+
v.rank = i
|
|
139
|
+
|
|
140
|
+
T = nx.MultiDiGraph()
|
|
141
|
+
v_root = next(iter(H))
|
|
142
|
+
|
|
143
|
+
while tight_tree(H, T, v_root) < len(H):
|
|
144
|
+
incident_edges = [(u, v, k) for u, v, k in H.edges(keys=True) if (u in T) ^ (v in T)]
|
|
145
|
+
e = min(incident_edges, key=get_slack)
|
|
146
|
+
slack = -get_slack(e) if e[1] in T else get_slack(e)
|
|
147
|
+
for v in T:
|
|
148
|
+
v.rank += slack
|
|
149
|
+
|
|
150
|
+
set_post_order_numbers(v_root, T)
|
|
151
|
+
compute_cut_values(H, T)
|
|
152
|
+
|
|
153
|
+
return T
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def leave_edge(T: nx.MultiDiGraph[Node]) -> MultiEdge | None:
|
|
157
|
+
return next(((u, v, k) for u, v, k, c in T.edges.data('cut_value', keys=True) if c < 0), None)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def is_in_head(v: Node, e: MultiEdge) -> bool:
|
|
161
|
+
u, w, _ = e
|
|
162
|
+
|
|
163
|
+
if u.lowest_po_num <= v.po_num and v.po_num <= u.po_num and w.lowest_po_num <= v.po_num and v.po_num <= w.po_num:
|
|
164
|
+
return u.po_num >= w.po_num
|
|
165
|
+
|
|
166
|
+
return u.po_num < w.po_num
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def enter_edge(H: nx.MultiDiGraph[Node], e: MultiEdge) -> MultiEdge:
|
|
170
|
+
edges = [f for f in H.edges(keys=True) if is_in_head(f[0], e) and not is_in_head(f[1], e)]
|
|
171
|
+
return min(edges, key=get_slack)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def exchange(
|
|
175
|
+
H: nx.MultiDiGraph[Node],
|
|
176
|
+
T: nx.MultiDiGraph[Node],
|
|
177
|
+
leave: MultiEdge,
|
|
178
|
+
enter: MultiEdge,
|
|
179
|
+
) -> None:
|
|
180
|
+
T.remove_edge(*leave)
|
|
181
|
+
T.add_edge(*enter)
|
|
182
|
+
|
|
183
|
+
slack = get_slack(enter)
|
|
184
|
+
if not is_in_head(enter[1], leave):
|
|
185
|
+
slack = -slack
|
|
186
|
+
|
|
187
|
+
for v in H:
|
|
188
|
+
if not is_in_head(v, leave):
|
|
189
|
+
v.rank += slack
|
|
190
|
+
|
|
191
|
+
get_adj_edges_T.cache_clear()
|
|
192
|
+
|
|
193
|
+
set_post_order_numbers(v, T)
|
|
194
|
+
compute_cut_values(H, T)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def normalize_and_balance(CG: ClusterGraph, H: nx.DiGraph[Node]) -> None:
|
|
198
|
+
for cc in nx.weakly_connected_components(CG.G):
|
|
199
|
+
c = next(iter(cc)).cluster
|
|
200
|
+
assert c
|
|
201
|
+
|
|
202
|
+
if any(v.cluster != c for v in cc):
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
ranked = group_by(cc, key=lambda v: v.rank, sort=True)
|
|
206
|
+
|
|
207
|
+
if c.node:
|
|
208
|
+
start = min(v.rank for v in CG.T[c] if v.type != Kind.CLUSTER)
|
|
209
|
+
else:
|
|
210
|
+
start = c.left.rank - (max(ranked.values()) - min(ranked.values()))
|
|
211
|
+
|
|
212
|
+
for i, col in enumerate(ranked, start):
|
|
213
|
+
for v in col:
|
|
214
|
+
v.rank = i
|
|
215
|
+
|
|
216
|
+
col_sizes = []
|
|
217
|
+
for i, col in enumerate(group_by(H, key=lambda v: v.rank, sort=True)):
|
|
218
|
+
col_sizes.append(len(col))
|
|
219
|
+
for v in col:
|
|
220
|
+
v.rank = i
|
|
221
|
+
|
|
222
|
+
for v in H:
|
|
223
|
+
if len(H.in_edges(v)) != len(H.out_edges(v)):
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
start = v.rank - min([v.rank - u.rank for u in H.pred[v]], default=-1) + 1
|
|
227
|
+
stop = v.rank + min([w.rank - v.rank for w in H[v]], default=-1)
|
|
228
|
+
new_rank = max(range(start, stop), key=lambda i: col_sizes[i], default=v.rank)
|
|
229
|
+
|
|
230
|
+
if col_sizes[new_rank] < col_sizes[v.rank]:
|
|
231
|
+
col_sizes[v.rank] -= 1
|
|
232
|
+
col_sizes[new_rank] += 1
|
|
233
|
+
v.rank = new_rank
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
_BASE_ITER_LIMIT = 50
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def compute_ranks(CG: ClusterGraph) -> None:
|
|
240
|
+
for i, layer in enumerate(nx.topological_generations(CG.T)):
|
|
241
|
+
for c in CG.S.intersection(layer):
|
|
242
|
+
c.nesting_level = i
|
|
243
|
+
|
|
244
|
+
H = get_nesting_graph(CG)
|
|
245
|
+
nx.set_edge_attributes(H, 1, 'weight') # type: ignore
|
|
246
|
+
|
|
247
|
+
T = feasible_tree(H)
|
|
248
|
+
i = 0
|
|
249
|
+
iter_limit = _BASE_ITER_LIMIT * sqrt(len(H))
|
|
250
|
+
while (e := leave_edge(T)) and i < iter_limit:
|
|
251
|
+
exchange(H, T, e, enter_edge(H, e))
|
|
252
|
+
i += 1
|
|
253
|
+
|
|
254
|
+
root = next(c for c in CG.S if not CG.T.pred[c])
|
|
255
|
+
H.remove_nodes_from((root.left, root.right))
|
|
256
|
+
normalize_and_balance(CG, H)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from itertools import chain
|
|
6
|
+
from math import isclose
|
|
7
|
+
from statistics import fmean
|
|
8
|
+
|
|
9
|
+
import networkx as nx
|
|
10
|
+
from bpy.types import Node as BlenderNode
|
|
11
|
+
from mathutils import Vector
|
|
12
|
+
|
|
13
|
+
from .. import config
|
|
14
|
+
from ..utils import get_ntree, move
|
|
15
|
+
from .graph import (
|
|
16
|
+
FROM_SOCKET,
|
|
17
|
+
TO_SOCKET,
|
|
18
|
+
Cluster,
|
|
19
|
+
ClusterGraph,
|
|
20
|
+
Kind,
|
|
21
|
+
Node,
|
|
22
|
+
Socket,
|
|
23
|
+
add_dummy_edge,
|
|
24
|
+
get_reroute_paths,
|
|
25
|
+
is_real,
|
|
26
|
+
socket_graph,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_safe_to_remove(v: Node) -> bool:
|
|
31
|
+
if not is_real(v):
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
if v.node.label:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
for val in config.multi_input_sort_ids.values():
|
|
38
|
+
if any(v == i[0].owner for i in val):
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
return all(
|
|
42
|
+
s.node.select for s in chain(
|
|
43
|
+
config.linked_sockets[v.node.inputs[0]],
|
|
44
|
+
config.linked_sockets[v.node.outputs[0]],
|
|
45
|
+
))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def dissolve_reroute_edges(G: nx.DiGraph[Node], path: list[Node]) -> None:
|
|
49
|
+
if not G[path[-1]]:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
u, _, o = next(iter(G.in_edges(path[0], data=FROM_SOCKET)))
|
|
54
|
+
except StopIteration:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
succ_inputs = [e[2] for e in G.out_edges(path[-1], data=TO_SOCKET)]
|
|
58
|
+
|
|
59
|
+
# Check if a reroute has been used to link the same output to the same multi-input multiple
|
|
60
|
+
# times
|
|
61
|
+
for *_, d in G.out_edges(u, data=True):
|
|
62
|
+
if d[FROM_SOCKET] == o and d[TO_SOCKET] in succ_inputs:
|
|
63
|
+
path.clear()
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
links = get_ntree().links
|
|
67
|
+
for i in succ_inputs:
|
|
68
|
+
G.add_edge(u, i.owner, from_socket=o, to_socket=i)
|
|
69
|
+
links.new(o.bpy, i.bpy)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def remove_reroutes(CG: ClusterGraph) -> None:
|
|
73
|
+
reroute_clusters = {#
|
|
74
|
+
c for c in CG.S
|
|
75
|
+
if all(v.type != Kind.CLUSTER and v.is_reroute for v in CG.T[c])}
|
|
76
|
+
for path in get_reroute_paths(CG, is_safe_to_remove):
|
|
77
|
+
if path[0].cluster in reroute_clusters:
|
|
78
|
+
if len(path) > 2:
|
|
79
|
+
u, *between, v = path
|
|
80
|
+
add_dummy_edge(CG.G, u, v)
|
|
81
|
+
CG.remove_nodes_from(between)
|
|
82
|
+
else:
|
|
83
|
+
dissolve_reroute_edges(CG.G, path)
|
|
84
|
+
CG.remove_nodes_from(path)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
_Y_TOL = 5
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def simplify_path(CG: ClusterGraph, path: list[Node]) -> None:
|
|
91
|
+
G = CG.G
|
|
92
|
+
pred_output = lambda w: next(iter(G.in_edges(w, data=FROM_SOCKET)))[2]
|
|
93
|
+
succ_input = lambda w: next(iter(G.out_edges(w, data=TO_SOCKET)))[2]
|
|
94
|
+
|
|
95
|
+
if len(path) == 1:
|
|
96
|
+
v = path[0]
|
|
97
|
+
|
|
98
|
+
if not G.pred[v] or G.out_degree[v] != 1 or v.col is None or is_real(v):
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
p = pred_output(v)
|
|
102
|
+
q = succ_input(v)
|
|
103
|
+
if isclose(p.y, q.y, rel_tol=0, abs_tol=_Y_TOL):
|
|
104
|
+
G.add_edge(p.owner, q.owner, from_socket=p, to_socket=q)
|
|
105
|
+
CG.remove_nodes_from(path)
|
|
106
|
+
path.clear()
|
|
107
|
+
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
u, *between, v = path
|
|
111
|
+
|
|
112
|
+
if G.pred[u] and isclose((p := pred_output(u)).y, u.y, rel_tol=0, abs_tol=_Y_TOL):
|
|
113
|
+
between.append(u)
|
|
114
|
+
else:
|
|
115
|
+
p = Socket(u, 0, True)
|
|
116
|
+
|
|
117
|
+
if G.out_degree[v] == 1 and isclose(v.y, (q := succ_input(v)).y, rel_tol=0, abs_tol=_Y_TOL):
|
|
118
|
+
between.append(v)
|
|
119
|
+
else:
|
|
120
|
+
q = Socket(v, 0, False)
|
|
121
|
+
|
|
122
|
+
if p.owner != u or q.owner != v or between:
|
|
123
|
+
G.add_edge(p.owner, q.owner, from_socket=p, to_socket=q)
|
|
124
|
+
|
|
125
|
+
CG.remove_nodes_from(between)
|
|
126
|
+
for v in between:
|
|
127
|
+
path.remove(v)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def add_reroute(v: Node) -> None:
|
|
131
|
+
reroute = get_ntree().nodes.new(type='NodeReroute')
|
|
132
|
+
assert v.cluster
|
|
133
|
+
reroute.parent = v.cluster.node
|
|
134
|
+
config.selected.append(reroute)
|
|
135
|
+
v.node = reroute
|
|
136
|
+
v.type = Kind.NODE
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def realize_edges(G: nx.DiGraph[Node]) -> None:
|
|
140
|
+
links = get_ntree().links
|
|
141
|
+
for u, v, d in G.edges.data():
|
|
142
|
+
if u.is_reroute or v.is_reroute:
|
|
143
|
+
links.new(d[FROM_SOCKET].bpy, d[TO_SOCKET].bpy)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def realize_dummy_nodes(CG: ClusterGraph) -> None:
|
|
147
|
+
for path in get_reroute_paths(CG, is_safe_to_remove, aligned=True):
|
|
148
|
+
simplify_path(CG, path)
|
|
149
|
+
|
|
150
|
+
for v in path:
|
|
151
|
+
if not is_real(v):
|
|
152
|
+
add_reroute(v)
|
|
153
|
+
|
|
154
|
+
realize_edges(CG.G)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def restore_multi_input_orders(G: nx.MultiDiGraph[Node]) -> None:
|
|
158
|
+
links = get_ntree().links
|
|
159
|
+
H = socket_graph(G)
|
|
160
|
+
for socket, sort_ids in config.multi_input_sort_ids.items():
|
|
161
|
+
multi_input = socket.bpy
|
|
162
|
+
assert multi_input
|
|
163
|
+
|
|
164
|
+
as_links = {l.from_socket: l for l in links if l.to_socket == multi_input}
|
|
165
|
+
|
|
166
|
+
for output in {s.bpy for s in H.pred[socket]} - as_links.keys():
|
|
167
|
+
assert output
|
|
168
|
+
as_links[output] = links.new(output, multi_input)
|
|
169
|
+
|
|
170
|
+
if len(as_links) != len({l.multi_input_sort_id for l in as_links.values()}):
|
|
171
|
+
for link in as_links.values():
|
|
172
|
+
links.remove(link)
|
|
173
|
+
|
|
174
|
+
for output in as_links:
|
|
175
|
+
as_links[output] = links.new(output, multi_input)
|
|
176
|
+
|
|
177
|
+
SH = H.subgraph({i[0] for i in sort_ids} | {socket} | {v for v in H if v.owner.is_reroute})
|
|
178
|
+
seen = set()
|
|
179
|
+
for base_from_socket, sort_id in sort_ids:
|
|
180
|
+
other = min(as_links.values(), key=lambda l: abs(l.multi_input_sort_id - sort_id))
|
|
181
|
+
from_socket = next(
|
|
182
|
+
s for s, t in nx.edge_dfs(SH, base_from_socket) if t == socket and s not in seen)
|
|
183
|
+
as_links[from_socket.bpy].swap_multi_input_sort_id(other) # type: ignore
|
|
184
|
+
seen.add(from_socket)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def realize_locations(G: nx.DiGraph[Node], old_center: Vector) -> None:
|
|
188
|
+
new_center = (fmean([v.x for v in G]), fmean([v.y for v in G]))
|
|
189
|
+
offset_x, offset_y = -Vector(new_center) + old_center
|
|
190
|
+
|
|
191
|
+
for v in G:
|
|
192
|
+
assert isinstance(v.node, BlenderNode)
|
|
193
|
+
assert v.cluster
|
|
194
|
+
|
|
195
|
+
# Optimization: avoid using bpy.ops for as many nodes as possible (see `utils.move()`)
|
|
196
|
+
v.node.parent = None
|
|
197
|
+
|
|
198
|
+
x, y = v.node.location
|
|
199
|
+
v.x += offset_x
|
|
200
|
+
v.y += offset_y
|
|
201
|
+
move(v.node, x=v.x - x, y=v.corrected_y() - y)
|
|
202
|
+
|
|
203
|
+
v.node.parent = v.cluster.node
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def resize_unshrunken_frame(CG: ClusterGraph, cluster: Cluster) -> None:
|
|
207
|
+
frame = cluster.node
|
|
208
|
+
|
|
209
|
+
if not frame or frame.shrink:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
real_children = [v for v in CG.T[cluster] if is_real(v)]
|
|
213
|
+
|
|
214
|
+
for v in real_children:
|
|
215
|
+
v.node.parent = None
|
|
216
|
+
|
|
217
|
+
frame.shrink = False
|
|
218
|
+
frame.shrink = True
|
|
219
|
+
|
|
220
|
+
for v in real_children:
|
|
221
|
+
v.node.parent = frame
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def realize_layout(CG: ClusterGraph, old_center: Vector) -> None:
|
|
225
|
+
if config.SETTINGS.add_reroutes:
|
|
226
|
+
realize_dummy_nodes(CG)
|
|
227
|
+
|
|
228
|
+
restore_multi_input_orders(CG.G)
|
|
229
|
+
realize_locations(CG.G, old_center)
|
|
230
|
+
for c in CG.S:
|
|
231
|
+
resize_unshrunken_frame(CG, c)
|