nodebpy 0.1.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/__init__.py +12 -0
- nodebpy/arrange.py +362 -0
- nodebpy/builder.py +931 -0
- nodebpy/nodes/__init__.py +12 -0
- nodebpy/nodes/attribute.py +580 -0
- nodebpy/nodes/curve.py +2006 -0
- nodebpy/nodes/geometry.py +7304 -0
- nodebpy/nodes/input.py +762 -0
- nodebpy/nodes/manually_specified.py +1356 -0
- nodebpy/nodes/mesh.py +1408 -0
- nodebpy/nodes/types.py +119 -0
- nodebpy/nodes/utilities.py +2344 -0
- nodebpy/screenshot.py +531 -0
- nodebpy/screenshot_subprocess.py +422 -0
- nodebpy/sockets.py +46 -0
- nodebpy-0.1.0.dist-info/METADATA +160 -0
- nodebpy-0.1.0.dist-info/RECORD +19 -0
- nodebpy-0.1.0.dist-info/WHEEL +4 -0
- nodebpy-0.1.0.dist-info/entry_points.txt +3 -0
nodebpy/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from . import nodes, sockets, screenshot
|
|
2
|
+
from .builder import TreeBuilder
|
|
3
|
+
from .screenshot import generate_mermaid_diagram, save_mermaid_diagram
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"nodes",
|
|
7
|
+
"sockets",
|
|
8
|
+
"screenshot",
|
|
9
|
+
"TreeBuilder",
|
|
10
|
+
"generate_mermaid_diagram",
|
|
11
|
+
"save_mermaid_diagram",
|
|
12
|
+
]
|
nodebpy/arrange.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from collections import Counter, deque
|
|
3
|
+
import bpy
|
|
4
|
+
from mathutils import Vector
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def contains_geo_socket(sockets: bpy.types.NodeInputs | bpy.types.NodeOutputs) -> bool:
|
|
8
|
+
"""
|
|
9
|
+
Check if any socket in the collection is a geometry socket
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
sockets : bpy.types.NodeInputs | bpy.types.NodeOutputs
|
|
14
|
+
Collection of node sockets to check
|
|
15
|
+
|
|
16
|
+
Returns
|
|
17
|
+
-------
|
|
18
|
+
bool
|
|
19
|
+
True if any socket is a geometry socket
|
|
20
|
+
"""
|
|
21
|
+
return any([s.bl_idname == "NodeSocketGeometry" for s in sockets])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def node_has_geo_socket(node: bpy.types.GeometryNode) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Check if a node has any geometry sockets in its inputs or outputs
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
node : bpy.types.GeometryNode
|
|
31
|
+
The node to check
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
bool
|
|
36
|
+
True if the node has at least one geometry socket
|
|
37
|
+
"""
|
|
38
|
+
return any([contains_geo_socket(x) for x in [node.inputs, node.outputs]])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_dependency_graph(tree: bpy.types.NodeTree) -> tuple[dict, Counter]:
|
|
42
|
+
"""
|
|
43
|
+
Build a graph representing node dependencies and count input connections
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
tree : bpy.types.NodeTree
|
|
48
|
+
The node tree to analyze
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
tuple
|
|
53
|
+
Contains:
|
|
54
|
+
dict
|
|
55
|
+
Mapping of nodes to their dependent nodes
|
|
56
|
+
Counter
|
|
57
|
+
Count of connections for each socket
|
|
58
|
+
"""
|
|
59
|
+
dependency_graph = {node: set() for node in tree.nodes}
|
|
60
|
+
socket_input_connection_count = Counter()
|
|
61
|
+
|
|
62
|
+
# populate the graph based on node connections
|
|
63
|
+
for link in tree.links:
|
|
64
|
+
dependency_graph[link.from_node].add(link.to_node)
|
|
65
|
+
socket_input_connection_count[link.to_socket] += 1
|
|
66
|
+
|
|
67
|
+
return dependency_graph, socket_input_connection_count
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def topological_sort(dependency_graph: dict) -> list:
|
|
71
|
+
"""
|
|
72
|
+
Sort nodes by their dependencies using a topological sort algorithm
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
dependency_graph : dict
|
|
77
|
+
Mapping of nodes to their dependent nodes
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
list
|
|
82
|
+
Nodes sorted in topological order
|
|
83
|
+
"""
|
|
84
|
+
# count incoming connections for each node
|
|
85
|
+
incoming_connection_count = {node: 0 for node in dependency_graph}
|
|
86
|
+
for source_node in dependency_graph:
|
|
87
|
+
for target_node in dependency_graph[source_node]:
|
|
88
|
+
incoming_connection_count[target_node] += 1
|
|
89
|
+
|
|
90
|
+
# start with nodes that have no dependencies
|
|
91
|
+
processing_queue = deque(
|
|
92
|
+
[
|
|
93
|
+
node
|
|
94
|
+
for node in incoming_connection_count
|
|
95
|
+
if incoming_connection_count[node] == 0
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
sorted_node_order = []
|
|
99
|
+
|
|
100
|
+
# process nodes in breadth-first order
|
|
101
|
+
while processing_queue:
|
|
102
|
+
current_node = processing_queue.popleft()
|
|
103
|
+
sorted_node_order.append(current_node)
|
|
104
|
+
|
|
105
|
+
# update counts for nodes that depend on the current node
|
|
106
|
+
for dependent_node in dependency_graph[current_node]:
|
|
107
|
+
incoming_connection_count[dependent_node] -= 1
|
|
108
|
+
# if all dependencies are processed, add to queue
|
|
109
|
+
if incoming_connection_count[dependent_node] == 0:
|
|
110
|
+
processing_queue.append(dependent_node)
|
|
111
|
+
|
|
112
|
+
return sorted_node_order
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def organize_into_columns(nodes_in_order: list, dependency_graph: dict) -> list:
|
|
116
|
+
"""
|
|
117
|
+
Organize nodes into columns based on their dependencies
|
|
118
|
+
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
nodes_in_order : list
|
|
122
|
+
Nodes sorted in topological order
|
|
123
|
+
dependency_graph : dict
|
|
124
|
+
Mapping of nodes to their dependent nodes
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
list
|
|
129
|
+
Columns of nodes, where each column is a list of nodes
|
|
130
|
+
"""
|
|
131
|
+
node_columns = []
|
|
132
|
+
node_column_assignment = {}
|
|
133
|
+
|
|
134
|
+
for node in reversed(nodes_in_order):
|
|
135
|
+
# node goes in column after its furthest dependent
|
|
136
|
+
node_column_assignment[node] = (
|
|
137
|
+
max(
|
|
138
|
+
[
|
|
139
|
+
node_column_assignment[dependent_node]
|
|
140
|
+
for dependent_node in dependency_graph[node]
|
|
141
|
+
],
|
|
142
|
+
default=-1,
|
|
143
|
+
)
|
|
144
|
+
+ 1
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# add node to its assigned column
|
|
148
|
+
if node_column_assignment[node] == len(node_columns):
|
|
149
|
+
node_columns.append([node])
|
|
150
|
+
else:
|
|
151
|
+
node_columns[node_column_assignment[node]].append(node)
|
|
152
|
+
|
|
153
|
+
# reverse columns to get left-to-right flow
|
|
154
|
+
return list(reversed(node_columns))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def calculate_node_dimensions(
|
|
158
|
+
node: bpy.types.Node, socket_input_connection_count: Counter, interface_scale: float
|
|
159
|
+
) -> tuple[float, float]:
|
|
160
|
+
"""
|
|
161
|
+
Calculate the visual dimensions of a node
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
node : bpy.types.Node
|
|
166
|
+
The node to calculate dimensions for
|
|
167
|
+
socket_input_connection_count : Counter
|
|
168
|
+
Counter of connections for each socket
|
|
169
|
+
interface_scale : float
|
|
170
|
+
UI scale factor
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
tuple[float, float]
|
|
175
|
+
Width and height of the node
|
|
176
|
+
"""
|
|
177
|
+
# height constants for different node elements
|
|
178
|
+
node_header_height = 20
|
|
179
|
+
node_socket_height = 28
|
|
180
|
+
node_property_row_height = 28
|
|
181
|
+
node_vector_input_height = 84
|
|
182
|
+
|
|
183
|
+
# count enabled inputs and outputs
|
|
184
|
+
enabled_input_count = len(
|
|
185
|
+
list(filter(lambda input_socket: input_socket.enabled, node.inputs))
|
|
186
|
+
)
|
|
187
|
+
enabled_output_count = len(
|
|
188
|
+
list(filter(lambda output_socket: output_socket.enabled, node.outputs))
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# get properties specific to this node type (not inherited)
|
|
192
|
+
inherited_property_ids = [
|
|
193
|
+
property.identifier
|
|
194
|
+
for base_class in type(node).__bases__
|
|
195
|
+
for property in base_class.bl_rna.properties
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
node_specific_property_count = len(
|
|
199
|
+
[
|
|
200
|
+
property
|
|
201
|
+
for property in node.bl_rna.properties
|
|
202
|
+
if property.identifier not in inherited_property_ids
|
|
203
|
+
]
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# count vector inputs that need UI widgets (not connected)
|
|
207
|
+
unconnected_vector_input_count = len(
|
|
208
|
+
list(
|
|
209
|
+
filter(
|
|
210
|
+
lambda input_socket: input_socket.enabled
|
|
211
|
+
and input_socket.type == "VECTOR"
|
|
212
|
+
and socket_input_connection_count[input_socket] == 0,
|
|
213
|
+
node.inputs,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# calculate total node height based on components
|
|
219
|
+
total_node_height = (
|
|
220
|
+
node_header_height
|
|
221
|
+
+ (enabled_output_count * node_socket_height)
|
|
222
|
+
+ (node_specific_property_count * node_property_row_height)
|
|
223
|
+
+ (enabled_input_count * node_socket_height)
|
|
224
|
+
+ (unconnected_vector_input_count * node_vector_input_height)
|
|
225
|
+
) * interface_scale
|
|
226
|
+
|
|
227
|
+
return node.width, total_node_height
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def position_nodes_in_columns(
|
|
231
|
+
node_columns: list,
|
|
232
|
+
socket_input_connection_count: Counter,
|
|
233
|
+
spacing: typing.Tuple[float, float] = (50, 25),
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Position nodes in columns with appropriate spacing
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
node_columns : list
|
|
240
|
+
List of columns, where each column is a list of nodes
|
|
241
|
+
socket_input_connection_count : Counter
|
|
242
|
+
Counter of connections for each socket
|
|
243
|
+
spacing : tuple of float, optional
|
|
244
|
+
Tuple of (horizontal, vertical) spacing between nodes, by default (50, 25)
|
|
245
|
+
"""
|
|
246
|
+
interface_scale = bpy.context.preferences.view.ui_scale
|
|
247
|
+
non_geo_offset = 20 + 28 * 2 # header + 2 socket heights
|
|
248
|
+
|
|
249
|
+
# position nodes column by column
|
|
250
|
+
position_x = 0
|
|
251
|
+
for column in node_columns:
|
|
252
|
+
widest_node_in_column = 0
|
|
253
|
+
position_y = 0
|
|
254
|
+
|
|
255
|
+
for node in column:
|
|
256
|
+
node.update()
|
|
257
|
+
|
|
258
|
+
width, height = calculate_node_dimensions(
|
|
259
|
+
node, socket_input_connection_count, interface_scale
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# track widest node for column spacing
|
|
263
|
+
if width > widest_node_in_column:
|
|
264
|
+
widest_node_in_column = width
|
|
265
|
+
|
|
266
|
+
# position node
|
|
267
|
+
node.location = (position_x, position_y)
|
|
268
|
+
|
|
269
|
+
# adjust position for non-geometry nodes
|
|
270
|
+
if not node_has_geo_socket(node):
|
|
271
|
+
node.location -= Vector((0, non_geo_offset))
|
|
272
|
+
|
|
273
|
+
# move down for next node with spacing
|
|
274
|
+
position_y -= height + spacing[1]
|
|
275
|
+
|
|
276
|
+
# move right for next column with spacing
|
|
277
|
+
position_x += widest_node_in_column + spacing[0]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def position_special_nodes(
|
|
281
|
+
tree: bpy.types.NodeTree, vertical_offset: float = 100
|
|
282
|
+
) -> None:
|
|
283
|
+
"""Position special nodes like Group Input and Group Output at the top
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
tree : bpy.types.NodeTree
|
|
288
|
+
The node tree to modify
|
|
289
|
+
vertical_offset : float, optional
|
|
290
|
+
Vertical offset above the highest node, by default 100
|
|
291
|
+
|
|
292
|
+
Returns
|
|
293
|
+
-------
|
|
294
|
+
None
|
|
295
|
+
"""
|
|
296
|
+
highest_y_position = max([node.location[1] for node in tree.nodes])
|
|
297
|
+
for special_node_name in ["Group Input", "Group Output"]:
|
|
298
|
+
if special_node_name in tree.nodes:
|
|
299
|
+
special_node = tree.nodes[special_node_name]
|
|
300
|
+
special_node.location = (
|
|
301
|
+
special_node.location[0],
|
|
302
|
+
highest_y_position + vertical_offset,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def cleanup_orphaned_nodes(
|
|
307
|
+
tree: bpy.types.NodeTree, max_iter: int = 100, add_group_input: bool = True
|
|
308
|
+
) -> None:
|
|
309
|
+
"""Remove nodes that are not connected to anything"""
|
|
310
|
+
to_remove = []
|
|
311
|
+
for _ in range(max_iter):
|
|
312
|
+
for node in tree.nodes:
|
|
313
|
+
if len(node.outputs) == 0:
|
|
314
|
+
continue
|
|
315
|
+
if not any([s.is_linked for s in node.outputs]):
|
|
316
|
+
to_remove.append(node)
|
|
317
|
+
|
|
318
|
+
if len(to_remove) == 0:
|
|
319
|
+
break
|
|
320
|
+
|
|
321
|
+
for node in to_remove:
|
|
322
|
+
tree.nodes.remove(node)
|
|
323
|
+
|
|
324
|
+
to_remove = []
|
|
325
|
+
|
|
326
|
+
if add_group_input and "Group Input" not in tree.nodes:
|
|
327
|
+
n_input = tree.nodes.new("NodeGroupInput")
|
|
328
|
+
if "Join Geometry" in tree.nodes:
|
|
329
|
+
tree.links.new(n_input.outputs[0], tree.nodes["Join Geometry"].inputs[0])
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def arrange_tree(
|
|
333
|
+
tree: bpy.types.NodeTree,
|
|
334
|
+
spacing: typing.Tuple[float, float] = (50, 25),
|
|
335
|
+
add_group_input: bool = True,
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Arrange nodes in a node tree based on their dependencies
|
|
338
|
+
|
|
339
|
+
Parameters
|
|
340
|
+
----------
|
|
341
|
+
tree : bpy.types.GeometryNodeTree
|
|
342
|
+
The node tree to arrange
|
|
343
|
+
spacing : tuple of float
|
|
344
|
+
Tuple of (horizontal, vertical) spacing between nodes
|
|
345
|
+
|
|
346
|
+
Returns
|
|
347
|
+
-------
|
|
348
|
+
None
|
|
349
|
+
This function modifies the node tree in place
|
|
350
|
+
|
|
351
|
+
Notes
|
|
352
|
+
-----
|
|
353
|
+
This function organizes nodes into columns and positions them with appropriate spacing.
|
|
354
|
+
Nodes are arranged from left to right based on their dependencies, with special
|
|
355
|
+
handling for geometry nodes and group input/output nodes.
|
|
356
|
+
"""
|
|
357
|
+
cleanup_orphaned_nodes(tree, add_group_input=add_group_input)
|
|
358
|
+
dependency_graph, socket_input_connection_count = build_dependency_graph(tree)
|
|
359
|
+
nodes_in_dependency_order = topological_sort(dependency_graph)
|
|
360
|
+
node_columns = organize_into_columns(nodes_in_dependency_order, dependency_graph)
|
|
361
|
+
position_nodes_in_columns(node_columns, socket_input_connection_count, spacing)
|
|
362
|
+
position_special_nodes(tree)
|