nodebpy 0.1.1__py3-none-any.whl → 0.2.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
@@ -2,6 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any, ClassVar, Literal
4
4
 
5
+ if TYPE_CHECKING:
6
+ from .nodes.converter import Math, VectorMath
7
+
5
8
  import arrangebpy
6
9
  import bpy
7
10
  from bpy.types import (
@@ -12,12 +15,18 @@ from bpy.types import (
12
15
  )
13
16
 
14
17
  from .nodes.types import (
18
+ LINKABLE,
19
+ SOCKET_COMPATIBILITY,
20
+ SOCKET_TYPES,
21
+ TYPE_INPUT_ALL,
15
22
  FloatInterfaceSubtypes,
16
23
  IntegerInterfaceSubtypes,
17
24
  StringInterfaceSubtypes,
18
25
  VectorInterfaceSubtypes,
19
26
  _AttributeDomains,
27
+ _SocketShapeStructureType,
20
28
  )
29
+
21
30
  # from .arrange import arrange_tree
22
31
 
23
32
  GEO_NODE_NAMES = (
@@ -33,13 +42,6 @@ GEO_NODE_NAMES = (
33
42
  )
34
43
 
35
44
 
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
45
  def normalize_name(name: str) -> str:
44
46
  """Convert 'Geometry' or 'My Socket' to 'geometry' or 'my_socket'."""
45
47
  return name.lower().replace(" ", "_")
@@ -50,28 +52,61 @@ def denormalize_name(attr_name: str) -> str:
50
52
  return attr_name.replace("_", " ").title()
51
53
 
52
54
 
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)}")
55
+ class SocketContext:
56
+ _direction: Literal["INPUT", "OUTPUT"] | None
57
+ _active_context: SocketContext | None = None
58
+
59
+ def __init__(self, tree_builder: TreeBuilder):
60
+ self.builder = tree_builder
61
+
62
+ @property
63
+ def tree(self) -> GeometryNodeTree:
64
+ tree = self.builder.tree
65
+ assert tree is not None and isinstance(tree, GeometryNodeTree)
66
+ return tree
67
+
68
+ @property
69
+ def interface(self) -> bpy.types.NodeTreeInterface:
70
+ interface = self.tree.interface
71
+ assert interface is not None
72
+ return interface
73
+
74
+ def _create_socket(
75
+ self, socket_def: SocketBase, name: str
76
+ ) -> bpy.types.NodeTreeInterfaceSocket:
77
+ """Create a socket from a socket definition."""
78
+ socket = self.interface.new_socket(
79
+ name=name,
80
+ in_out=self._direction,
81
+ socket_type=socket_def._bl_socket_type,
82
+ )
83
+ socket.description = socket_def.description
84
+ return socket
85
+
86
+ def __enter__(self):
87
+ SocketContext._direction = self._direction
88
+ SocketContext._active_context = self
89
+ return self
90
+
91
+ def __exit__(self, *args):
92
+ SocketContext._direction = None
93
+ SocketContext._active_context = None
94
+ pass
95
+
96
+
97
+ class DirectionalContext(SocketContext):
98
+ """Base class for directional socket contexts"""
99
+
100
+ _direction: Literal["INPUT", "OUTPUT"] = "INPUT"
101
+ _active_context = None
102
+
103
+
104
+ class InputInterfaceContext(DirectionalContext):
105
+ _direction = "INPUT"
63
106
 
64
107
 
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)}")
108
+ class OutputInterfaceContext(DirectionalContext):
109
+ _direction = "OUTPUT"
75
110
 
76
111
 
77
112
  class TreeBuilder:
@@ -112,6 +147,9 @@ class TreeBuilder:
112
147
  def nodes(self) -> Nodes:
113
148
  return self.tree.nodes
114
149
 
150
+ def __len__(self) -> int:
151
+ return len(self.nodes)
152
+
115
153
  def arrange(self):
116
154
  settings = arrangebpy.LayoutSettings(
117
155
  horizontal_spacing=200, vertical_spacing=200, align_top_layer=True
@@ -148,92 +186,46 @@ class TreeBuilder:
148
186
  except KeyError:
149
187
  return self.tree.nodes.new("NodeGroupOutput") # type: ignore
150
188
 
151
- def link(self, socket1: NodeSocket, socket2: NodeSocket):
189
+ def link(self, socket1: NodeSocket, socket2: NodeSocket) -> bpy.types.NodeLink:
152
190
  if isinstance(socket1, SocketLinker):
153
191
  socket1 = socket1.socket
154
192
  if isinstance(socket2, SocketLinker):
155
193
  socket2 = socket2.socket
156
194
 
157
- self.tree.links.new(socket1, socket2)
195
+ link = self.tree.links.new(socket1, socket2, handle_dynamic_sockets=True)
158
196
 
159
197
  if any(socket.is_inactive for socket in [socket1, socket2]):
160
198
  # the warning message should report which sockets from which nodes were linked and which were innactive
161
199
  for socket in [socket1, socket2]:
162
- if socket.is_inactive:
200
+ # we want to be loud about it if we end up linking an inactive socket to a node that is not a switch
201
+ if socket.is_inactive and socket.node.bl_idname not in (
202
+ "GeometryNodeIndexSwitch",
203
+ "GeometryNodeMenuSwitch",
204
+ ):
163
205
  message = f"Socket {socket.name} from node {socket.node.name} is inactive."
164
206
  message += f" It is linked to socket {socket2.name} from node {socket2.node.name}."
165
207
  message += " This link will be created by Blender but ignored when evaluated."
166
208
  message += f"Socket type: {socket.bl_idname}"
167
209
  raise RuntimeError(message)
168
210
 
211
+ return link
212
+
169
213
  def add(self, name: str) -> Node:
170
214
  self.just_added = self.tree.nodes.new(name) # type: ignore
171
215
  assert self.just_added is not None
172
216
  return self.just_added
173
217
 
174
218
 
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
219
  class NodeBuilder:
228
220
  """Base class for all geometry node wrappers."""
229
221
 
230
- node: Node
231
- name: str
222
+ node: Any
232
223
  _tree: "TreeBuilder"
233
- _link_target: str | None = None # Track which input should receive links
224
+ _link_target: str | None = None
234
225
  _from_socket: NodeSocket | None = None
235
226
  _default_input_id: str | None = None
236
227
  _default_output_id: str | None = None
228
+ _socket_data_types = tuple(SOCKET_COMPATIBILITY.keys())
237
229
 
238
230
  def __init__(self):
239
231
  # Get active tree from context manager
@@ -246,9 +238,6 @@ class NodeBuilder:
246
238
  f" node = {self.__class__.__name__}()\n"
247
239
  )
248
240
 
249
- self.inputs = InputInterfaceContext(tree)
250
- self.outputs = OutputInterfaceContext(tree)
251
-
252
241
  self._tree = tree
253
242
  self._link_target = None
254
243
  if self.__class__.name is not None:
@@ -266,6 +255,14 @@ class NodeBuilder:
266
255
  def tree(self, value: "TreeBuilder"):
267
256
  self._tree = value
268
257
 
258
+ @property
259
+ def type(self) -> SOCKET_TYPES:
260
+ return self._default_output_socket.type # type: ignore
261
+
262
+ @property
263
+ def name(self) -> str:
264
+ return str(self.node.name)
265
+
269
266
  @property
270
267
  def _default_input_socket(self) -> NodeSocket:
271
268
  if self._default_input_id is not None:
@@ -278,6 +275,309 @@ class NodeBuilder:
278
275
  return self.node.outputs[self._output_idx(self._default_output_id)]
279
276
  return self.node.outputs[0]
280
277
 
278
+ def _source_socket(self, node: LINKABLE | SocketLinker | NodeSocket) -> NodeSocket:
279
+ assert node
280
+ if isinstance(node, NodeSocket):
281
+ return node
282
+ elif isinstance(node, Node):
283
+ return node.outputs[0]
284
+ elif hasattr(node, "_default_output_socket"):
285
+ return node._default_output_socket
286
+ else:
287
+ raise TypeError(f"Unsupported type: {type(node)}")
288
+
289
+ def _target_socket(self, node: LINKABLE | SocketLinker | NodeSocket) -> NodeSocket:
290
+ assert node
291
+ if isinstance(node, NodeSocket):
292
+ return node
293
+ elif isinstance(node, Node):
294
+ return node.inputs[0]
295
+ elif hasattr(node, "_default_input_socket"):
296
+ return node._default_input_socket
297
+ else:
298
+ raise TypeError(f"Unsupported type: {type(node)}")
299
+
300
+ def _find_compatible_output_socket(self, linkable: "NodeBuilder") -> NodeSocket:
301
+ """Find a compatible output socket from the linkable node that matches our accepted socket types."""
302
+ # First try the default output socket
303
+ default_output = linkable._default_output_socket
304
+ for comp in SOCKET_COMPATIBILITY[default_output.type]:
305
+ if comp in self._socket_data_types:
306
+ return default_output
307
+
308
+ # If default doesn't work, try all other output sockets
309
+ for output_socket in linkable.node.outputs:
310
+ for comp in SOCKET_COMPATIBILITY[output_socket.type]:
311
+ if comp in self._socket_data_types:
312
+ return output_socket
313
+
314
+ # No compatible socket found
315
+ raise ValueError(
316
+ f"No compatible output socket found on {linkable.node.name} for {self.__class__.__name__}. "
317
+ f"Available output types: {[s.type for s in linkable.node.outputs]}, "
318
+ f"Accepted input types: {self._socket_data_types}"
319
+ )
320
+
321
+ def _find_compatible_source_socket(
322
+ self, source_node: "NodeBuilder", target_socket: NodeSocket
323
+ ) -> NodeSocket:
324
+ """Find a compatible output socket from source node that can link to the target input socket."""
325
+ target_type = target_socket.type
326
+ compatible_types = SOCKET_COMPATIBILITY.get(target_type, ())
327
+
328
+ # Collect all compatible sockets with their compatibility priority
329
+ compatible_sockets = []
330
+ for output_socket in source_node.node.outputs:
331
+ if output_socket.type in compatible_types:
332
+ # Priority is the index in the compatibility list (0 = highest priority)
333
+ priority = compatible_types.index(output_socket.type)
334
+ compatible_sockets.append((priority, output_socket))
335
+
336
+ if not compatible_sockets:
337
+ # No compatible socket found
338
+ raise ValueError(
339
+ f"No compatible output socket found on {source_node.node.name} for target socket {target_socket.name} of type {target_type}. "
340
+ f"Available output types: {[s.type for s in source_node.node.outputs]}, "
341
+ f"Compatible types: {compatible_types}"
342
+ )
343
+
344
+ # Sort by priority (lowest number = highest priority) and return the best match
345
+ compatible_sockets.sort(key=lambda x: x[0])
346
+ return compatible_sockets[0][1]
347
+
348
+ def _find_best_socket_pair(
349
+ self, target_node: "NodeBuilder"
350
+ ) -> tuple[NodeSocket, NodeSocket]:
351
+ """Find the best compatible socket pair between this node (source) and target node."""
352
+ # First try to connect default output to default input
353
+ default_output = self._default_output_socket
354
+ default_input = target_node._default_input_socket
355
+
356
+ # Handle zone outputs that don't have inputs yet
357
+ if default_input is None and hasattr(target_node, "_add_socket"):
358
+ # Target is a zone without inputs - create compatible socket
359
+ source_type = default_output.type
360
+ compatible_types = SOCKET_COMPATIBILITY.get(source_type, [source_type])
361
+ best_type = compatible_types[0] if compatible_types else source_type
362
+
363
+ # Create socket on target zone
364
+ default_input = target_node._add_socket(
365
+ name=best_type.title(), type=best_type
366
+ )
367
+ return default_output, default_input
368
+
369
+ # Check if default sockets are compatible
370
+ if default_input is not None:
371
+ output_compatibles = SOCKET_COMPATIBILITY.get(default_output.type, ())
372
+ if default_input.type in output_compatibles and (
373
+ not default_input.links or default_input.is_multi_input
374
+ ):
375
+ return default_output, default_input
376
+
377
+ # If defaults don't work, try all combinations with priority-based matching
378
+ best_match = None
379
+ best_priority = float("inf")
380
+
381
+ for output_socket in self.node.outputs:
382
+ output_compatibles = SOCKET_COMPATIBILITY.get(output_socket.type, ())
383
+ for input_socket in target_node.node.inputs:
384
+ # Skip if socket already has a link and isn't multi-input
385
+ if input_socket.links and not input_socket.is_multi_input:
386
+ continue
387
+
388
+ if input_socket.type in output_compatibles:
389
+ # Calculate priority as the index in the compatibility list
390
+ priority = output_compatibles.index(input_socket.type)
391
+ if priority < best_priority:
392
+ best_priority = priority
393
+ best_match = (output_socket, input_socket)
394
+
395
+ if best_match:
396
+ return best_match
397
+
398
+ # No compatible pair found
399
+ available_outputs = [s.type for s in self.node.outputs]
400
+ available_inputs = [
401
+ s.type for s in target_node.node.inputs if not s.links or s.is_multi_input
402
+ ]
403
+ raise RuntimeError(
404
+ f"Cannot link any output from {self.node.name} to any input of {target_node.node.name}. "
405
+ f"Available output types: {available_outputs}, "
406
+ f"Available input types: {available_inputs}"
407
+ )
408
+
409
+ def _socket_type_from_linkable(self, linkable: LINKABLE):
410
+ assert linkable, "Linkable cannot be None"
411
+ # If it's a NodeBuilder, try to find a compatible output socket
412
+ if hasattr(linkable, "node") and hasattr(linkable, "_default_output_socket"):
413
+ compatible_socket = self._find_compatible_output_socket(linkable)
414
+ socket_type = compatible_socket.type
415
+ for comp in SOCKET_COMPATIBILITY[socket_type]:
416
+ if comp in self._socket_data_types:
417
+ return comp if comp != "VALUE" else "FLOAT"
418
+
419
+ # Fallback to original logic for other types
420
+ for comp in SOCKET_COMPATIBILITY[linkable.type]:
421
+ if comp in self._socket_data_types:
422
+ return comp if comp != "VALUE" else "FLOAT"
423
+ raise ValueError(
424
+ f"Unsupported socket type for linking: {linkable}, type: {linkable.type=}"
425
+ )
426
+
427
+ def _add_inputs(self, *args, **kwargs) -> dict[str, LINKABLE]:
428
+ """Dictionary with {new_socket.name: from_linkable} for link creation"""
429
+ new_sockets = {}
430
+ items = {}
431
+ for arg in args:
432
+ if isinstance(arg, bpy.types.NodeSocket):
433
+ name = arg.name
434
+ items[name] = arg
435
+ else:
436
+ items[arg._default_output_socket.name] = arg
437
+ items.update(kwargs)
438
+ for key, value in items.items():
439
+ # For NodeBuilder objects, find the best compatible output socket
440
+ if hasattr(value, "node") and hasattr(value, "_default_output_socket"):
441
+ compatible_socket = self._find_compatible_output_socket(value)
442
+ # Create a SocketLinker to represent the specific socket we want to link from
443
+ # from . import SocketLinker
444
+ socket_linker = SocketLinker(compatible_socket)
445
+ type = self._socket_type_from_linkable(value)
446
+ socket = self._add_socket(name=key, type=type)
447
+ new_sockets[socket.name] = socket_linker
448
+ else:
449
+ type = self._socket_type_from_linkable(value)
450
+ socket = self._add_socket(name=key, type=type)
451
+ new_sockets[socket.name] = value
452
+
453
+ return new_sockets
454
+
455
+ def _add_socket(
456
+ self, name: str, type: str, default_value: Any | None = None
457
+ ) -> NodeSocket:
458
+ raise NotImplementedError
459
+
460
+ def _find_or_create_compatible_output_socket(
461
+ self, target_type: str
462
+ ) -> NodeSocket | None:
463
+ """Find an existing compatible output socket or create a new one if this node supports it.
464
+
465
+ Args:
466
+ target_type: The socket type needed for compatibility
467
+
468
+ Returns:
469
+ Compatible output socket if found/created, None if not possible
470
+ """
471
+ if not hasattr(self, "_add_socket"):
472
+ return None
473
+
474
+ # Check if we already have a compatible output socket
475
+ if hasattr(self, "outputs"):
476
+ for name, socket_linker in self.outputs.items():
477
+ socket_compatibles = SOCKET_COMPATIBILITY.get(
478
+ socket_linker.socket.type, []
479
+ )
480
+ if target_type in socket_compatibles:
481
+ return socket_linker.socket
482
+
483
+ # No existing compatible socket found, try to create one
484
+ try:
485
+ # Check if this node type supports the target socket type
486
+ # by examining the type signature of _add_socket
487
+ import inspect
488
+
489
+ sig = inspect.signature(self._add_socket)
490
+ type_param = sig.parameters.get("type")
491
+
492
+ # If there's a type annotation that limits the allowed types, check it
493
+ if type_param and hasattr(type_param.annotation, "__args__"):
494
+ # This is a Literal type with specific allowed values
495
+ allowed_types = list(type_param.annotation.__args__)
496
+ if target_type not in allowed_types:
497
+ # Try to find a compatible type that this node can create
498
+ for allowed_type in allowed_types:
499
+ if target_type in SOCKET_COMPATIBILITY.get(allowed_type, []):
500
+ # Create the allowed type instead
501
+ self._add_socket(
502
+ name=allowed_type.title(), type=allowed_type
503
+ )
504
+ break
505
+ else:
506
+ # No compatible type found
507
+ return None
508
+ else:
509
+ # Target type is directly supported
510
+ self._add_socket(name=target_type.title(), type=target_type)
511
+ else:
512
+ # No type restrictions, try to create the target type
513
+ self._add_socket(name=target_type.title(), type=target_type)
514
+
515
+ # Find the newly created output socket
516
+ if hasattr(self, "outputs"):
517
+ for name, socket_linker in self.outputs.items():
518
+ socket_compatibles = SOCKET_COMPATIBILITY.get(
519
+ socket_linker.socket.type, []
520
+ )
521
+ if target_type in socket_compatibles:
522
+ return socket_linker.socket
523
+
524
+ # Fallback: try to get the socket directly from the node
525
+ if hasattr(self.node, "outputs"):
526
+ for output_socket in self.node.outputs:
527
+ socket_compatibles = SOCKET_COMPATIBILITY.get(
528
+ output_socket.type, []
529
+ )
530
+ if target_type in socket_compatibles:
531
+ return output_socket
532
+ except (NotImplementedError, AttributeError, RuntimeError):
533
+ # Node doesn't support dynamic socket creation or the type is not supported
534
+ pass
535
+
536
+ return None
537
+
538
+ def _smart_link_to(self, target_node: "NodeBuilder") -> "NodeBuilder":
539
+ """Smart linking that creates compatible sockets when needed.
540
+
541
+ This method checks if we have a compatible output socket for the target node's input,
542
+ and creates one if this node supports dynamic socket creation.
543
+
544
+ Args:
545
+ target_node: The node to link to
546
+
547
+ Returns:
548
+ The target node (for chaining)
549
+ """
550
+ if not hasattr(target_node, "_default_input_socket"):
551
+ # Fall back to regular linking
552
+ return target_node
553
+
554
+ target_socket = target_node._default_input_socket
555
+ if not target_socket:
556
+ # Target has no input socket - can't link
557
+ return target_node
558
+
559
+ # Check if our default output is compatible
560
+ source_socket = self._default_output_socket
561
+ if source_socket:
562
+ source_compatibles = SOCKET_COMPATIBILITY.get(source_socket.type, [])
563
+ if target_socket.type in source_compatibles:
564
+ # Compatible - use normal linking
565
+ self.tree.link(source_socket, target_socket)
566
+ return target_node
567
+
568
+ # Not compatible - try to find/create a compatible output socket
569
+ compatible_socket = self._find_or_create_compatible_output_socket(
570
+ target_socket.type
571
+ )
572
+ if compatible_socket:
573
+ self.tree.link(compatible_socket, target_socket)
574
+ return target_node
575
+
576
+ # Fall back to regular linking (may create reroute nodes)
577
+ if source_socket:
578
+ self.tree.link(source_socket, target_socket)
579
+ return target_node
580
+
281
581
  def _input_idx(self, identifier: str) -> int:
282
582
  # currently there is a Blender bug that is preventing the lookup of sockets from identifiers on some
283
583
  # nodes but not others
@@ -312,22 +612,120 @@ class NodeBuilder:
312
612
  """Output socket: Vector"""
313
613
  return SocketLinker(self.node.outputs[self._output_idx(identifier)])
314
614
 
315
- def link(self, source: LINKABLE, target: LINKABLE):
316
- self.tree.link(source_socket(source), target_socket(target))
615
+ def _link(
616
+ self, source: LINKABLE | SocketLinker | NodeSocket, target: LINKABLE
617
+ ) -> bpy.types.NodeLink:
618
+ return self.tree.link(self._source_socket(source), self._target_socket(target))
317
619
 
318
- def link_to(self, target: LINKABLE):
319
- self.tree.link(self._default_output_socket, target_socket(target))
620
+ def _link_to(self, target: LINKABLE) -> bpy.types.NodeLink:
621
+ return self.tree.link(self._default_output_socket, self._target_socket(target))
622
+
623
+ def _link_from(
624
+ self,
625
+ source: LINKABLE,
626
+ input: LINKABLE | str,
627
+ ):
628
+ # Special handling for dynamic socket nodes (zones, bake, capture attribute, etc.)
629
+ # These nodes have an 'outputs' property that returns a dict based on their items
630
+ if (
631
+ hasattr(source, "_add_socket")
632
+ and hasattr(source, "_smart_link_to")
633
+ and hasattr(source.__class__, "outputs")
634
+ and isinstance(getattr(source.__class__, "outputs"), property)
635
+ and not isinstance(input, str)
636
+ ):
637
+ # Use smart linking that can create compatible sockets
638
+ return source._smart_link_to(input)
320
639
 
321
- def link_from(self, source: LINKABLE, input: "LINKABLE | str"):
322
640
  if isinstance(input, str):
323
641
  try:
324
- self.link(source, self.node.inputs[input])
642
+ self._link(source, self.node.inputs[input])
325
643
  except KeyError:
326
- self.link(source, self.node.inputs[self._input_idx(input)])
644
+ self._link(source, self.node.inputs[self._input_idx(input)])
645
+ else:
646
+ self._link(source, input)
647
+
648
+ def _smart_link_from(
649
+ self,
650
+ source: LINKABLE,
651
+ input_name: str,
652
+ ):
653
+ """Smart linking that finds compatible sockets when default fails."""
654
+ # Get the target input socket
655
+ try:
656
+ target_socket = self.node.inputs[input_name]
657
+ except KeyError:
658
+ target_socket = self.node.inputs[self._input_idx(input_name)]
659
+
660
+ # If source is a NodeBuilder, find the best compatible output socket
661
+ if isinstance(source, NodeBuilder):
662
+ # Search for compatible output sockets - don't try default first as it might be wrong type
663
+ try:
664
+ compatible_output = self._find_compatible_source_socket(
665
+ source, target_socket
666
+ )
667
+ self._link(compatible_output, target_socket)
668
+ return
669
+ except ValueError:
670
+ # No compatible socket found - this is an error, don't fall back
671
+ raise ValueError(
672
+ f"Cannot link {source.node.name} to {self.node.name}.{input_name}: "
673
+ f"No compatible sockets. Available output types: {[s.type for s in source.node.outputs]}, "
674
+ f"Target socket type: {target_socket.type}, "
675
+ f"Compatible types: {SOCKET_COMPATIBILITY.get(target_socket.type, ())}"
676
+ )
677
+ else:
678
+ # For other types, use original link_from behavior
679
+ self._link_from(source, input_name)
680
+
681
+ def _set_input_default_value(self, input, value):
682
+ """Set the default value for an input socket, handling type conversions."""
683
+ if (
684
+ hasattr(input, "type")
685
+ and input.type == "VECTOR"
686
+ and isinstance(value, (int, float))
687
+ ):
688
+ input.default_value = [value] * len(input.default_value)
327
689
  else:
328
- self.link(source, input)
690
+ input.default_value = value
691
+
692
+ def _find_best_compatible_socket(
693
+ self, target_node: "NodeBuilder", output_socket: NodeSocket
694
+ ) -> NodeSocket:
695
+ """Find the best compatible input socket on target node for the given output socket."""
696
+ output_type = output_socket.type
697
+ compatible_types = SOCKET_COMPATIBILITY.get(output_type, ())
698
+
699
+ # Collect all compatible input sockets with their priority
700
+ compatible_inputs = []
701
+ for input_socket in target_node.node.inputs:
702
+ # Skip if socket already has a link and isn't multi-input
703
+ if input_socket.links and not input_socket.is_multi_input:
704
+ continue
329
705
 
330
- def _establish_links(self, **kwargs):
706
+ if input_socket.type in compatible_types:
707
+ # Priority is the index in the compatibility list (0 = highest priority)
708
+ priority = compatible_types.index(input_socket.type)
709
+ compatible_inputs.append((priority, input_socket))
710
+
711
+ if not compatible_inputs:
712
+ # No compatible socket found
713
+ available_types = [
714
+ socket.type
715
+ for socket in target_node.node.inputs
716
+ if not socket.links or socket.is_multi_input
717
+ ]
718
+ raise RuntimeError(
719
+ f"Cannot link {output_type} output to {target_node.node.name}. "
720
+ f"Compatible types: {compatible_types}, "
721
+ f"Available input types: {available_types}"
722
+ )
723
+
724
+ # Sort by priority (lowest number = highest priority) and return the best match
725
+ compatible_inputs.sort(key=lambda x: x[0])
726
+ return compatible_inputs[0][1]
727
+
728
+ def _establish_links(self, **kwargs: TYPE_INPUT_ALL):
331
729
  input_ids = [input.identifier for input in self.node.inputs]
332
730
  for name, value in kwargs.items():
333
731
  if value is None:
@@ -337,21 +735,23 @@ class NodeBuilder:
337
735
  # Ellipsis indicates this input should receive links from >> operator
338
736
  # which can potentially target multiple inputs on the new node
339
737
  if self._from_socket is not None:
340
- self.link(
738
+ self._link(
341
739
  self._from_socket, self.node.inputs[self._input_idx(name)]
342
740
  )
343
741
 
742
+ elif isinstance(value, SocketLinker):
743
+ self._link(value, self.node.inputs[self._input_idx(name)])
344
744
  # we can also provide just a default value for the socket to take if we aren't
345
745
  # providing a socket to link with
346
- elif isinstance(value, (NodeBuilder, SocketNodeBuilder, NodeSocket, Node)):
347
- self.link_from(value, name)
746
+ elif isinstance(value, (NodeBuilder, NodeSocket, Node)):
747
+ self._smart_link_from(value, name)
348
748
  else:
349
749
  if name in input_ids:
350
750
  input = self.node.inputs[input_ids.index(name)]
351
- input.default_value = value
751
+ self._set_input_default_value(input, value)
352
752
  else:
353
753
  input = self.node.inputs[name.replace("_", "").capitalize()]
354
- input.default_value = value
754
+ self._set_input_default_value(input, value)
355
755
 
356
756
  def __rshift__(self, other: "NodeBuilder | SocketLinker") -> "NodeBuilder":
357
757
  """Chain nodes using >> operator. Links output to input.
@@ -361,32 +761,34 @@ class NodeBuilder:
361
761
  tree.inputs.value >> Math.add(..., 0.1) >> tree.outputs.result
362
762
 
363
763
  If the target node has an ellipsis placeholder (...), links to that specific input.
364
- Otherwise, tries to find Geometry sockets first, then falls back to default.
764
+ Otherwise, finds the best compatible socket pair based on type compatibility.
365
765
 
366
766
  Returns the right-hand node to enable continued chaining.
367
767
  """
368
- # Get source socket - prefer Geometry, fall back to default
369
- socket_out = self.node.outputs.get("Geometry") or self._default_output_socket
370
- other._from_socket = socket_out
371
-
372
768
  if isinstance(other, SocketLinker):
769
+ # Direct socket linking - use default output
770
+ socket_out = self._default_output_socket
373
771
  socket_in = other.socket
772
+ other._from_socket = socket_out
374
773
  else:
375
- # Get target socket
774
+ # Standard NodeBuilder linking - need to find compatible sockets
376
775
  if other._link_target is not None:
377
- # Use specific target if set by ellipsis
776
+ # Target socket is specified
378
777
  socket_in = self._get_input_socket_by_name(other, other._link_target)
379
- else:
380
- # Default behavior - prefer Geometry, fall back to default
381
- socket_in = (
382
- other.node.inputs.get("Geometry") or other._default_input_socket
383
- )
778
+ socket_out = self._default_output_socket
384
779
 
385
- # If target socket already has a link and isn't multi-input, try next available socket
386
- if socket_in.links and not socket_in.is_multi_input:
387
- socket_in = (
388
- self._get_next_available_socket(socket_in, socket_out) or socket_in
389
- )
780
+ # Try to find a better source socket if default doesn't work
781
+ try:
782
+ socket_out = self._find_compatible_source_socket(self, socket_in)
783
+ except ValueError:
784
+ # If no compatible socket found, use default and let the link fail with a clear error
785
+ pass
786
+
787
+ other._from_socket = socket_out
788
+ else:
789
+ # No target specified - find best compatible socket pair
790
+ socket_out, socket_in = self._find_best_socket_pair(other)
791
+ other._from_socket = socket_out
390
792
 
391
793
  self.tree.link(socket_out, socket_in)
392
794
  return other
@@ -396,116 +798,92 @@ class NodeBuilder:
396
798
  try:
397
799
  return node.node.inputs[name]
398
800
  except KeyError:
399
- # Try with title case if direct access fails
400
801
  title_name = name.replace("_", " ").title()
401
802
  return node.node.inputs[title_name]
402
803
 
403
- def _get_next_available_socket(
404
- self, socket: NodeSocket, socket_out: NodeSocket
405
- ) -> NodeSocket | None:
406
- """Get the next available socket after the given one."""
407
- try:
408
- inputs = socket.node.inputs
409
- current_idx = inputs.find(socket.identifier)
410
- if current_idx >= 0 and current_idx + 1 < len(inputs):
411
- if socket_out.type == "GEOMETRY":
412
- # Prefer Geometry sockets
413
- for idx in range(current_idx + 1, len(inputs)):
414
- if inputs[idx].type == "GEOMETRY" and not inputs[idx].links:
415
- return inputs[idx]
416
- raise RuntimeError("No available Geometry input sockets found.")
417
- return inputs[current_idx + 1]
418
- except (KeyError, IndexError, AttributeError):
419
- pass
420
- return None
804
+ def _apply_math_operation(
805
+ self, other: Any, operation: str, reverse: bool = False
806
+ ) -> "VectorMath | Math":
807
+ """Apply a math operation with appropriate Math/VectorMath node."""
808
+ from .nodes.converter import VectorMath
421
809
 
422
- def __mul__(self, other: Any) -> "VectorMath | Math":
423
- from .nodes import Math, VectorMath
810
+ values = (
811
+ (self._default_output_socket, other)
812
+ if not reverse
813
+ else (other, self._default_output_socket)
814
+ )
424
815
 
425
- match self._default_output_socket.type:
426
- case "VECTOR":
816
+ # Determine if either operand is a vector type
817
+ self_is_vector = self._default_output_socket.type == "VECTOR"
818
+ other_is_vector = False
819
+ if isinstance(other, NodeBuilder):
820
+ other_is_vector = other._default_output_socket.type == "VECTOR"
821
+
822
+ # Use VectorMath if either operand is a vector
823
+ if self_is_vector or other_is_vector:
824
+ if operation == "multiply":
825
+ # Handle special cases for vector multiplication where we might scale instead
826
+ # of using the multiply method
427
827
  if isinstance(other, (int, float)):
428
828
  return VectorMath.scale(self._default_output_socket, other)
429
829
  elif isinstance(other, (list, tuple)) and len(other) == 3:
430
- return VectorMath.multiply(self._default_output_socket, other)
830
+ return VectorMath.multiply(*values)
831
+ elif isinstance(other, NodeBuilder):
832
+ return VectorMath.multiply(*values)
431
833
  else:
432
834
  raise TypeError(
433
- f"Unsupported type for multiplication with VECTOR socket: {type(other)}"
835
+ f"Unsupported type for {operation} with VECTOR socket: {type(other)}"
434
836
  )
435
- case "VALUE":
436
- return Math.multiply(self._default_output_socket, other)
437
- case _:
438
- raise TypeError(
439
- f"Unsupported socket type for multiplication: {self._default_output_socket.type}"
440
- )
441
-
442
- def __rmul__(self, other: Any) -> "VectorMath | Math":
443
- from .nodes import Math, VectorMath
444
-
445
- match self._default_output_socket.type:
446
- case "VECTOR":
837
+ else:
838
+ vector_method = getattr(VectorMath, operation)
447
839
  if isinstance(other, (int, float)):
448
- return VectorMath.scale(self._default_output_socket, other)
449
- elif isinstance(other, (list, tuple)) and len(other) == 3:
450
- return VectorMath.multiply(other, self._default_output_socket)
840
+ scalar_vector = (other, other, other)
841
+ return (
842
+ vector_method(scalar_vector, self._default_output_socket)
843
+ if not reverse
844
+ else vector_method(self._default_output_socket, scalar_vector)
845
+ )
846
+ elif (
847
+ isinstance(other, (list, tuple)) and len(other) == 3
848
+ ) or isinstance(other, NodeBuilder):
849
+ return vector_method(*values)
850
+
451
851
  else:
452
852
  raise TypeError(
453
- f"Unsupported type for multiplication with VECTOR socket: {type(other)}"
853
+ f"Unsupported type for {operation} with VECTOR operand: {type(other)}"
454
854
  )
455
- case "VALUE":
456
- return Math.multiply(other, self._default_output_socket)
457
- case _:
458
- raise TypeError(
459
- f"Unsupported socket type for multiplication: {self._default_output_socket.type}"
460
- )
855
+ else:
856
+ # Both operands are scalar types, use regular Math
857
+ from .nodes.converter import IntegerMath, Math
461
858
 
462
- def __truediv__(self, other: Any) -> "VectorMath":
463
- from .nodes import VectorMath
859
+ if isinstance(other, int):
860
+ return getattr(IntegerMath, operation)(*values)
861
+ else:
862
+ return getattr(Math, operation)(*values)
464
863
 
465
- match self._default_output_socket.type:
466
- case "VECTOR":
467
- return VectorMath.divide(self._default_output_socket, other)
468
- case _:
469
- raise TypeError(
470
- f"Unsupported socket type for division: {self._default_output_socket.type}"
471
- )
864
+ def __mul__(self, other: Any) -> "VectorMath | Math":
865
+ return self._apply_math_operation(other, "multiply")
472
866
 
473
- def __rtruediv__(self, other: Any) -> "VectorMath":
474
- from .nodes import VectorMath
867
+ def __rmul__(self, other: Any) -> "VectorMath | Math":
868
+ return self._apply_math_operation(other, "multiply", reverse=True)
475
869
 
476
- match self._default_output_socket.type:
477
- case "VECTOR":
478
- return VectorMath.divide(other, self._default_output_socket)
479
- case _:
480
- raise TypeError(
481
- f"Unsupported socket type for division: {self._default_output_socket.type}"
482
- )
870
+ def __truediv__(self, other: Any) -> "VectorMath | Math":
871
+ return self._apply_math_operation(other, "divide")
872
+
873
+ def __rtruediv__(self, other: Any) -> "VectorMath | Math":
874
+ return self._apply_math_operation(other, "divide", reverse=True)
483
875
 
484
876
  def __add__(self, other: Any) -> "VectorMath | Math":
485
- from .nodes import Math, VectorMath
486
-
487
- match self._default_output_socket.type:
488
- case "VECTOR":
489
- return VectorMath.add(self._default_output_socket, other)
490
- case "VALUE":
491
- return Math.add(self._default_output_socket, other)
492
- case _:
493
- raise TypeError(
494
- f"Unsupported socket type for addition: {self._default_output_socket.type}"
495
- )
877
+ return self._apply_math_operation(other, "add")
496
878
 
497
879
  def __radd__(self, other: Any) -> "VectorMath | Math":
498
- from .nodes import Math, VectorMath
499
-
500
- match self._default_output_socket.type:
501
- case "VECTOR":
502
- return VectorMath.add(other, self._default_output_socket)
503
- case "VALUE":
504
- return Math.add(other, self._default_output_socket)
505
- case _:
506
- raise TypeError(
507
- f"Unsupported socket type for addition: {self._default_output_socket.type}"
508
- )
880
+ return self._apply_math_operation(other, "add", reverse=True)
881
+
882
+ def __sub__(self, other: Any) -> "VectorMath | Math":
883
+ return self._apply_math_operation(other, "subtract")
884
+
885
+ def __rsub__(self, other: Any) -> "VectorMath | Math":
886
+ return self._apply_math_operation(other, "subtract", reverse=True)
509
887
 
510
888
 
511
889
  class SocketLinker(NodeBuilder):
@@ -517,39 +895,16 @@ class SocketLinker(NodeBuilder):
517
895
  self._tree = TreeBuilder(socket.node.id_data) # type: ignore
518
896
 
519
897
  @property
520
- def type(self) -> str:
521
- return self.socket.type
898
+ def type(self) -> SOCKET_TYPES:
899
+ return self.socket.type # type: ignore
522
900
 
523
901
  @property
524
902
  def socket_name(self) -> str:
525
903
  return self.socket.name
526
904
 
527
-
528
- class SocketNodeBuilder(NodeBuilder):
529
- """Special NodeBuilder for accessing specific sockets on input/output nodes."""
530
-
531
- def __init__(self, node: Node, socket_name: str, direction: str):
532
- # Don't call super().__init__ - we already have a node
533
- self.node = node
534
- self._tree = TreeBuilder(node.id_data) # type: ignore
535
- self._socket_name = socket_name
536
- self._direction = direction
537
-
538
905
  @property
539
- def _default_output_socket(self) -> NodeSocket:
540
- """Return the specific named output socket."""
541
- if self._direction == "INPUT":
542
- return self.node.outputs[self._socket_name]
543
- else:
544
- raise ValueError("Output nodes don't have outputs")
545
-
546
- @property
547
- def _default_input_socket(self) -> NodeSocket:
548
- """Return the specific named input socket."""
549
- if self._direction == "OUTPUT":
550
- return self.node.inputs[self._socket_name]
551
- else:
552
- raise ValueError("Input nodes don't have inputs")
906
+ def name(self) -> str:
907
+ return str(self.socket.name)
553
908
 
554
909
 
555
910
  class SocketBase(SocketLinker):
@@ -558,11 +913,10 @@ class SocketBase(SocketLinker):
558
913
  _bl_socket_type: str = ""
559
914
 
560
915
  def __init__(self, name: str, description: str = ""):
561
- self.name = name
562
916
  self.description = description
563
917
 
564
918
  self._socket_context: SocketContext = SocketContext._active_context
565
- self.interface_socket = self._socket_context._create_socket(self)
919
+ self.interface_socket = self._socket_context._create_socket(self, name)
566
920
  self._tree = self._socket_context.builder
567
921
  if self._socket_context._direction == "INPUT":
568
922
  socket = self.tree._input_node().outputs[self.interface_socket.identifier]
@@ -576,58 +930,78 @@ class SocketBase(SocketLinker):
576
930
  continue
577
931
  setattr(self.interface_socket, key, value)
578
932
 
933
+ @property
934
+ def default_value(self):
935
+ if not hasattr(self.interface_socket, "default_value"):
936
+ raise AttributeError(
937
+ f"'{self.__class__.__name__}' object has no attribute 'default_value'"
938
+ )
939
+ return self.interface_socket.default_value
579
940
 
580
- class SocketGeometry(SocketBase):
581
- """Geometry socket - holds mesh, curve, point cloud, or volume data."""
582
-
583
- _bl_socket_type: str = "NodeSocketGeometry"
584
- socket: bpy.types.NodeTreeInterfaceSocketGeometry
585
-
586
- def __init__(self, name: str = "Geometry", description: str = ""):
587
- super().__init__(name, description)
941
+ @default_value.setter
942
+ def default_value(self, value):
943
+ if not hasattr(self.interface_socket, "default_value"):
944
+ raise AttributeError(
945
+ f"'{self.__class__.__name__}' object has no attribute 'default_value'"
946
+ )
947
+ self.interface_socket.default_value = value
588
948
 
589
949
 
590
- class SocketBoolean(SocketBase):
591
- """Boolean socket - true/false value."""
950
+ class SocketFloat(SocketBase):
951
+ """Float socket"""
592
952
 
593
- _bl_socket_type: str = "NodeSocketBool"
594
- socket: bpy.types.NodeTreeInterfaceSocketBool
953
+ _bl_socket_type: str = "NodeSocketFloat"
954
+ socket: bpy.types.NodeTreeInterfaceSocketFloat
595
955
 
596
956
  def __init__(
597
957
  self,
598
- name: str = "Boolean",
599
- default_value: bool = False,
600
- *,
958
+ name: str = "Value",
959
+ default_value: float = 0.0,
601
960
  description: str = "",
961
+ *,
962
+ min_value: float | None = None,
963
+ max_value: float | None = None,
964
+ optional_label: bool = False,
602
965
  hide_value: bool = False,
966
+ hide_in_modifier: bool = False,
967
+ structure_type: _SocketShapeStructureType = "AUTO",
968
+ subtype: FloatInterfaceSubtypes = "NONE",
603
969
  attribute_domain: _AttributeDomains = "POINT",
604
970
  default_attribute: str | None = None,
605
971
  ):
606
972
  super().__init__(name, description)
607
973
  self._set_values(
608
974
  default_value=default_value,
975
+ min_value=min_value,
976
+ max_value=max_value,
977
+ optional_label=optional_label,
609
978
  hide_value=hide_value,
979
+ hide_in_modifier=hide_in_modifier,
980
+ structure_type=structure_type,
981
+ subtype=subtype,
610
982
  attribute_domain=attribute_domain,
611
983
  default_attribute=default_attribute,
612
984
  )
613
985
 
614
986
 
615
- class SocketFloat(SocketBase):
616
- """Float socket"""
617
-
618
- _bl_socket_type: str = "NodeSocketFloat"
619
- socket: bpy.types.NodeTreeInterfaceSocketFloat
987
+ class SocketInt(SocketBase):
988
+ _bl_socket_type: str = "NodeSocketInt"
989
+ socket: bpy.types.NodeTreeInterfaceSocketInt
620
990
 
621
991
  def __init__(
622
992
  self,
623
- name: str = "Value",
624
- default_value: float = 0.0,
625
- *,
993
+ name: str = "Integer",
994
+ default_value: int = 0,
626
995
  description: str = "",
627
- min_value: float | None = None,
628
- max_value: float | None = None,
629
- subtype: FloatInterfaceSubtypes = "NONE",
996
+ *,
997
+ min_value: int = -2147483648,
998
+ max_value: int = 2147483647,
999
+ optional_label: bool = False,
630
1000
  hide_value: bool = False,
1001
+ hide_in_modifier: bool = False,
1002
+ structure_type: _SocketShapeStructureType = "AUTO",
1003
+ default_input: Literal["INDEX", "VALUE", "ID_OR_INDEX"] = "VALUE",
1004
+ subtype: IntegerInterfaceSubtypes = "NONE",
631
1005
  attribute_domain: _AttributeDomains = "POINT",
632
1006
  default_attribute: str | None = None,
633
1007
  ):
@@ -636,73 +1010,87 @@ class SocketFloat(SocketBase):
636
1010
  default_value=default_value,
637
1011
  min_value=min_value,
638
1012
  max_value=max_value,
639
- subtype=subtype,
1013
+ optional_label=optional_label,
640
1014
  hide_value=hide_value,
1015
+ hide_in_modifier=hide_in_modifier,
1016
+ structure_type=structure_type,
1017
+ default_input=default_input,
1018
+ subtype=subtype,
641
1019
  attribute_domain=attribute_domain,
642
1020
  default_attribute=default_attribute,
643
1021
  )
644
1022
 
645
1023
 
646
- class SocketVector(SocketBase):
647
- _bl_socket_type: str = "NodeSocketVector"
648
- socket: bpy.types.NodeTreeInterfaceSocketVector
1024
+ class SocketBoolean(SocketBase):
1025
+ """Boolean socket - true/false value."""
1026
+
1027
+ _bl_socket_type: str = "NodeSocketBool"
1028
+ socket: bpy.types.NodeTreeInterfaceSocketBool
649
1029
 
650
1030
  def __init__(
651
1031
  self,
652
- name: str = "Vector",
653
- default_value: tuple[float, float, float] = (0.0, 0.0, 0.0),
654
- *,
1032
+ name: str = "Boolean",
1033
+ default_value: bool = False,
655
1034
  description: str = "",
656
- dimensions: int = 3,
657
- min_value: float | None = None,
658
- max_value: float | None = None,
1035
+ *,
1036
+ optional_label: bool = False,
659
1037
  hide_value: bool = False,
660
- subtype: VectorInterfaceSubtypes = "NONE",
661
- default_attribute: str | None = None,
1038
+ hide_in_modifier: bool = False,
1039
+ structure_type: _SocketShapeStructureType = "AUTO",
1040
+ layer_selection_field: bool = False,
662
1041
  attribute_domain: _AttributeDomains = "POINT",
1042
+ default_attribute: str | None = None,
663
1043
  ):
664
1044
  super().__init__(name, description)
665
- assert len(default_value) == dimensions, (
666
- "Default value length must match dimensions"
667
- )
668
1045
  self._set_values(
669
- dimensions=dimensions,
670
1046
  default_value=default_value,
671
- min_value=min_value,
672
- max_value=max_value,
1047
+ optional_label=optional_label,
673
1048
  hide_value=hide_value,
674
- subtype=subtype,
675
- default_attribute=default_attribute,
1049
+ layer_selection_field=layer_selection_field,
1050
+ hide_in_modifier=hide_in_modifier,
1051
+ structure_type=structure_type,
676
1052
  attribute_domain=attribute_domain,
1053
+ default_attribute=default_attribute,
677
1054
  )
678
1055
 
679
1056
 
680
- class SocketInt(SocketBase):
681
- _bl_socket_type: str = "NodeSocketInt"
682
- socket: bpy.types.NodeTreeInterfaceSocketInt
1057
+ class SocketVector(SocketBase):
1058
+ _bl_socket_type: str = "NodeSocketVector"
1059
+ socket: bpy.types.NodeTreeInterfaceSocketVector
683
1060
 
684
1061
  def __init__(
685
1062
  self,
686
- name: str = "Integer",
687
- default_value: int = 0,
688
- *,
1063
+ name: str = "Vector",
1064
+ default_value: tuple[float, float, float] = (0.0, 0.0, 0.0),
689
1065
  description: str = "",
690
- min_value: int = -2147483648,
691
- max_value: int = 2147483647,
1066
+ *,
1067
+ dimensions: int = 3,
1068
+ min_value: float | None = None,
1069
+ max_value: float | None = None,
1070
+ optional_label: bool = False,
692
1071
  hide_value: bool = False,
693
- subtype: IntegerInterfaceSubtypes = "NONE",
694
- attribute_domain: _AttributeDomains = "POINT",
1072
+ hide_in_modifier: bool = False,
1073
+ structure_type: _SocketShapeStructureType = "AUTO",
1074
+ subtype: VectorInterfaceSubtypes = "NONE",
695
1075
  default_attribute: str | None = None,
1076
+ attribute_domain: _AttributeDomains = "POINT",
696
1077
  ):
1078
+ assert len(default_value) == dimensions, (
1079
+ "Default value length must match dimensions"
1080
+ )
697
1081
  super().__init__(name, description)
698
1082
  self._set_values(
1083
+ dimensions=dimensions,
699
1084
  default_value=default_value,
700
1085
  min_value=min_value,
701
1086
  max_value=max_value,
1087
+ optional_label=optional_label,
702
1088
  hide_value=hide_value,
1089
+ hide_in_modifier=hide_in_modifier,
1090
+ structure_type=structure_type,
703
1091
  subtype=subtype,
704
- attribute_domain=attribute_domain,
705
1092
  default_attribute=default_attribute,
1093
+ attribute_domain=attribute_domain,
706
1094
  )
707
1095
 
708
1096
 
@@ -716,17 +1104,23 @@ class SocketColor(SocketBase):
716
1104
  self,
717
1105
  name: str = "Color",
718
1106
  default_value: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
719
- *,
720
1107
  description: str = "",
1108
+ *,
1109
+ optional_label: bool = False,
721
1110
  hide_value: bool = False,
1111
+ hide_in_modifier: bool = False,
1112
+ structure_type: _SocketShapeStructureType = "AUTO",
722
1113
  attribute_domain: _AttributeDomains = "POINT",
723
1114
  default_attribute: str | None = None,
724
1115
  ):
725
- super().__init__(name, description)
726
1116
  assert len(default_value) == 4, "Default color must be RGBA tuple"
1117
+ super().__init__(name, description)
727
1118
  self._set_values(
728
1119
  default_value=default_value,
1120
+ optional_label=optional_label,
729
1121
  hide_value=hide_value,
1122
+ hide_in_modifier=hide_in_modifier,
1123
+ structure_type=structure_type,
730
1124
  attribute_domain=attribute_domain,
731
1125
  default_attribute=default_attribute,
732
1126
  )
@@ -742,17 +1136,22 @@ class SocketRotation(SocketBase):
742
1136
  self,
743
1137
  name: str = "Rotation",
744
1138
  default_value: tuple[float, float, float] = (1.0, 0.0, 0.0),
745
- *,
746
1139
  description: str = "",
1140
+ *,
1141
+ optional_label: bool = False,
747
1142
  hide_value: bool = False,
1143
+ hide_in_modifier: bool = False,
1144
+ structure_type: _SocketShapeStructureType = "AUTO",
748
1145
  attribute_domain: _AttributeDomains = "POINT",
749
1146
  default_attribute: str | None = None,
750
1147
  ):
751
1148
  super().__init__(name, description)
752
- assert len(default_value) == 4, "Default rotation must be quaternion tuple"
753
1149
  self._set_values(
754
1150
  default_value=default_value,
1151
+ optional_label=optional_label,
755
1152
  hide_value=hide_value,
1153
+ hide_in_modifier=hide_in_modifier,
1154
+ structure_type=structure_type,
756
1155
  attribute_domain=attribute_domain,
757
1156
  default_attribute=default_attribute,
758
1157
  )
@@ -767,15 +1166,23 @@ class SocketMatrix(SocketBase):
767
1166
  def __init__(
768
1167
  self,
769
1168
  name: str = "Matrix",
770
- *,
771
1169
  description: str = "",
1170
+ *,
1171
+ optional_label: bool = False,
772
1172
  hide_value: bool = False,
1173
+ hide_in_modifier: bool = False,
1174
+ structure_type: _SocketShapeStructureType = "AUTO",
1175
+ default_input: Literal["VALUE", "INSTANCE_TRANSFORM"] = "VALUE",
773
1176
  attribute_domain: _AttributeDomains = "POINT",
774
1177
  default_attribute: str | None = None,
775
1178
  ):
776
1179
  super().__init__(name, description)
777
1180
  self._set_values(
1181
+ optional_label=optional_label,
778
1182
  hide_value=hide_value,
1183
+ hide_in_modifier=hide_in_modifier,
1184
+ structure_type=structure_type,
1185
+ default_input=default_input,
779
1186
  attribute_domain=attribute_domain,
780
1187
  default_attribute=default_attribute,
781
1188
  )
@@ -789,20 +1196,24 @@ class SocketString(SocketBase):
789
1196
  self,
790
1197
  name: str = "String",
791
1198
  default_value: str = "",
792
- *,
793
1199
  description: str = "",
1200
+ *,
1201
+ optional_label: bool = False,
794
1202
  hide_value: bool = False,
1203
+ hide_in_modifier: bool = False,
795
1204
  subtype: StringInterfaceSubtypes = "NONE",
796
1205
  ):
797
1206
  super().__init__(name, description)
798
1207
  self._set_values(
799
1208
  default_value=default_value,
1209
+ optional_label=optional_label,
800
1210
  hide_value=hide_value,
1211
+ hide_in_modifier=hide_in_modifier,
801
1212
  subtype=subtype,
802
1213
  )
803
1214
 
804
1215
 
805
- class MenuSocket(SocketBase):
1216
+ class SocketMenu(SocketBase):
806
1217
  """Menu socket - holds a selection from predefined items."""
807
1218
 
808
1219
  _bl_socket_type: str = "NodeSocketMenu"
@@ -812,16 +1223,22 @@ class MenuSocket(SocketBase):
812
1223
  self,
813
1224
  name: str = "Menu",
814
1225
  default_value: str | None = None,
815
- *,
816
1226
  description: str = "",
1227
+ *,
817
1228
  expanded: bool = False,
1229
+ optional_label: bool = False,
818
1230
  hide_value: bool = False,
1231
+ hide_in_modifier: bool = False,
1232
+ structure_type: _SocketShapeStructureType = "AUTO",
819
1233
  ):
820
1234
  super().__init__(name, description)
821
1235
  self._set_values(
822
1236
  default_value=default_value,
823
1237
  menu_expanded=expanded,
1238
+ optional_label=optional_label,
824
1239
  hide_value=hide_value,
1240
+ hide_in_modifier=hide_in_modifier,
1241
+ structure_type=structure_type,
825
1242
  )
826
1243
 
827
1244
 
@@ -835,14 +1252,41 @@ class SocketObject(SocketBase):
835
1252
  self,
836
1253
  name: str = "Object",
837
1254
  default_value: bpy.types.Object | None = None,
838
- *,
839
1255
  description: str = "",
1256
+ *,
1257
+ optional_label: bool = False,
840
1258
  hide_value: bool = False,
1259
+ hide_in_modifier: bool = False,
841
1260
  ):
842
1261
  super().__init__(name, description)
843
1262
  self._set_values(
844
1263
  default_value=default_value,
1264
+ optional_label=optional_label,
1265
+ hide_value=hide_value,
1266
+ hide_in_modifier=hide_in_modifier,
1267
+ )
1268
+
1269
+
1270
+ class SocketGeometry(SocketBase):
1271
+ """Geometry socket - holds mesh, curve, point cloud, or volume data."""
1272
+
1273
+ _bl_socket_type: str = "NodeSocketGeometry"
1274
+ socket: bpy.types.NodeTreeInterfaceSocketGeometry
1275
+
1276
+ def __init__(
1277
+ self,
1278
+ name: str = "Geometry",
1279
+ description: str = "",
1280
+ *,
1281
+ optional_label: bool = False,
1282
+ hide_value: bool = False,
1283
+ hide_in_modifier: bool = False,
1284
+ ):
1285
+ super().__init__(name, description)
1286
+ self._set_values(
1287
+ optional_label=optional_label,
845
1288
  hide_value=hide_value,
1289
+ hide_in_modifier=hide_in_modifier,
846
1290
  )
847
1291
 
848
1292
 
@@ -856,14 +1300,18 @@ class SocketCollection(SocketBase):
856
1300
  self,
857
1301
  name: str = "Collection",
858
1302
  default_value: bpy.types.Collection | None = None,
859
- *,
860
1303
  description: str = "",
1304
+ *,
1305
+ optional_label: bool = False,
861
1306
  hide_value: bool = False,
1307
+ hide_in_modifier: bool = False,
862
1308
  ):
863
1309
  super().__init__(name, description)
864
1310
  self._set_values(
865
1311
  default_value=default_value,
1312
+ optional_label=optional_label,
866
1313
  hide_value=hide_value,
1314
+ hide_in_modifier=hide_in_modifier,
867
1315
  )
868
1316
 
869
1317
 
@@ -877,14 +1325,18 @@ class SocketImage(SocketBase):
877
1325
  self,
878
1326
  name: str = "Image",
879
1327
  default_value: bpy.types.Image | None = None,
880
- *,
881
1328
  description: str = "",
1329
+ *,
1330
+ optional_label: bool = False,
882
1331
  hide_value: bool = False,
1332
+ hide_in_modifier: bool = False,
883
1333
  ):
884
1334
  super().__init__(name, description)
885
1335
  self._set_values(
886
1336
  default_value=default_value,
1337
+ optional_label=optional_label,
887
1338
  hide_value=hide_value,
1339
+ hide_in_modifier=hide_in_modifier,
888
1340
  )
889
1341
 
890
1342
 
@@ -898,14 +1350,18 @@ class SocketMaterial(SocketBase):
898
1350
  self,
899
1351
  name: str = "Material",
900
1352
  default_value: bpy.types.Material | None = None,
901
- *,
902
1353
  description: str = "",
1354
+ *,
1355
+ optional_label: bool = False,
903
1356
  hide_value: bool = False,
1357
+ hide_in_modifier: bool = False,
904
1358
  ):
905
1359
  super().__init__(name, description)
906
1360
  self._set_values(
907
1361
  default_value=default_value,
1362
+ optional_label=optional_label,
908
1363
  hide_value=hide_value,
1364
+ hide_in_modifier=hide_in_modifier,
909
1365
  )
910
1366
 
911
1367
 
@@ -918,13 +1374,17 @@ class SocketBundle(SocketBase):
918
1374
  def __init__(
919
1375
  self,
920
1376
  name: str = "Bundle",
921
- *,
922
1377
  description: str = "",
1378
+ *,
1379
+ optional_label: bool = False,
923
1380
  hide_value: bool = False,
1381
+ hide_in_modifier: bool = False,
924
1382
  ):
925
1383
  super().__init__(name, description)
926
1384
  self._set_values(
1385
+ optional_label=optional_label,
927
1386
  hide_value=hide_value,
1387
+ hide_in_modifier=hide_in_modifier,
928
1388
  )
929
1389
 
930
1390
 
@@ -937,11 +1397,15 @@ class SocketClosure(SocketBase):
937
1397
  def __init__(
938
1398
  self,
939
1399
  name: str = "Closure",
940
- *,
941
1400
  description: str = "",
1401
+ *,
1402
+ optional_label: bool = False,
942
1403
  hide_value: bool = False,
1404
+ hide_in_modifier: bool = False,
943
1405
  ):
944
1406
  super().__init__(name, description)
945
1407
  self._set_values(
1408
+ optional_label=optional_label,
946
1409
  hide_value=hide_value,
1410
+ hide_in_modifier=hide_in_modifier,
947
1411
  )