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/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
+ )