nodebpy 0.2.1__py3-none-any.whl → 0.3.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
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, ClassVar, Literal
3
+ from typing import TYPE_CHECKING, Any, ClassVar, Iterable, Literal
4
4
 
5
5
  if TYPE_CHECKING:
6
6
  from .nodes import Math, VectorMath
@@ -52,6 +52,10 @@ def denormalize_name(attr_name: str) -> str:
52
52
  return attr_name.replace("_", " ").title()
53
53
 
54
54
 
55
+ class SocketError(Exception):
56
+ """Raised when a socket operation fails."""
57
+
58
+
55
59
  class SocketContext:
56
60
  _direction: Literal["INPUT", "OUTPUT"] | None
57
61
  _active_context: SocketContext | None = None
@@ -112,8 +116,9 @@ class OutputInterfaceContext(DirectionalContext):
112
116
  class TreeBuilder:
113
117
  """Builder for creating Blender geometry node trees with a clean Python API."""
114
118
 
115
- _active_tree: ClassVar["TreeBuilder | None"] = None
116
- _previous_tree: ClassVar["TreeBuilder | None"] = None
119
+ _tree_contexts: ClassVar["list[TreeBuilder]"] = []
120
+ # _active_tree: ClassVar["TreeBuilder | None"] = None
121
+ # _previous_tree: ClassVar["list[TreeBuilder]"] = list()
117
122
  just_added: "Node | None" = None
118
123
 
119
124
  def __init__(
@@ -130,16 +135,22 @@ class TreeBuilder:
130
135
  self.outputs = OutputInterfaceContext(self)
131
136
  self._arrange = arrange
132
137
 
138
+ def activate_tree(self) -> None:
139
+ """Make this tree the active tree for all new node creation."""
140
+ TreeBuilder._tree_contexts.append(self)
141
+
142
+ def deactivate_tree(self) -> None:
143
+ """Whatever tree ws previously active is set to be the active one (or None if no previously active tree)."""
144
+ TreeBuilder._tree_contexts.pop()
145
+
133
146
  def __enter__(self):
134
- TreeBuilder._previous_tree = TreeBuilder._active_tree
135
- TreeBuilder._active_tree = self
147
+ self.activate_tree()
136
148
  return self
137
149
 
138
150
  def __exit__(self, *args):
139
151
  if self._arrange:
140
152
  self.arrange()
141
- TreeBuilder._active_tree = TreeBuilder._previous_tree
142
- TreeBuilder._previous_tree = None
153
+ self.deactivate_tree()
143
154
 
144
155
  @property
145
156
  def nodes(self) -> Nodes:
@@ -190,6 +201,14 @@ class TreeBuilder:
190
201
  if isinstance(socket2, SocketLinker):
191
202
  socket2 = socket2.socket
192
203
 
204
+ if (
205
+ socket1.type not in SOCKET_COMPATIBILITY.get(socket2.type, ())
206
+ and socket2.type != "CUSTOM"
207
+ ):
208
+ raise SocketError(
209
+ f"Incompatible socket types, {socket1.type} and {socket2.type}"
210
+ )
211
+
193
212
  link = self.tree.links.new(socket1, socket2, handle_dynamic_sockets=True)
194
213
 
195
214
  if any(socket.is_inactive for socket in [socket1, socket2]):
@@ -224,11 +243,12 @@ class NodeBuilder:
224
243
  _from_socket: NodeSocket | None = None
225
244
  _default_input_id: str | None = None
226
245
  _default_output_id: str | None = None
227
- _socket_data_types = tuple(SOCKET_COMPATIBILITY.keys())
228
246
 
229
247
  def __init__(self):
230
248
  # Get active tree from context manager
231
- tree = TreeBuilder._active_tree
249
+ tree = (
250
+ TreeBuilder._tree_contexts[-1] if len(TreeBuilder._tree_contexts) else None
251
+ )
232
252
  if tree is None:
233
253
  raise RuntimeError(
234
254
  f"Node '{self.__class__.__name__}' must be created within a TreeBuilder context manager.\n"
@@ -276,7 +296,6 @@ class NodeBuilder:
276
296
  counter = 0
277
297
  socket = self.node.outputs[counter]
278
298
  while not socket.is_icon_visible:
279
- print(f"skipping inactive socket {socket.name}")
280
299
  counter += 1
281
300
  socket = self.node.outputs[counter]
282
301
  return socket
@@ -303,286 +322,69 @@ class NodeBuilder:
303
322
  else:
304
323
  raise TypeError(f"Unsupported type: {type(node)}")
305
324
 
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]
325
+ @property
326
+ def _available_outputs(self) -> list[NodeSocket]:
327
+ return [socket for socket in self.node.outputs if socket.is_icon_visible]
353
328
 
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
329
+ @property
330
+ def _available_inputs(self) -> list[NodeSocket]:
331
+ return [
332
+ socket
333
+ for socket in self.node.inputs
334
+ # only sockets that are available, don't have a link already (unless multi-input)
335
+ if not socket.is_inactive
336
+ and socket.is_icon_visible
337
+ and (not socket.links or socket.is_multi_input)
408
338
  ]
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
339
 
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
340
+ def _best_output_socket(self, type: str) -> NodeSocket:
341
+ compatible = SOCKET_COMPATIBILITY.get(type, ())
342
+ possible = [
343
+ socket for socket in self._available_outputs if socket.type in compatible
344
+ ]
345
+ if possible:
346
+ possible.sort(key=lambda x: compatible.index(x.type))
347
+ return possible[0]
479
348
 
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
349
+ raise SocketError("No compatible output sockets found")
488
350
 
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, []
351
+ def _find_best_socket_pair(
352
+ self,
353
+ source: "NodeBuilder | SocketLinker | NodeSocket",
354
+ target: "NodeBuilder | SocketLinker | NodeSocket",
355
+ ) -> tuple[NodeSocket, NodeSocket]:
356
+ """Find the best possible compatible pair of sockets between two nodes, looking only at the
357
+ the currently available outputs from the source and the inputs from the target"""
358
+ possible_combos = []
359
+ if isinstance(source, (NodeBuilder, SocketLinker)):
360
+ outputs = source._available_outputs
361
+ elif isinstance(source, NodeSocket):
362
+ outputs = [source]
363
+
364
+ if isinstance(target, (NodeBuilder, SocketLinker)):
365
+ inputs = target._available_inputs
366
+ else:
367
+ inputs = [target]
368
+ for output in outputs:
369
+ compat_sockets = SOCKET_COMPATIBILITY.get(output.type, ())
370
+ for input in inputs:
371
+ if input.type == output.type:
372
+ return input, output
373
+
374
+ if input.type in compat_sockets:
375
+ possible_combos.append(
376
+ (compat_sockets.index(input.type), (input, output))
535
377
  )
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
378
 
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.
379
+ if possible_combos:
380
+ # sort by distance between compatible sockets and return the best match
381
+ return sorted(possible_combos, key=lambda x: x[0])[0][1]
549
382
 
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
383
+ raise SocketError(
384
+ f"Cannot link any output from {source.node.name} to any input of {target.node.name}. "
385
+ f"Available output types: {[f'{o.name}:{o.type}' for o in outputs]}, "
386
+ f"Available input types: {[f'{i.name}:{i.type}' for i in inputs]}"
577
387
  )
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
586
388
 
587
389
  def _input_idx(self, identifier: str) -> int:
588
390
  # currently there is a Blender bug that is preventing the lookup of sockets from identifiers on some
@@ -621,28 +423,15 @@ class NodeBuilder:
621
423
  def _link(
622
424
  self, source: LINKABLE | SocketLinker | NodeSocket, target: LINKABLE
623
425
  ) -> bpy.types.NodeLink:
624
- return self.tree.link(self._source_socket(source), self._target_socket(target))
625
-
626
- def _link_to(self, target: LINKABLE) -> bpy.types.NodeLink:
627
- return self.tree.link(self._default_output_socket, self._target_socket(target))
426
+ source_socket = self._source_socket(source)
427
+ target_socket = self._target_socket(target)
428
+ return self.tree.link(source_socket, target_socket)
628
429
 
629
430
  def _link_from(
630
431
  self,
631
432
  source: LINKABLE,
632
433
  input: LINKABLE | str,
633
434
  ):
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)
645
-
646
435
  if isinstance(input, str):
647
436
  try:
648
437
  self._link(source, self.node.inputs[input])
@@ -651,39 +440,6 @@ class NodeBuilder:
651
440
  else:
652
441
  self._link(source, input)
653
442
 
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
443
  def _set_input_default_value(self, input, value):
688
444
  """Set the default value for an input socket, handling type conversions."""
689
445
  if (
@@ -695,62 +451,28 @@ class NodeBuilder:
695
451
  else:
696
452
  input.default_value = value
697
453
 
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
454
  def _establish_links(self, **kwargs: TYPE_INPUT_ALL):
735
455
  input_ids = [input.identifier for input in self.node.inputs]
736
456
  for name, value in kwargs.items():
737
457
  if value is None:
738
458
  continue
459
+ if isinstance(value, Node):
460
+ node = NodeBuilder()
461
+ node.node = value
462
+ value = node
739
463
 
740
464
  if value is ...:
741
465
  # Ellipsis indicates this input should receive links from >> operator
742
466
  # which can potentially target multiple inputs on the new node
743
467
  if self._from_socket is not None:
744
- self._link(
745
- self._from_socket, self.node.inputs[self._input_idx(name)]
746
- )
468
+ self._link_from(self._from_socket, name)
747
469
 
748
470
  elif isinstance(value, SocketLinker):
749
- self._link(value, self.node.inputs[self._input_idx(name)])
750
- # we can also provide just a default value for the socket to take if we aren't
751
- # providing a socket to link with
752
- elif isinstance(value, (NodeBuilder, NodeSocket, Node)):
753
- self._smart_link_from(value, name)
471
+ self._link_from(value, name)
472
+ elif isinstance(value, NodeSocket):
473
+ self._link_from(value, name)
474
+ elif isinstance(value, NodeBuilder):
475
+ self._link_from(value._best_output_socket(self._input(name).type), name)
754
476
  else:
755
477
  if name in input_ids:
756
478
  input = self.node.inputs[input_ids.index(name)]
@@ -772,31 +494,16 @@ class NodeBuilder:
772
494
  Returns the right-hand node to enable continued chaining.
773
495
  """
774
496
  if isinstance(other, SocketLinker):
775
- # Direct socket linking - use default output
776
- socket_out = self._default_output_socket
777
- socket_in = other.socket
778
- other._from_socket = socket_out
497
+ source = self._default_output_socket
498
+ target = other.socket
499
+ other._from_socket = source
779
500
  else:
780
- # Standard NodeBuilder linking - need to find compatible sockets
781
- if other._link_target is not None:
782
- # Target socket is specified
783
- socket_in = self._get_input_socket_by_name(other, other._link_target)
784
- socket_out = self._default_output_socket
785
-
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
501
+ try:
502
+ source, target = self._find_best_socket_pair(self, other)
503
+ except SocketError:
504
+ source, target = other._find_best_socket_pair(self, other)
798
505
 
799
- self.tree.link(socket_out, socket_in)
506
+ self.tree.link(source, target)
800
507
  return other
801
508
 
802
509
  def _get_input_socket_by_name(self, node: "NodeBuilder", name: str) -> NodeSocket:
@@ -819,14 +526,14 @@ class NodeBuilder:
819
526
  else (other, self._default_output_socket)
820
527
  )
821
528
 
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"
529
+ component_is_vector = False
530
+ for value in values:
531
+ if getattr(value, "type", None) == "VECTOR":
532
+ component_is_vector = True
533
+ break
827
534
 
828
535
  # Use VectorMath if either operand is a vector
829
- if self_is_vector or other_is_vector:
536
+ if component_is_vector:
830
537
  if operation == "multiply":
831
538
  # Handle special cases for vector multiplication where we might scale instead
832
539
  # of using the multiply method
@@ -838,7 +545,7 @@ class NodeBuilder:
838
545
  return VectorMath.multiply(*values)
839
546
  else:
840
547
  raise TypeError(
841
- f"Unsupported type for {operation} with VECTOR socket: {type(other)}"
548
+ f"Unsupported type for {operation} with VECTOR socket: {type(other)}, {other=}"
842
549
  )
843
550
  else:
844
551
  vector_method = getattr(VectorMath, operation)
@@ -858,11 +565,10 @@ class NodeBuilder:
858
565
  raise TypeError(
859
566
  f"Unsupported type for {operation} with VECTOR operand: {type(other)}"
860
567
  )
861
- else:
862
- # Both operands are scalar types, use regular Math
568
+ else: # Both operands are scalar types, use regular Math
863
569
  from .nodes.converter import IntegerMath, Math
864
570
 
865
- if isinstance(other, int):
571
+ if isinstance(other, int) and self._default_output_socket.type == "INT":
866
572
  return getattr(IntegerMath, operation)(*values)
867
573
  else:
868
574
  return getattr(Math, operation)(*values)
@@ -892,6 +598,80 @@ class NodeBuilder:
892
598
  return self._apply_math_operation(other, "subtract", reverse=True)
893
599
 
894
600
 
601
+ class DynamicInputsMixin:
602
+ _socket_data_types: tuple[str]
603
+ _type_map: dict[str, str] = {}
604
+
605
+ def _match_compatible_data(
606
+ self, sockets: Iterable[NodeSocket]
607
+ ) -> tuple[NodeSocket, str]:
608
+ possible = []
609
+ for socket in sockets:
610
+ compatible = SOCKET_COMPATIBILITY.get(socket.type, ())
611
+ for type in self._socket_data_types:
612
+ if type in compatible:
613
+ possible.append((socket, type, compatible.index(type)))
614
+
615
+ if len(possible) > 0:
616
+ possible.sort(key=lambda x: x[2])
617
+ best_value = possible[0]
618
+ return best_value[:2]
619
+
620
+ raise SocketError("No compatible socket found")
621
+
622
+ def _find_best_socket_pair(
623
+ self, source: NodeBuilder | NodeSocket, target: NodeBuilder | NodeSocket
624
+ ) -> tuple[NodeSocket, NodeSocket]:
625
+ try:
626
+ return super()._find_best_socket_pair(source, target)
627
+ except SocketError:
628
+ if target == self:
629
+ target_name, source_socket = list(target._add_inputs(source).items())[0]
630
+ return (source_socket, target.inputs[target_name].socket)
631
+ else:
632
+ target_name, source_socket = list(
633
+ source._add_inputs(*target.node.inputs).items()
634
+ )[0]
635
+ return (
636
+ source.outputs[target_name].socket,
637
+ target.inputs[target_name].socket,
638
+ )
639
+
640
+ # for target_name, source_socket in new_sockets.items():
641
+ # target_socket = target.inputs[target_name].socket
642
+ # return (source_socket, target_socket)
643
+
644
+ # def _best_output_socket(self, type: str) -> NodeSocket:
645
+ # # compatible = SOCKET_COMPATIBILITY.get(type, ())
646
+ # # possible = [
647
+ # # socket for socket in self._available_outputs if socket.type in compatible
648
+ # # ]
649
+ # # if possible:
650
+ # # return sorted(possible, key=lambda x: compatible.index(x.type))[0]
651
+
652
+ # raise SocketError("No compatible output sockets found")
653
+
654
+ def _add_inputs(self, *args, **kwargs) -> dict[str, LINKABLE]:
655
+ """Dictionary with {new_socket.name: from_linkable} for link creation"""
656
+ new_sockets = {}
657
+ items = {}
658
+ for arg in args:
659
+ if isinstance(arg, bpy.types.NodeSocket):
660
+ name = arg.name
661
+ items[name] = arg
662
+ else:
663
+ items[arg._default_output_socket.name] = arg
664
+ items.update(kwargs)
665
+ for key, source in items.items():
666
+ socket_source, type = self._match_compatible_data(source._available_outputs)
667
+ if type in self._type_map:
668
+ type = self._type_map[type]
669
+ socket = self._add_socket(name=key, type=type)
670
+ new_sockets[socket.name] = socket_source
671
+
672
+ return new_sockets
673
+
674
+
895
675
  class SocketLinker(NodeBuilder):
896
676
  def __init__(self, socket: NodeSocket):
897
677
  assert socket.node is not None
@@ -900,6 +680,14 @@ class SocketLinker(NodeBuilder):
900
680
  self._default_output_id = socket.identifier
901
681
  self._tree = TreeBuilder(socket.node.id_data) # type: ignore
902
682
 
683
+ @property
684
+ def _available_outputs(self) -> list[NodeSocket]:
685
+ return [self.socket]
686
+
687
+ @property
688
+ def _available_inputs(self) -> list[NodeSocket]:
689
+ return [self.socket]
690
+
903
691
  @property
904
692
  def type(self) -> SOCKET_TYPES:
905
693
  return self.socket.type # type: ignore