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.
@@ -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)