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,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