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/builder.py
ADDED
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Literal
|
|
4
|
+
|
|
5
|
+
import arrangebpy
|
|
6
|
+
import bpy
|
|
7
|
+
from bpy.types import (
|
|
8
|
+
GeometryNodeTree,
|
|
9
|
+
Node,
|
|
10
|
+
Nodes,
|
|
11
|
+
NodeSocket,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .nodes.types import (
|
|
15
|
+
FloatInterfaceSubtypes,
|
|
16
|
+
IntegerInterfaceSubtypes,
|
|
17
|
+
StringInterfaceSubtypes,
|
|
18
|
+
VectorInterfaceSubtypes,
|
|
19
|
+
_AttributeDomains,
|
|
20
|
+
)
|
|
21
|
+
# from .arrange import arrange_tree
|
|
22
|
+
|
|
23
|
+
GEO_NODE_NAMES = (
|
|
24
|
+
f"GeometryNode{name}"
|
|
25
|
+
for name in (
|
|
26
|
+
"SetPosition",
|
|
27
|
+
"TransformGeometry",
|
|
28
|
+
"GroupInput",
|
|
29
|
+
"GroupOutput",
|
|
30
|
+
"MeshToPoints",
|
|
31
|
+
"PointsToVertices",
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# POSSIBLE_NODE_NAMES = "GeometryNode"
|
|
37
|
+
LINKABLE = "Node | NodeSocket | NodeBuilder"
|
|
38
|
+
TYPE_INPUT_VECTOR = "NodeSocketVector | Vector | NodeBuilder | list[float] | tuple[float, float, float] | None"
|
|
39
|
+
TYPE_INPUT_ROTATION = "NodeSocketRotation | Quaternion | NodeBuilder | list[float] | tuple[float, float, float, float] | None"
|
|
40
|
+
TYPE_INPUT_BOOLEAN = "NodeSocketBool | bool | NodeBuilder | None"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normalize_name(name: str) -> str:
|
|
44
|
+
"""Convert 'Geometry' or 'My Socket' to 'geometry' or 'my_socket'."""
|
|
45
|
+
return name.lower().replace(" ", "_")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def denormalize_name(attr_name: str) -> str:
|
|
49
|
+
"""Convert 'geometry' or 'my_socket' to 'Geometry' or 'My Socket'."""
|
|
50
|
+
return attr_name.replace("_", " ").title()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def source_socket(node: LINKABLE) -> NodeSocket:
|
|
54
|
+
if isinstance(node, NodeSocket):
|
|
55
|
+
return node
|
|
56
|
+
elif isinstance(node, Node):
|
|
57
|
+
return node.outputs[0]
|
|
58
|
+
elif hasattr(node, "_default_output_socket"):
|
|
59
|
+
# NodeBuilder or SocketNodeBuilder
|
|
60
|
+
return node._default_output_socket
|
|
61
|
+
else:
|
|
62
|
+
raise TypeError(f"Unsupported type: {type(node)}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def target_socket(node: LINKABLE) -> NodeSocket:
|
|
66
|
+
if isinstance(node, NodeSocket):
|
|
67
|
+
return node
|
|
68
|
+
elif isinstance(node, Node):
|
|
69
|
+
return node.inputs[0]
|
|
70
|
+
elif hasattr(node, "_default_input_socket"):
|
|
71
|
+
# NodeBuilder or SocketNodeBuilder
|
|
72
|
+
return node._default_input_socket
|
|
73
|
+
else:
|
|
74
|
+
raise TypeError(f"Unsupported type: {type(node)}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TreeBuilder:
|
|
78
|
+
"""Builder for creating Blender geometry node trees with a clean Python API."""
|
|
79
|
+
|
|
80
|
+
_active_tree: ClassVar["TreeBuilder | None"] = None
|
|
81
|
+
_previous_tree: ClassVar["TreeBuilder | None"] = None
|
|
82
|
+
just_added: "Node | None" = None
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self, tree: "GeometryNodeTree | str | None" = None, arrange: bool = True
|
|
86
|
+
):
|
|
87
|
+
if isinstance(tree, str):
|
|
88
|
+
self.tree = bpy.data.node_groups.new(tree, "GeometryNodeTree")
|
|
89
|
+
elif tree is None:
|
|
90
|
+
self.tree = bpy.data.node_groups.new("GeometryNodeTree", "GeometryNodeTree")
|
|
91
|
+
else:
|
|
92
|
+
assert isinstance(tree, GeometryNodeTree)
|
|
93
|
+
self.tree = tree
|
|
94
|
+
|
|
95
|
+
# Create socket accessors for named access
|
|
96
|
+
self.inputs = InputInterfaceContext(self)
|
|
97
|
+
self.outputs = OutputInterfaceContext(self)
|
|
98
|
+
self._arrange = arrange
|
|
99
|
+
|
|
100
|
+
def __enter__(self):
|
|
101
|
+
TreeBuilder._previous_tree = TreeBuilder._active_tree
|
|
102
|
+
TreeBuilder._active_tree = self
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def __exit__(self, *args):
|
|
106
|
+
if self._arrange:
|
|
107
|
+
self.arrange()
|
|
108
|
+
TreeBuilder._active_tree = TreeBuilder._previous_tree
|
|
109
|
+
TreeBuilder._previous_tree = None
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def nodes(self) -> Nodes:
|
|
113
|
+
return self.tree.nodes
|
|
114
|
+
|
|
115
|
+
def arrange(self):
|
|
116
|
+
settings = arrangebpy.LayoutSettings(
|
|
117
|
+
horizontal_spacing=200, vertical_spacing=200, align_top_layer=True
|
|
118
|
+
)
|
|
119
|
+
arrangebpy.sugiyama_layout(self.tree, settings)
|
|
120
|
+
|
|
121
|
+
def _repr_markdown_(self) -> str | None:
|
|
122
|
+
"""
|
|
123
|
+
Return Markdown representation for Jupyter notebook display.
|
|
124
|
+
|
|
125
|
+
This special method is called by Jupyter to display the TreeBuilder as a Mermaid diagram
|
|
126
|
+
when it's the return value of a cell.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
from .screenshot import generate_mermaid_diagram
|
|
130
|
+
|
|
131
|
+
return generate_mermaid_diagram(self)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
# Diagram generation failed - return None to let Jupyter use text representation
|
|
134
|
+
print(f"Mermaid diagram generation failed: {e}")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
def _input_node(self) -> Node:
|
|
138
|
+
"""Get or create the Group Input node."""
|
|
139
|
+
try:
|
|
140
|
+
return self.tree.nodes["Group Input"] # type: ignore
|
|
141
|
+
except KeyError:
|
|
142
|
+
return self.tree.nodes.new("NodeGroupInput") # type: ignore
|
|
143
|
+
|
|
144
|
+
def _output_node(self) -> Node:
|
|
145
|
+
"""Get or create the Group Output node."""
|
|
146
|
+
try:
|
|
147
|
+
return self.tree.nodes["Group Output"] # type: ignore
|
|
148
|
+
except KeyError:
|
|
149
|
+
return self.tree.nodes.new("NodeGroupOutput") # type: ignore
|
|
150
|
+
|
|
151
|
+
def link(self, socket1: NodeSocket, socket2: NodeSocket):
|
|
152
|
+
if isinstance(socket1, SocketLinker):
|
|
153
|
+
socket1 = socket1.socket
|
|
154
|
+
if isinstance(socket2, SocketLinker):
|
|
155
|
+
socket2 = socket2.socket
|
|
156
|
+
|
|
157
|
+
self.tree.links.new(socket1, socket2)
|
|
158
|
+
|
|
159
|
+
if any(socket.is_inactive for socket in [socket1, socket2]):
|
|
160
|
+
# the warning message should report which sockets from which nodes were linked and which were innactive
|
|
161
|
+
for socket in [socket1, socket2]:
|
|
162
|
+
if socket.is_inactive:
|
|
163
|
+
message = f"Socket {socket.name} from node {socket.node.name} is inactive."
|
|
164
|
+
message += f" It is linked to socket {socket2.name} from node {socket2.node.name}."
|
|
165
|
+
message += " This link will be created by Blender but ignored when evaluated."
|
|
166
|
+
message += f"Socket type: {socket.bl_idname}"
|
|
167
|
+
raise RuntimeError(message)
|
|
168
|
+
|
|
169
|
+
def add(self, name: str) -> Node:
|
|
170
|
+
self.just_added = self.tree.nodes.new(name) # type: ignore
|
|
171
|
+
assert self.just_added is not None
|
|
172
|
+
return self.just_added
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class SocketContext:
|
|
176
|
+
_direction: Literal["INPUT", "OUTPUT"] | None
|
|
177
|
+
_active_context: SocketContext | None = None
|
|
178
|
+
|
|
179
|
+
def __init__(self, tree_builder: TreeBuilder):
|
|
180
|
+
self.builder = tree_builder
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def tree(self) -> GeometryNodeTree:
|
|
184
|
+
tree = self.builder.tree
|
|
185
|
+
assert tree is not None and isinstance(tree, GeometryNodeTree)
|
|
186
|
+
return tree
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def interface(self) -> bpy.types.NodeTreeInterface:
|
|
190
|
+
interface = self.tree.interface
|
|
191
|
+
assert interface is not None
|
|
192
|
+
return interface
|
|
193
|
+
|
|
194
|
+
def _create_socket(
|
|
195
|
+
self, socket_def: SocketBase
|
|
196
|
+
) -> bpy.types.NodeTreeInterfaceSocket:
|
|
197
|
+
"""Create a socket from a socket definition."""
|
|
198
|
+
socket = self.interface.new_socket(
|
|
199
|
+
name=socket_def.name,
|
|
200
|
+
in_out=self._direction,
|
|
201
|
+
socket_type=socket_def._bl_socket_type,
|
|
202
|
+
)
|
|
203
|
+
socket.description = socket_def.description
|
|
204
|
+
return socket
|
|
205
|
+
|
|
206
|
+
def __enter__(self):
|
|
207
|
+
SocketContext._direction = self._direction
|
|
208
|
+
SocketContext._active_context = self
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
def __exit__(self, *args):
|
|
212
|
+
SocketContext._direction = None
|
|
213
|
+
SocketContext._active_context = None
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class InputInterfaceContext(SocketContext):
|
|
218
|
+
_direction = "INPUT"
|
|
219
|
+
_active_context = None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class OutputInterfaceContext(SocketContext):
|
|
223
|
+
_direction = "OUTPUT"
|
|
224
|
+
_active_context = None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class NodeBuilder:
|
|
228
|
+
"""Base class for all geometry node wrappers."""
|
|
229
|
+
|
|
230
|
+
node: Node
|
|
231
|
+
name: str
|
|
232
|
+
_tree: "TreeBuilder"
|
|
233
|
+
_link_target: str | None = None # Track which input should receive links
|
|
234
|
+
_from_socket: NodeSocket | None = None
|
|
235
|
+
_default_input_id: str | None = None
|
|
236
|
+
_default_output_id: str | None = None
|
|
237
|
+
|
|
238
|
+
def __init__(self):
|
|
239
|
+
# Get active tree from context manager
|
|
240
|
+
tree = TreeBuilder._active_tree
|
|
241
|
+
if tree is None:
|
|
242
|
+
raise RuntimeError(
|
|
243
|
+
f"Node '{self.__class__.__name__}' must be created within a TreeBuilder context manager.\n"
|
|
244
|
+
f"Usage:\n"
|
|
245
|
+
f" with tree:\n"
|
|
246
|
+
f" node = {self.__class__.__name__}()\n"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
self.inputs = InputInterfaceContext(tree)
|
|
250
|
+
self.outputs = OutputInterfaceContext(tree)
|
|
251
|
+
|
|
252
|
+
self._tree = tree
|
|
253
|
+
self._link_target = None
|
|
254
|
+
if self.__class__.name is not None:
|
|
255
|
+
self.node = self._tree.add(self.__class__.name)
|
|
256
|
+
else:
|
|
257
|
+
raise ValueError(
|
|
258
|
+
f"Class {self.__class__.__name__} must define a 'name' attribute"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def tree(self) -> "TreeBuilder":
|
|
263
|
+
return self._tree
|
|
264
|
+
|
|
265
|
+
@tree.setter
|
|
266
|
+
def tree(self, value: "TreeBuilder"):
|
|
267
|
+
self._tree = value
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def _default_input_socket(self) -> NodeSocket:
|
|
271
|
+
if self._default_input_id is not None:
|
|
272
|
+
return self.node.inputs[self._input_idx(self._default_input_id)]
|
|
273
|
+
return self.node.inputs[0]
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def _default_output_socket(self) -> NodeSocket:
|
|
277
|
+
if self._default_output_id is not None:
|
|
278
|
+
return self.node.outputs[self._output_idx(self._default_output_id)]
|
|
279
|
+
return self.node.outputs[0]
|
|
280
|
+
|
|
281
|
+
def _input_idx(self, identifier: str) -> int:
|
|
282
|
+
# currently there is a Blender bug that is preventing the lookup of sockets from identifiers on some
|
|
283
|
+
# nodes but not others
|
|
284
|
+
# This currently fails:
|
|
285
|
+
#
|
|
286
|
+
# node = bpy.data.node_groups["Geometry Nodes"].nodes['Mix']
|
|
287
|
+
# node.inputs[node.inputs[0].identifier]
|
|
288
|
+
#
|
|
289
|
+
# This should succeed because it should be able to lookup the socket by identifier
|
|
290
|
+
# so instead we have to convert the identifier to an index and then lookup the socket
|
|
291
|
+
# from the index instead
|
|
292
|
+
input_ids = [input.identifier for input in self.node.inputs]
|
|
293
|
+
return input_ids.index(identifier)
|
|
294
|
+
|
|
295
|
+
def _output_idx(self, identifier: str) -> int:
|
|
296
|
+
output_ids = [output.identifier for output in self.node.outputs]
|
|
297
|
+
return output_ids.index(identifier)
|
|
298
|
+
|
|
299
|
+
def _input(self, identifier: str) -> SocketLinker:
|
|
300
|
+
"""Input socket: Vector"""
|
|
301
|
+
return SocketLinker(self.node.inputs[self._input_idx(identifier)])
|
|
302
|
+
|
|
303
|
+
def _output(self, identifier: str) -> SocketLinker:
|
|
304
|
+
"""Output socket: Vector"""
|
|
305
|
+
return SocketLinker(self.node.outputs[self._output_idx(identifier)])
|
|
306
|
+
|
|
307
|
+
def link(self, source: LINKABLE, target: LINKABLE):
|
|
308
|
+
self.tree.link(source_socket(source), target_socket(target))
|
|
309
|
+
|
|
310
|
+
def link_to(self, target: LINKABLE):
|
|
311
|
+
self.tree.link(self._default_output_socket, target_socket(target))
|
|
312
|
+
|
|
313
|
+
def link_from(self, source: LINKABLE, input: "LINKABLE | str"):
|
|
314
|
+
if isinstance(input, str):
|
|
315
|
+
try:
|
|
316
|
+
self.link(source, self.node.inputs[input])
|
|
317
|
+
except KeyError:
|
|
318
|
+
self.link(source, self.node.inputs[self._input_idx(input)])
|
|
319
|
+
else:
|
|
320
|
+
self.link(source, input)
|
|
321
|
+
|
|
322
|
+
def _establish_links(self, **kwargs):
|
|
323
|
+
input_ids = [input.identifier for input in self.node.inputs]
|
|
324
|
+
for name, value in kwargs.items():
|
|
325
|
+
if value is None:
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
if value is ...:
|
|
329
|
+
# Ellipsis indicates this input should receive links from >> operator
|
|
330
|
+
# which can potentially target multiple inputs on the new node
|
|
331
|
+
if self._from_socket is not None:
|
|
332
|
+
self.link(
|
|
333
|
+
self._from_socket, self.node.inputs[self._input_idx(name)]
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# we can also provide just a default value for the socket to take if we aren't
|
|
337
|
+
# providing a socket to link with
|
|
338
|
+
elif isinstance(value, (NodeBuilder, SocketNodeBuilder, NodeSocket, Node)):
|
|
339
|
+
# print("Linking from", value, "to", name)
|
|
340
|
+
self.link_from(value, name)
|
|
341
|
+
else:
|
|
342
|
+
if name in input_ids:
|
|
343
|
+
input = self.node.inputs[input_ids.index(name)]
|
|
344
|
+
input.default_value = value
|
|
345
|
+
else:
|
|
346
|
+
input = self.node.inputs[name.replace("_", "").capitalize()]
|
|
347
|
+
input.default_value = value
|
|
348
|
+
|
|
349
|
+
def __rshift__(self, other: "NodeBuilder") -> "NodeBuilder":
|
|
350
|
+
"""Chain nodes using >> operator. Links output to input.
|
|
351
|
+
|
|
352
|
+
Usage:
|
|
353
|
+
node1 >> node2 >> node3
|
|
354
|
+
tree.inputs.value >> Math.add(..., 0.1) >> tree.outputs.result
|
|
355
|
+
|
|
356
|
+
If the target node has an ellipsis placeholder (...), links to that specific input.
|
|
357
|
+
Otherwise, tries to find Geometry sockets first, then falls back to default.
|
|
358
|
+
|
|
359
|
+
Returns the right-hand node to enable continued chaining.
|
|
360
|
+
"""
|
|
361
|
+
# Get source socket - prefer Geometry, fall back to default
|
|
362
|
+
socket_out = self.node.outputs.get("Geometry") or self._default_output_socket
|
|
363
|
+
other._from_socket = socket_out
|
|
364
|
+
|
|
365
|
+
# Get target socket
|
|
366
|
+
if other._link_target is not None:
|
|
367
|
+
# Use specific target if set by ellipsis
|
|
368
|
+
socket_in = self._get_input_socket_by_name(other, other._link_target)
|
|
369
|
+
else:
|
|
370
|
+
# Default behavior - prefer Geometry, fall back to default
|
|
371
|
+
socket_in = other.node.inputs.get("Geometry") or other._default_input_socket
|
|
372
|
+
|
|
373
|
+
# If target socket already has a link and isn't multi-input, try next available socket
|
|
374
|
+
if socket_in.links and not socket_in.is_multi_input:
|
|
375
|
+
socket_in = (
|
|
376
|
+
self._get_next_available_socket(socket_in, socket_out) or socket_in
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
self.tree.link(socket_out, socket_in)
|
|
380
|
+
return other
|
|
381
|
+
|
|
382
|
+
def _get_input_socket_by_name(self, node: "NodeBuilder", name: str) -> NodeSocket:
|
|
383
|
+
"""Get input socket by name, trying direct access first, then title case."""
|
|
384
|
+
try:
|
|
385
|
+
return node.node.inputs[name]
|
|
386
|
+
except KeyError:
|
|
387
|
+
# Try with title case if direct access fails
|
|
388
|
+
title_name = name.replace("_", " ").title()
|
|
389
|
+
return node.node.inputs[title_name]
|
|
390
|
+
|
|
391
|
+
def _get_next_available_socket(
|
|
392
|
+
self, socket: NodeSocket, socket_out: NodeSocket
|
|
393
|
+
) -> NodeSocket | None:
|
|
394
|
+
"""Get the next available socket after the given one."""
|
|
395
|
+
try:
|
|
396
|
+
inputs = socket.node.inputs
|
|
397
|
+
current_idx = inputs.find(socket.identifier)
|
|
398
|
+
if current_idx >= 0 and current_idx + 1 < len(inputs):
|
|
399
|
+
if socket_out.type == "GEOMETRY":
|
|
400
|
+
# Prefer Geometry sockets
|
|
401
|
+
for idx in range(current_idx + 1, len(inputs)):
|
|
402
|
+
if inputs[idx].type == "GEOMETRY" and not inputs[idx].links:
|
|
403
|
+
return inputs[idx]
|
|
404
|
+
raise RuntimeError("No available Geometry input sockets found.")
|
|
405
|
+
return inputs[current_idx + 1]
|
|
406
|
+
except (KeyError, IndexError, AttributeError):
|
|
407
|
+
pass
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
def __mul__(self, other: Any) -> "VectorMath | Math":
|
|
411
|
+
from .nodes import Math, VectorMath
|
|
412
|
+
|
|
413
|
+
match self._default_output_socket.type:
|
|
414
|
+
case "VECTOR":
|
|
415
|
+
if isinstance(other, (int, float)):
|
|
416
|
+
return VectorMath.scale(self._default_output_socket, other)
|
|
417
|
+
elif isinstance(other, (list, tuple)) and len(other) == 3:
|
|
418
|
+
return VectorMath.multiply(self._default_output_socket, other)
|
|
419
|
+
else:
|
|
420
|
+
raise TypeError(
|
|
421
|
+
f"Unsupported type for multiplication with VECTOR socket: {type(other)}"
|
|
422
|
+
)
|
|
423
|
+
case "VALUE":
|
|
424
|
+
return Math.multiply(self._default_output_socket, other)
|
|
425
|
+
case _:
|
|
426
|
+
raise TypeError(
|
|
427
|
+
f"Unsupported socket type for multiplication: {self._default_output_socket.type}"
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
def __rmul__(self, other: Any) -> "VectorMath | Math":
|
|
431
|
+
from .nodes import Math, VectorMath
|
|
432
|
+
|
|
433
|
+
match self._default_output_socket.type:
|
|
434
|
+
case "VECTOR":
|
|
435
|
+
if isinstance(other, (int, float)):
|
|
436
|
+
return VectorMath.scale(self._default_output_socket, other)
|
|
437
|
+
elif isinstance(other, (list, tuple)) and len(other) == 3:
|
|
438
|
+
return VectorMath.multiply(other, self._default_output_socket)
|
|
439
|
+
else:
|
|
440
|
+
raise TypeError(
|
|
441
|
+
f"Unsupported type for multiplication with VECTOR socket: {type(other)}"
|
|
442
|
+
)
|
|
443
|
+
case "VALUE":
|
|
444
|
+
return Math.multiply(other, self._default_output_socket)
|
|
445
|
+
case _:
|
|
446
|
+
raise TypeError(
|
|
447
|
+
f"Unsupported socket type for multiplication: {self._default_output_socket.type}"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def __truediv__(self, other: Any) -> "VectorMath":
|
|
451
|
+
from .nodes import VectorMath
|
|
452
|
+
|
|
453
|
+
match self._default_output_socket.type:
|
|
454
|
+
case "VECTOR":
|
|
455
|
+
return VectorMath.divide(self._default_output_socket, other)
|
|
456
|
+
case _:
|
|
457
|
+
raise TypeError(
|
|
458
|
+
f"Unsupported socket type for division: {self._default_output_socket.type}"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
def __rtruediv__(self, other: Any) -> "VectorMath":
|
|
462
|
+
from .nodes import VectorMath
|
|
463
|
+
|
|
464
|
+
match self._default_output_socket.type:
|
|
465
|
+
case "VECTOR":
|
|
466
|
+
return VectorMath.divide(other, self._default_output_socket)
|
|
467
|
+
case _:
|
|
468
|
+
raise TypeError(
|
|
469
|
+
f"Unsupported socket type for division: {self._default_output_socket.type}"
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def __add__(self, other: Any) -> "VectorMath | Math":
|
|
473
|
+
from .nodes import Math, VectorMath
|
|
474
|
+
|
|
475
|
+
match self._default_output_socket.type:
|
|
476
|
+
case "VECTOR":
|
|
477
|
+
return VectorMath.add(self._default_output_socket, other)
|
|
478
|
+
case "VALUE":
|
|
479
|
+
return Math.add(self._default_output_socket, other)
|
|
480
|
+
case _:
|
|
481
|
+
raise TypeError(
|
|
482
|
+
f"Unsupported socket type for addition: {self._default_output_socket.type}"
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
def __radd__(self, other: Any) -> "VectorMath | Math":
|
|
486
|
+
from .nodes import Math, VectorMath
|
|
487
|
+
|
|
488
|
+
match self._default_output_socket.type:
|
|
489
|
+
case "VECTOR":
|
|
490
|
+
return VectorMath.add(other, self._default_output_socket)
|
|
491
|
+
case "VALUE":
|
|
492
|
+
return Math.add(other, self._default_output_socket)
|
|
493
|
+
case _:
|
|
494
|
+
raise TypeError(
|
|
495
|
+
f"Unsupported socket type for addition: {self._default_output_socket.type}"
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class SocketLinker(NodeBuilder):
|
|
500
|
+
def __init__(self, socket: NodeSocket):
|
|
501
|
+
assert socket.node is not None
|
|
502
|
+
self.socket = socket
|
|
503
|
+
self.node = socket.node
|
|
504
|
+
self._default_output_id = socket.identifier
|
|
505
|
+
self._tree = TreeBuilder(socket.node.id_data) # type: ignore
|
|
506
|
+
|
|
507
|
+
@property
|
|
508
|
+
def type(self) -> str:
|
|
509
|
+
return self.socket.type
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class SocketNodeBuilder(NodeBuilder):
|
|
513
|
+
"""Special NodeBuilder for accessing specific sockets on input/output nodes."""
|
|
514
|
+
|
|
515
|
+
def __init__(self, node: Node, socket_name: str, direction: str):
|
|
516
|
+
# Don't call super().__init__ - we already have a node
|
|
517
|
+
self.node = node
|
|
518
|
+
self._tree = TreeBuilder(node.id_data) # type: ignore
|
|
519
|
+
self._socket_name = socket_name
|
|
520
|
+
self._direction = direction
|
|
521
|
+
|
|
522
|
+
@property
|
|
523
|
+
def _default_output_socket(self) -> NodeSocket:
|
|
524
|
+
"""Return the specific named output socket."""
|
|
525
|
+
if self._direction == "INPUT":
|
|
526
|
+
return self.node.outputs[self._socket_name]
|
|
527
|
+
else:
|
|
528
|
+
raise ValueError("Output nodes don't have outputs")
|
|
529
|
+
|
|
530
|
+
@property
|
|
531
|
+
def _default_input_socket(self) -> NodeSocket:
|
|
532
|
+
"""Return the specific named input socket."""
|
|
533
|
+
if self._direction == "OUTPUT":
|
|
534
|
+
return self.node.inputs[self._socket_name]
|
|
535
|
+
else:
|
|
536
|
+
raise ValueError("Input nodes don't have inputs")
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
class SocketBase(SocketLinker):
|
|
540
|
+
"""Base class for all socket definitions."""
|
|
541
|
+
|
|
542
|
+
_bl_socket_type: str = ""
|
|
543
|
+
|
|
544
|
+
def __init__(self, name: str, description: str = ""):
|
|
545
|
+
self.name = name
|
|
546
|
+
self.description = description
|
|
547
|
+
|
|
548
|
+
self._socket_context: SocketContext = SocketContext._active_context
|
|
549
|
+
self.interface_socket = self._socket_context._create_socket(self)
|
|
550
|
+
self._tree = self._socket_context.builder
|
|
551
|
+
if self._socket_context._direction == "INPUT":
|
|
552
|
+
socket = self.tree._input_node().outputs[self.interface_socket.identifier]
|
|
553
|
+
else:
|
|
554
|
+
socket = self.tree._output_node().inputs[self.interface_socket.identifier]
|
|
555
|
+
super().__init__(socket)
|
|
556
|
+
|
|
557
|
+
def _set_values(self, **kwargs):
|
|
558
|
+
for key, value in kwargs.items():
|
|
559
|
+
if value is None:
|
|
560
|
+
continue
|
|
561
|
+
setattr(self.interface_socket, key, value)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class SocketGeometry(SocketBase):
|
|
565
|
+
"""Geometry socket - holds mesh, curve, point cloud, or volume data."""
|
|
566
|
+
|
|
567
|
+
_bl_socket_type: str = "NodeSocketGeometry"
|
|
568
|
+
socket: bpy.types.NodeTreeInterfaceSocketGeometry
|
|
569
|
+
|
|
570
|
+
def __init__(self, name: str = "Geometry", description: str = ""):
|
|
571
|
+
super().__init__(name, description)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
class SocketBoolean(SocketBase):
|
|
575
|
+
"""Boolean socket - true/false value."""
|
|
576
|
+
|
|
577
|
+
_bl_socket_type: str = "NodeSocketBool"
|
|
578
|
+
socket: bpy.types.NodeTreeInterfaceSocketBool
|
|
579
|
+
|
|
580
|
+
def __init__(
|
|
581
|
+
self,
|
|
582
|
+
name: str = "Boolean",
|
|
583
|
+
default_value: bool = False,
|
|
584
|
+
*,
|
|
585
|
+
description: str = "",
|
|
586
|
+
hide_value: bool = False,
|
|
587
|
+
attribute_domain: _AttributeDomains = "POINT",
|
|
588
|
+
default_attribute: str | None = None,
|
|
589
|
+
):
|
|
590
|
+
super().__init__(name, description)
|
|
591
|
+
self._set_values(
|
|
592
|
+
default_value=default_value,
|
|
593
|
+
hide_value=hide_value,
|
|
594
|
+
attribute_domain=attribute_domain,
|
|
595
|
+
default_attribute=default_attribute,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
class SocketFloat(SocketBase):
|
|
600
|
+
"""Float socket"""
|
|
601
|
+
|
|
602
|
+
_bl_socket_type: str = "NodeSocketFloat"
|
|
603
|
+
socket: bpy.types.NodeTreeInterfaceSocketFloat
|
|
604
|
+
|
|
605
|
+
def __init__(
|
|
606
|
+
self,
|
|
607
|
+
name: str = "Value",
|
|
608
|
+
default_value: float = 0.0,
|
|
609
|
+
*,
|
|
610
|
+
description: str = "",
|
|
611
|
+
min_value: float | None = None,
|
|
612
|
+
max_value: float | None = None,
|
|
613
|
+
subtype: FloatInterfaceSubtypes = "NONE",
|
|
614
|
+
hide_value: bool = False,
|
|
615
|
+
attribute_domain: _AttributeDomains = "POINT",
|
|
616
|
+
default_attribute: str | None = None,
|
|
617
|
+
):
|
|
618
|
+
super().__init__(name, description)
|
|
619
|
+
self._set_values(
|
|
620
|
+
default_value=default_value,
|
|
621
|
+
min_value=min_value,
|
|
622
|
+
max_value=max_value,
|
|
623
|
+
subtype=subtype,
|
|
624
|
+
hide_value=hide_value,
|
|
625
|
+
attribute_domain=attribute_domain,
|
|
626
|
+
default_attribute=default_attribute,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
class SocketVector(SocketBase):
|
|
631
|
+
_bl_socket_type: str = "NodeSocketVector"
|
|
632
|
+
socket: bpy.types.NodeTreeInterfaceSocketVector
|
|
633
|
+
|
|
634
|
+
def __init__(
|
|
635
|
+
self,
|
|
636
|
+
name: str = "Vector",
|
|
637
|
+
default_value: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
638
|
+
*,
|
|
639
|
+
description: str = "",
|
|
640
|
+
dimensions: int = 3,
|
|
641
|
+
min_value: float | None = None,
|
|
642
|
+
max_value: float | None = None,
|
|
643
|
+
hide_value: bool = False,
|
|
644
|
+
subtype: VectorInterfaceSubtypes = "NONE",
|
|
645
|
+
default_attribute: str | None = None,
|
|
646
|
+
attribute_domain: _AttributeDomains = "POINT",
|
|
647
|
+
):
|
|
648
|
+
super().__init__(name, description)
|
|
649
|
+
assert len(default_value) == dimensions, (
|
|
650
|
+
"Default value length must match dimensions"
|
|
651
|
+
)
|
|
652
|
+
self._set_values(
|
|
653
|
+
dimensions=dimensions,
|
|
654
|
+
default_value=default_value,
|
|
655
|
+
min_value=min_value,
|
|
656
|
+
max_value=max_value,
|
|
657
|
+
hide_value=hide_value,
|
|
658
|
+
subtype=subtype,
|
|
659
|
+
default_attribute=default_attribute,
|
|
660
|
+
attribute_domain=attribute_domain,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
class SocketInt(SocketBase):
|
|
665
|
+
_bl_socket_type: str = "NodeSocketInt"
|
|
666
|
+
socket: bpy.types.NodeTreeInterfaceSocketInt
|
|
667
|
+
|
|
668
|
+
def __init__(
|
|
669
|
+
self,
|
|
670
|
+
name: str = "Integer",
|
|
671
|
+
default_value: int = 0,
|
|
672
|
+
*,
|
|
673
|
+
description: str = "",
|
|
674
|
+
min_value: int = -2147483648,
|
|
675
|
+
max_value: int = 2147483647,
|
|
676
|
+
hide_value: bool = False,
|
|
677
|
+
subtype: IntegerInterfaceSubtypes = "NONE",
|
|
678
|
+
attribute_domain: _AttributeDomains = "POINT",
|
|
679
|
+
default_attribute: str | None = None,
|
|
680
|
+
):
|
|
681
|
+
super().__init__(name, description)
|
|
682
|
+
self._set_values(
|
|
683
|
+
default_value=default_value,
|
|
684
|
+
min_value=min_value,
|
|
685
|
+
max_value=max_value,
|
|
686
|
+
hide_value=hide_value,
|
|
687
|
+
subtype=subtype,
|
|
688
|
+
attribute_domain=attribute_domain,
|
|
689
|
+
default_attribute=default_attribute,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class SocketColor(SocketBase):
|
|
694
|
+
"""Color socket - RGB color value."""
|
|
695
|
+
|
|
696
|
+
_bl_socket_type: str = "NodeSocketColor"
|
|
697
|
+
socket: bpy.types.NodeTreeInterfaceSocketColor
|
|
698
|
+
|
|
699
|
+
def __init__(
|
|
700
|
+
self,
|
|
701
|
+
name: str = "Color",
|
|
702
|
+
default_value: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
703
|
+
*,
|
|
704
|
+
description: str = "",
|
|
705
|
+
hide_value: bool = False,
|
|
706
|
+
attribute_domain: _AttributeDomains = "POINT",
|
|
707
|
+
default_attribute: str | None = None,
|
|
708
|
+
):
|
|
709
|
+
super().__init__(name, description)
|
|
710
|
+
assert len(default_value) == 4, "Default color must be RGBA tuple"
|
|
711
|
+
self._set_values(
|
|
712
|
+
default_value=default_value,
|
|
713
|
+
hide_value=hide_value,
|
|
714
|
+
attribute_domain=attribute_domain,
|
|
715
|
+
default_attribute=default_attribute,
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
class SocketRotation(SocketBase):
|
|
720
|
+
"""Rotation socket - rotation value (Euler or Quaternion)."""
|
|
721
|
+
|
|
722
|
+
_bl_socket_type: str = "NodeSocketRotation"
|
|
723
|
+
socket: bpy.types.NodeTreeInterfaceSocketRotation
|
|
724
|
+
|
|
725
|
+
def __init__(
|
|
726
|
+
self,
|
|
727
|
+
name: str = "Rotation",
|
|
728
|
+
default_value: tuple[float, float, float] = (1.0, 0.0, 0.0),
|
|
729
|
+
*,
|
|
730
|
+
description: str = "",
|
|
731
|
+
hide_value: bool = False,
|
|
732
|
+
attribute_domain: _AttributeDomains = "POINT",
|
|
733
|
+
default_attribute: str | None = None,
|
|
734
|
+
):
|
|
735
|
+
super().__init__(name, description)
|
|
736
|
+
assert len(default_value) == 4, "Default rotation must be quaternion tuple"
|
|
737
|
+
self._set_values(
|
|
738
|
+
default_value=default_value,
|
|
739
|
+
hide_value=hide_value,
|
|
740
|
+
attribute_domain=attribute_domain,
|
|
741
|
+
default_attribute=default_attribute,
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
class SocketMatrix(SocketBase):
|
|
746
|
+
"""Matrix socket - 4x4 transformation matrix."""
|
|
747
|
+
|
|
748
|
+
_bl_socket_type: str = "NodeSocketMatrix"
|
|
749
|
+
socket: bpy.types.NodeTreeInterfaceSocketMatrix
|
|
750
|
+
|
|
751
|
+
def __init__(
|
|
752
|
+
self,
|
|
753
|
+
name: str = "Matrix",
|
|
754
|
+
*,
|
|
755
|
+
description: str = "",
|
|
756
|
+
hide_value: bool = False,
|
|
757
|
+
attribute_domain: _AttributeDomains = "POINT",
|
|
758
|
+
default_attribute: str | None = None,
|
|
759
|
+
):
|
|
760
|
+
super().__init__(name, description)
|
|
761
|
+
self._set_values(
|
|
762
|
+
hide_value=hide_value,
|
|
763
|
+
attribute_domain=attribute_domain,
|
|
764
|
+
default_attribute=default_attribute,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
class SocketString(SocketBase):
|
|
769
|
+
_bl_socket_type: str = "NodeSocketString"
|
|
770
|
+
socket: bpy.types.NodeTreeInterfaceSocketString
|
|
771
|
+
|
|
772
|
+
def __init__(
|
|
773
|
+
self,
|
|
774
|
+
name: str = "String",
|
|
775
|
+
default_value: str = "",
|
|
776
|
+
*,
|
|
777
|
+
description: str = "",
|
|
778
|
+
hide_value: bool = False,
|
|
779
|
+
subtype: StringInterfaceSubtypes = "NONE",
|
|
780
|
+
):
|
|
781
|
+
super().__init__(name, description)
|
|
782
|
+
self._set_values(
|
|
783
|
+
default_value=default_value,
|
|
784
|
+
hide_value=hide_value,
|
|
785
|
+
subtype=subtype,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
class MenuSocket(SocketBase):
|
|
790
|
+
"""Menu socket - holds a selection from predefined items."""
|
|
791
|
+
|
|
792
|
+
_bl_socket_type: str = "NodeSocketMenu"
|
|
793
|
+
socket: bpy.types.NodeTreeInterfaceSocketMenu
|
|
794
|
+
|
|
795
|
+
def __init__(
|
|
796
|
+
self,
|
|
797
|
+
name: str = "Menu",
|
|
798
|
+
default_value: str | None = None,
|
|
799
|
+
*,
|
|
800
|
+
description: str = "",
|
|
801
|
+
expanded: bool = False,
|
|
802
|
+
hide_value: bool = False,
|
|
803
|
+
):
|
|
804
|
+
super().__init__(name, description)
|
|
805
|
+
self._set_values(
|
|
806
|
+
default_value=default_value,
|
|
807
|
+
menu_expanded=expanded,
|
|
808
|
+
hide_value=hide_value,
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
class SocketObject(SocketBase):
|
|
813
|
+
"""Object socket - Blender object reference."""
|
|
814
|
+
|
|
815
|
+
_bl_socket_type: str = "NodeSocketObject"
|
|
816
|
+
socket: bpy.types.NodeTreeInterfaceSocketObject
|
|
817
|
+
|
|
818
|
+
def __init__(
|
|
819
|
+
self,
|
|
820
|
+
name: str = "Object",
|
|
821
|
+
default_value: bpy.types.Object | None = None,
|
|
822
|
+
*,
|
|
823
|
+
description: str = "",
|
|
824
|
+
hide_value: bool = False,
|
|
825
|
+
):
|
|
826
|
+
super().__init__(name, description)
|
|
827
|
+
self._set_values(
|
|
828
|
+
default_value=default_value,
|
|
829
|
+
hide_value=hide_value,
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
class SocketCollection(SocketBase):
|
|
834
|
+
"""Collection socket - Blender collection reference."""
|
|
835
|
+
|
|
836
|
+
_bl_socket_type: str = "NodeSocketCollection"
|
|
837
|
+
socket: bpy.types.NodeTreeInterfaceSocketCollection
|
|
838
|
+
|
|
839
|
+
def __init__(
|
|
840
|
+
self,
|
|
841
|
+
name: str = "Collection",
|
|
842
|
+
default_value: bpy.types.Collection | None = None,
|
|
843
|
+
*,
|
|
844
|
+
description: str = "",
|
|
845
|
+
hide_value: bool = False,
|
|
846
|
+
):
|
|
847
|
+
super().__init__(name, description)
|
|
848
|
+
self._set_values(
|
|
849
|
+
default_value=default_value,
|
|
850
|
+
hide_value=hide_value,
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
class SocketImage(SocketBase):
|
|
855
|
+
"""Image socket - Blender image datablock reference."""
|
|
856
|
+
|
|
857
|
+
_bl_socket_type: str = "NodeSocketImage"
|
|
858
|
+
socket: bpy.types.NodeTreeInterfaceSocketImage
|
|
859
|
+
|
|
860
|
+
def __init__(
|
|
861
|
+
self,
|
|
862
|
+
name: str = "Image",
|
|
863
|
+
default_value: bpy.types.Image | None = None,
|
|
864
|
+
*,
|
|
865
|
+
description: str = "",
|
|
866
|
+
hide_value: bool = False,
|
|
867
|
+
):
|
|
868
|
+
super().__init__(name, description)
|
|
869
|
+
self._set_values(
|
|
870
|
+
default_value=default_value,
|
|
871
|
+
hide_value=hide_value,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
class SocketMaterial(SocketBase):
|
|
876
|
+
"""Material socket - Blender material reference."""
|
|
877
|
+
|
|
878
|
+
_bl_socket_type: str = "NodeSocketMaterial"
|
|
879
|
+
socket: bpy.types.NodeTreeInterfaceSocketMaterial
|
|
880
|
+
|
|
881
|
+
def __init__(
|
|
882
|
+
self,
|
|
883
|
+
name: str = "Material",
|
|
884
|
+
default_value: bpy.types.Material | None = None,
|
|
885
|
+
*,
|
|
886
|
+
description: str = "",
|
|
887
|
+
hide_value: bool = False,
|
|
888
|
+
):
|
|
889
|
+
super().__init__(name, description)
|
|
890
|
+
self._set_values(
|
|
891
|
+
default_value=default_value,
|
|
892
|
+
hide_value=hide_value,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
class SocketBundle(SocketBase):
|
|
897
|
+
"""Bundle socket - holds multiple data types in one socket."""
|
|
898
|
+
|
|
899
|
+
_bl_socket_type: str = "NodeSocketBundle"
|
|
900
|
+
socket: bpy.types.NodeTreeInterfaceSocketBundle
|
|
901
|
+
|
|
902
|
+
def __init__(
|
|
903
|
+
self,
|
|
904
|
+
name: str = "Bundle",
|
|
905
|
+
*,
|
|
906
|
+
description: str = "",
|
|
907
|
+
hide_value: bool = False,
|
|
908
|
+
):
|
|
909
|
+
super().__init__(name, description)
|
|
910
|
+
self._set_values(
|
|
911
|
+
hide_value=hide_value,
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
class SocketClosure(SocketBase):
|
|
916
|
+
"""Closure socket - holds shader closure data."""
|
|
917
|
+
|
|
918
|
+
_bl_socket_type: str = "NodeSocketClosure"
|
|
919
|
+
socket: bpy.types.NodeTreeInterfaceSocketClosure
|
|
920
|
+
|
|
921
|
+
def __init__(
|
|
922
|
+
self,
|
|
923
|
+
name: str = "Closure",
|
|
924
|
+
*,
|
|
925
|
+
description: str = "",
|
|
926
|
+
hide_value: bool = False,
|
|
927
|
+
):
|
|
928
|
+
super().__init__(name, description)
|
|
929
|
+
self._set_values(
|
|
930
|
+
hide_value=hide_value,
|
|
931
|
+
)
|