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