nodebpy 0.1.0__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
63
58
 
59
+ def __init__(self, tree_builder: TreeBuilder):
60
+ self.builder = tree_builder
64
61
 
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)}")
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"
106
+
107
+
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
@@ -290,7 +590,14 @@ class NodeBuilder:
290
590
  # so instead we have to convert the identifier to an index and then lookup the socket
291
591
  # from the index instead
292
592
  input_ids = [input.identifier for input in self.node.inputs]
293
- return input_ids.index(identifier)
593
+ if identifier in input_ids:
594
+ idx = input_ids.index(identifier)
595
+ return idx
596
+ input_names = [input.name for input in self.node.inputs]
597
+ if identifier in input_names:
598
+ return input_names.index(identifier)
599
+
600
+ raise RuntimeError()
294
601
 
295
602
  def _output_idx(self, identifier: str) -> int:
296
603
  output_ids = [output.identifier for output in self.node.outputs]
@@ -298,28 +605,127 @@ class NodeBuilder:
298
605
 
299
606
  def _input(self, identifier: str) -> SocketLinker:
300
607
  """Input socket: Vector"""
301
- return SocketLinker(self.node.inputs[self._input_idx(identifier)])
608
+ input = self.node.inputs[self._input_idx(identifier)]
609
+ return SocketLinker(input)
302
610
 
303
611
  def _output(self, identifier: str) -> SocketLinker:
304
612
  """Output socket: Vector"""
305
613
  return SocketLinker(self.node.outputs[self._output_idx(identifier)])
306
614
 
307
- def link(self, source: LINKABLE, target: LINKABLE):
308
- 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))
309
619
 
310
- def link_to(self, target: LINKABLE):
311
- 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)
312
639
 
313
- def link_from(self, source: LINKABLE, input: "LINKABLE | str"):
314
640
  if isinstance(input, str):
315
641
  try:
316
- self.link(source, self.node.inputs[input])
642
+ self._link(source, self.node.inputs[input])
317
643
  except KeyError:
318
- 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)
319
689
  else:
320
- 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
705
+
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]
321
727
 
322
- def _establish_links(self, **kwargs):
728
+ def _establish_links(self, **kwargs: TYPE_INPUT_ALL):
323
729
  input_ids = [input.identifier for input in self.node.inputs]
324
730
  for name, value in kwargs.items():
325
731
  if value is None:
@@ -329,24 +735,25 @@ class NodeBuilder:
329
735
  # Ellipsis indicates this input should receive links from >> operator
330
736
  # which can potentially target multiple inputs on the new node
331
737
  if self._from_socket is not None:
332
- self.link(
738
+ self._link(
333
739
  self._from_socket, self.node.inputs[self._input_idx(name)]
334
740
  )
335
741
 
742
+ elif isinstance(value, SocketLinker):
743
+ self._link(value, self.node.inputs[self._input_idx(name)])
336
744
  # we can also provide just a default value for the socket to take if we aren't
337
745
  # 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)
746
+ elif isinstance(value, (NodeBuilder, NodeSocket, Node)):
747
+ self._smart_link_from(value, name)
341
748
  else:
342
749
  if name in input_ids:
343
750
  input = self.node.inputs[input_ids.index(name)]
344
- input.default_value = value
751
+ self._set_input_default_value(input, value)
345
752
  else:
346
753
  input = self.node.inputs[name.replace("_", "").capitalize()]
347
- input.default_value = value
754
+ self._set_input_default_value(input, value)
348
755
 
349
- def __rshift__(self, other: "NodeBuilder") -> "NodeBuilder":
756
+ def __rshift__(self, other: "NodeBuilder | SocketLinker") -> "NodeBuilder":
350
757
  """Chain nodes using >> operator. Links output to input.
351
758
 
352
759
  Usage:
@@ -354,27 +761,34 @@ class NodeBuilder:
354
761
  tree.inputs.value >> Math.add(..., 0.1) >> tree.outputs.result
355
762
 
356
763
  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.
764
+ Otherwise, finds the best compatible socket pair based on type compatibility.
358
765
 
359
766
  Returns the right-hand node to enable continued chaining.
360
767
  """
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)
768
+ if isinstance(other, SocketLinker):
769
+ # Direct socket linking - use default output
770
+ socket_out = self._default_output_socket
771
+ socket_in = other.socket
772
+ other._from_socket = socket_out
369
773
  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
- )
774
+ # Standard NodeBuilder linking - need to find compatible sockets
775
+ if other._link_target is not None:
776
+ # Target socket is specified
777
+ socket_in = self._get_input_socket_by_name(other, other._link_target)
778
+ socket_out = self._default_output_socket
779
+
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
378
792
 
379
793
  self.tree.link(socket_out, socket_in)
380
794
  return other
@@ -384,116 +798,92 @@ class NodeBuilder:
384
798
  try:
385
799
  return node.node.inputs[name]
386
800
  except KeyError:
387
- # Try with title case if direct access fails
388
801
  title_name = name.replace("_", " ").title()
389
802
  return node.node.inputs[title_name]
390
803
 
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
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
409
809
 
410
- def __mul__(self, other: Any) -> "VectorMath | Math":
411
- 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
+ )
412
815
 
413
- match self._default_output_socket.type:
414
- 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
415
827
  if isinstance(other, (int, float)):
416
828
  return VectorMath.scale(self._default_output_socket, other)
417
829
  elif isinstance(other, (list, tuple)) and len(other) == 3:
418
- return VectorMath.multiply(self._default_output_socket, other)
830
+ return VectorMath.multiply(*values)
831
+ elif isinstance(other, NodeBuilder):
832
+ return VectorMath.multiply(*values)
419
833
  else:
420
834
  raise TypeError(
421
- f"Unsupported type for multiplication with VECTOR socket: {type(other)}"
835
+ f"Unsupported type for {operation} with VECTOR socket: {type(other)}"
422
836
  )
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":
837
+ else:
838
+ vector_method = getattr(VectorMath, operation)
435
839
  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)
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
+
439
851
  else:
440
852
  raise TypeError(
441
- f"Unsupported type for multiplication with VECTOR socket: {type(other)}"
853
+ f"Unsupported type for {operation} with VECTOR operand: {type(other)}"
442
854
  )
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
- )
855
+ else:
856
+ # Both operands are scalar types, use regular Math
857
+ from .nodes.converter import IntegerMath, Math
449
858
 
450
- def __truediv__(self, other: Any) -> "VectorMath":
451
- from .nodes import VectorMath
859
+ if isinstance(other, int):
860
+ return getattr(IntegerMath, operation)(*values)
861
+ else:
862
+ return getattr(Math, operation)(*values)
452
863
 
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
- )
864
+ def __mul__(self, other: Any) -> "VectorMath | Math":
865
+ return self._apply_math_operation(other, "multiply")
460
866
 
461
- def __rtruediv__(self, other: Any) -> "VectorMath":
462
- from .nodes import VectorMath
867
+ def __rmul__(self, other: Any) -> "VectorMath | Math":
868
+ return self._apply_math_operation(other, "multiply", reverse=True)
463
869
 
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
- )
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)
471
875
 
472
876
  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
- )
877
+ return self._apply_math_operation(other, "add")
484
878
 
485
879
  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
- )
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)
497
887
 
498
888
 
499
889
  class SocketLinker(NodeBuilder):
@@ -505,35 +895,16 @@ class SocketLinker(NodeBuilder):
505
895
  self._tree = TreeBuilder(socket.node.id_data) # type: ignore
506
896
 
507
897
  @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
898
+ def type(self) -> SOCKET_TYPES:
899
+ return self.socket.type # type: ignore
521
900
 
522
901
  @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")
902
+ def socket_name(self) -> str:
903
+ return self.socket.name
529
904
 
530
905
  @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")
906
+ def name(self) -> str:
907
+ return str(self.socket.name)
537
908
 
538
909
 
539
910
  class SocketBase(SocketLinker):
@@ -542,11 +913,10 @@ class SocketBase(SocketLinker):
542
913
  _bl_socket_type: str = ""
543
914
 
544
915
  def __init__(self, name: str, description: str = ""):
545
- self.name = name
546
916
  self.description = description
547
917
 
548
918
  self._socket_context: SocketContext = SocketContext._active_context
549
- self.interface_socket = self._socket_context._create_socket(self)
919
+ self.interface_socket = self._socket_context._create_socket(self, name)
550
920
  self._tree = self._socket_context.builder
551
921
  if self._socket_context._direction == "INPUT":
552
922
  socket = self.tree._input_node().outputs[self.interface_socket.identifier]
@@ -560,58 +930,78 @@ class SocketBase(SocketLinker):
560
930
  continue
561
931
  setattr(self.interface_socket, key, value)
562
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
563
940
 
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)
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
572
948
 
573
949
 
574
- class SocketBoolean(SocketBase):
575
- """Boolean socket - true/false value."""
950
+ class SocketFloat(SocketBase):
951
+ """Float socket"""
576
952
 
577
- _bl_socket_type: str = "NodeSocketBool"
578
- socket: bpy.types.NodeTreeInterfaceSocketBool
953
+ _bl_socket_type: str = "NodeSocketFloat"
954
+ socket: bpy.types.NodeTreeInterfaceSocketFloat
579
955
 
580
956
  def __init__(
581
957
  self,
582
- name: str = "Boolean",
583
- default_value: bool = False,
584
- *,
958
+ name: str = "Value",
959
+ default_value: float = 0.0,
585
960
  description: str = "",
961
+ *,
962
+ min_value: float | None = None,
963
+ max_value: float | None = None,
964
+ optional_label: bool = False,
586
965
  hide_value: bool = False,
966
+ hide_in_modifier: bool = False,
967
+ structure_type: _SocketShapeStructureType = "AUTO",
968
+ subtype: FloatInterfaceSubtypes = "NONE",
587
969
  attribute_domain: _AttributeDomains = "POINT",
588
970
  default_attribute: str | None = None,
589
971
  ):
590
972
  super().__init__(name, description)
591
973
  self._set_values(
592
974
  default_value=default_value,
975
+ min_value=min_value,
976
+ max_value=max_value,
977
+ optional_label=optional_label,
593
978
  hide_value=hide_value,
979
+ hide_in_modifier=hide_in_modifier,
980
+ structure_type=structure_type,
981
+ subtype=subtype,
594
982
  attribute_domain=attribute_domain,
595
983
  default_attribute=default_attribute,
596
984
  )
597
985
 
598
986
 
599
- class SocketFloat(SocketBase):
600
- """Float socket"""
601
-
602
- _bl_socket_type: str = "NodeSocketFloat"
603
- socket: bpy.types.NodeTreeInterfaceSocketFloat
987
+ class SocketInt(SocketBase):
988
+ _bl_socket_type: str = "NodeSocketInt"
989
+ socket: bpy.types.NodeTreeInterfaceSocketInt
604
990
 
605
991
  def __init__(
606
992
  self,
607
- name: str = "Value",
608
- default_value: float = 0.0,
609
- *,
993
+ name: str = "Integer",
994
+ default_value: int = 0,
610
995
  description: str = "",
611
- min_value: float | None = None,
612
- max_value: float | None = None,
613
- subtype: FloatInterfaceSubtypes = "NONE",
996
+ *,
997
+ min_value: int = -2147483648,
998
+ max_value: int = 2147483647,
999
+ optional_label: bool = False,
614
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",
615
1005
  attribute_domain: _AttributeDomains = "POINT",
616
1006
  default_attribute: str | None = None,
617
1007
  ):
@@ -620,73 +1010,87 @@ class SocketFloat(SocketBase):
620
1010
  default_value=default_value,
621
1011
  min_value=min_value,
622
1012
  max_value=max_value,
623
- subtype=subtype,
1013
+ optional_label=optional_label,
624
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,
625
1019
  attribute_domain=attribute_domain,
626
1020
  default_attribute=default_attribute,
627
1021
  )
628
1022
 
629
1023
 
630
- class SocketVector(SocketBase):
631
- _bl_socket_type: str = "NodeSocketVector"
632
- 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
633
1029
 
634
1030
  def __init__(
635
1031
  self,
636
- name: str = "Vector",
637
- default_value: tuple[float, float, float] = (0.0, 0.0, 0.0),
638
- *,
1032
+ name: str = "Boolean",
1033
+ default_value: bool = False,
639
1034
  description: str = "",
640
- dimensions: int = 3,
641
- min_value: float | None = None,
642
- max_value: float | None = None,
1035
+ *,
1036
+ optional_label: bool = False,
643
1037
  hide_value: bool = False,
644
- subtype: VectorInterfaceSubtypes = "NONE",
645
- default_attribute: str | None = None,
1038
+ hide_in_modifier: bool = False,
1039
+ structure_type: _SocketShapeStructureType = "AUTO",
1040
+ layer_selection_field: bool = False,
646
1041
  attribute_domain: _AttributeDomains = "POINT",
1042
+ default_attribute: str | None = None,
647
1043
  ):
648
1044
  super().__init__(name, description)
649
- assert len(default_value) == dimensions, (
650
- "Default value length must match dimensions"
651
- )
652
1045
  self._set_values(
653
- dimensions=dimensions,
654
1046
  default_value=default_value,
655
- min_value=min_value,
656
- max_value=max_value,
1047
+ optional_label=optional_label,
657
1048
  hide_value=hide_value,
658
- subtype=subtype,
659
- default_attribute=default_attribute,
1049
+ layer_selection_field=layer_selection_field,
1050
+ hide_in_modifier=hide_in_modifier,
1051
+ structure_type=structure_type,
660
1052
  attribute_domain=attribute_domain,
1053
+ default_attribute=default_attribute,
661
1054
  )
662
1055
 
663
1056
 
664
- class SocketInt(SocketBase):
665
- _bl_socket_type: str = "NodeSocketInt"
666
- socket: bpy.types.NodeTreeInterfaceSocketInt
1057
+ class SocketVector(SocketBase):
1058
+ _bl_socket_type: str = "NodeSocketVector"
1059
+ socket: bpy.types.NodeTreeInterfaceSocketVector
667
1060
 
668
1061
  def __init__(
669
1062
  self,
670
- name: str = "Integer",
671
- default_value: int = 0,
672
- *,
1063
+ name: str = "Vector",
1064
+ default_value: tuple[float, float, float] = (0.0, 0.0, 0.0),
673
1065
  description: str = "",
674
- min_value: int = -2147483648,
675
- 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,
676
1071
  hide_value: bool = False,
677
- subtype: IntegerInterfaceSubtypes = "NONE",
678
- attribute_domain: _AttributeDomains = "POINT",
1072
+ hide_in_modifier: bool = False,
1073
+ structure_type: _SocketShapeStructureType = "AUTO",
1074
+ subtype: VectorInterfaceSubtypes = "NONE",
679
1075
  default_attribute: str | None = None,
1076
+ attribute_domain: _AttributeDomains = "POINT",
680
1077
  ):
1078
+ assert len(default_value) == dimensions, (
1079
+ "Default value length must match dimensions"
1080
+ )
681
1081
  super().__init__(name, description)
682
1082
  self._set_values(
1083
+ dimensions=dimensions,
683
1084
  default_value=default_value,
684
1085
  min_value=min_value,
685
1086
  max_value=max_value,
1087
+ optional_label=optional_label,
686
1088
  hide_value=hide_value,
1089
+ hide_in_modifier=hide_in_modifier,
1090
+ structure_type=structure_type,
687
1091
  subtype=subtype,
688
- attribute_domain=attribute_domain,
689
1092
  default_attribute=default_attribute,
1093
+ attribute_domain=attribute_domain,
690
1094
  )
691
1095
 
692
1096
 
@@ -700,17 +1104,23 @@ class SocketColor(SocketBase):
700
1104
  self,
701
1105
  name: str = "Color",
702
1106
  default_value: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
703
- *,
704
1107
  description: str = "",
1108
+ *,
1109
+ optional_label: bool = False,
705
1110
  hide_value: bool = False,
1111
+ hide_in_modifier: bool = False,
1112
+ structure_type: _SocketShapeStructureType = "AUTO",
706
1113
  attribute_domain: _AttributeDomains = "POINT",
707
1114
  default_attribute: str | None = None,
708
1115
  ):
709
- super().__init__(name, description)
710
1116
  assert len(default_value) == 4, "Default color must be RGBA tuple"
1117
+ super().__init__(name, description)
711
1118
  self._set_values(
712
1119
  default_value=default_value,
1120
+ optional_label=optional_label,
713
1121
  hide_value=hide_value,
1122
+ hide_in_modifier=hide_in_modifier,
1123
+ structure_type=structure_type,
714
1124
  attribute_domain=attribute_domain,
715
1125
  default_attribute=default_attribute,
716
1126
  )
@@ -726,17 +1136,22 @@ class SocketRotation(SocketBase):
726
1136
  self,
727
1137
  name: str = "Rotation",
728
1138
  default_value: tuple[float, float, float] = (1.0, 0.0, 0.0),
729
- *,
730
1139
  description: str = "",
1140
+ *,
1141
+ optional_label: bool = False,
731
1142
  hide_value: bool = False,
1143
+ hide_in_modifier: bool = False,
1144
+ structure_type: _SocketShapeStructureType = "AUTO",
732
1145
  attribute_domain: _AttributeDomains = "POINT",
733
1146
  default_attribute: str | None = None,
734
1147
  ):
735
1148
  super().__init__(name, description)
736
- assert len(default_value) == 4, "Default rotation must be quaternion tuple"
737
1149
  self._set_values(
738
1150
  default_value=default_value,
1151
+ optional_label=optional_label,
739
1152
  hide_value=hide_value,
1153
+ hide_in_modifier=hide_in_modifier,
1154
+ structure_type=structure_type,
740
1155
  attribute_domain=attribute_domain,
741
1156
  default_attribute=default_attribute,
742
1157
  )
@@ -751,15 +1166,23 @@ class SocketMatrix(SocketBase):
751
1166
  def __init__(
752
1167
  self,
753
1168
  name: str = "Matrix",
754
- *,
755
1169
  description: str = "",
1170
+ *,
1171
+ optional_label: bool = False,
756
1172
  hide_value: bool = False,
1173
+ hide_in_modifier: bool = False,
1174
+ structure_type: _SocketShapeStructureType = "AUTO",
1175
+ default_input: Literal["VALUE", "INSTANCE_TRANSFORM"] = "VALUE",
757
1176
  attribute_domain: _AttributeDomains = "POINT",
758
1177
  default_attribute: str | None = None,
759
1178
  ):
760
1179
  super().__init__(name, description)
761
1180
  self._set_values(
1181
+ optional_label=optional_label,
762
1182
  hide_value=hide_value,
1183
+ hide_in_modifier=hide_in_modifier,
1184
+ structure_type=structure_type,
1185
+ default_input=default_input,
763
1186
  attribute_domain=attribute_domain,
764
1187
  default_attribute=default_attribute,
765
1188
  )
@@ -773,20 +1196,24 @@ class SocketString(SocketBase):
773
1196
  self,
774
1197
  name: str = "String",
775
1198
  default_value: str = "",
776
- *,
777
1199
  description: str = "",
1200
+ *,
1201
+ optional_label: bool = False,
778
1202
  hide_value: bool = False,
1203
+ hide_in_modifier: bool = False,
779
1204
  subtype: StringInterfaceSubtypes = "NONE",
780
1205
  ):
781
1206
  super().__init__(name, description)
782
1207
  self._set_values(
783
1208
  default_value=default_value,
1209
+ optional_label=optional_label,
784
1210
  hide_value=hide_value,
1211
+ hide_in_modifier=hide_in_modifier,
785
1212
  subtype=subtype,
786
1213
  )
787
1214
 
788
1215
 
789
- class MenuSocket(SocketBase):
1216
+ class SocketMenu(SocketBase):
790
1217
  """Menu socket - holds a selection from predefined items."""
791
1218
 
792
1219
  _bl_socket_type: str = "NodeSocketMenu"
@@ -796,16 +1223,22 @@ class MenuSocket(SocketBase):
796
1223
  self,
797
1224
  name: str = "Menu",
798
1225
  default_value: str | None = None,
799
- *,
800
1226
  description: str = "",
1227
+ *,
801
1228
  expanded: bool = False,
1229
+ optional_label: bool = False,
802
1230
  hide_value: bool = False,
1231
+ hide_in_modifier: bool = False,
1232
+ structure_type: _SocketShapeStructureType = "AUTO",
803
1233
  ):
804
1234
  super().__init__(name, description)
805
1235
  self._set_values(
806
1236
  default_value=default_value,
807
1237
  menu_expanded=expanded,
1238
+ optional_label=optional_label,
808
1239
  hide_value=hide_value,
1240
+ hide_in_modifier=hide_in_modifier,
1241
+ structure_type=structure_type,
809
1242
  )
810
1243
 
811
1244
 
@@ -819,14 +1252,41 @@ class SocketObject(SocketBase):
819
1252
  self,
820
1253
  name: str = "Object",
821
1254
  default_value: bpy.types.Object | None = None,
822
- *,
823
1255
  description: str = "",
1256
+ *,
1257
+ optional_label: bool = False,
824
1258
  hide_value: bool = False,
1259
+ hide_in_modifier: bool = False,
825
1260
  ):
826
1261
  super().__init__(name, description)
827
1262
  self._set_values(
828
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,
829
1288
  hide_value=hide_value,
1289
+ hide_in_modifier=hide_in_modifier,
830
1290
  )
831
1291
 
832
1292
 
@@ -840,14 +1300,18 @@ class SocketCollection(SocketBase):
840
1300
  self,
841
1301
  name: str = "Collection",
842
1302
  default_value: bpy.types.Collection | None = None,
843
- *,
844
1303
  description: str = "",
1304
+ *,
1305
+ optional_label: bool = False,
845
1306
  hide_value: bool = False,
1307
+ hide_in_modifier: bool = False,
846
1308
  ):
847
1309
  super().__init__(name, description)
848
1310
  self._set_values(
849
1311
  default_value=default_value,
1312
+ optional_label=optional_label,
850
1313
  hide_value=hide_value,
1314
+ hide_in_modifier=hide_in_modifier,
851
1315
  )
852
1316
 
853
1317
 
@@ -861,14 +1325,18 @@ class SocketImage(SocketBase):
861
1325
  self,
862
1326
  name: str = "Image",
863
1327
  default_value: bpy.types.Image | None = None,
864
- *,
865
1328
  description: str = "",
1329
+ *,
1330
+ optional_label: bool = False,
866
1331
  hide_value: bool = False,
1332
+ hide_in_modifier: bool = False,
867
1333
  ):
868
1334
  super().__init__(name, description)
869
1335
  self._set_values(
870
1336
  default_value=default_value,
1337
+ optional_label=optional_label,
871
1338
  hide_value=hide_value,
1339
+ hide_in_modifier=hide_in_modifier,
872
1340
  )
873
1341
 
874
1342
 
@@ -882,14 +1350,18 @@ class SocketMaterial(SocketBase):
882
1350
  self,
883
1351
  name: str = "Material",
884
1352
  default_value: bpy.types.Material | None = None,
885
- *,
886
1353
  description: str = "",
1354
+ *,
1355
+ optional_label: bool = False,
887
1356
  hide_value: bool = False,
1357
+ hide_in_modifier: bool = False,
888
1358
  ):
889
1359
  super().__init__(name, description)
890
1360
  self._set_values(
891
1361
  default_value=default_value,
1362
+ optional_label=optional_label,
892
1363
  hide_value=hide_value,
1364
+ hide_in_modifier=hide_in_modifier,
893
1365
  )
894
1366
 
895
1367
 
@@ -902,13 +1374,17 @@ class SocketBundle(SocketBase):
902
1374
  def __init__(
903
1375
  self,
904
1376
  name: str = "Bundle",
905
- *,
906
1377
  description: str = "",
1378
+ *,
1379
+ optional_label: bool = False,
907
1380
  hide_value: bool = False,
1381
+ hide_in_modifier: bool = False,
908
1382
  ):
909
1383
  super().__init__(name, description)
910
1384
  self._set_values(
1385
+ optional_label=optional_label,
911
1386
  hide_value=hide_value,
1387
+ hide_in_modifier=hide_in_modifier,
912
1388
  )
913
1389
 
914
1390
 
@@ -921,11 +1397,15 @@ class SocketClosure(SocketBase):
921
1397
  def __init__(
922
1398
  self,
923
1399
  name: str = "Closure",
924
- *,
925
1400
  description: str = "",
1401
+ *,
1402
+ optional_label: bool = False,
926
1403
  hide_value: bool = False,
1404
+ hide_in_modifier: bool = False,
927
1405
  ):
928
1406
  super().__init__(name, description)
929
1407
  self._set_values(
1408
+ optional_label=optional_label,
930
1409
  hide_value=hide_value,
1410
+ hide_in_modifier=hide_in_modifier,
931
1411
  )