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 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, tree: GeometryNodeTree | str = "Geometry Nodes", arrange: bool = True
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
- settings = arrangebpy.LayoutSettings(
164
- horizontal_spacing=200, vertical_spacing=200, align_top_layer=True
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
- return self.tree.nodes.new(name)
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,2 @@
1
+ # SPDX-License-Identifier: GPL-2.0-or-later
2
+ from .arrange import sugiyama
@@ -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