nodebpy 0.2.0__py3-none-any.whl → 0.3.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
@@ -1,9 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from ast import Return
3
4
  from typing import TYPE_CHECKING, Any, ClassVar, Literal
4
5
 
6
+ from arrangebpy.arrange.routing import Socket
7
+ from numpy import isin
8
+
5
9
  if TYPE_CHECKING:
6
- from .nodes.converter import Math, VectorMath
10
+ from .nodes import Math, VectorMath
7
11
 
8
12
  import arrangebpy
9
13
  import bpy
@@ -14,13 +18,14 @@ from bpy.types import (
14
18
  NodeSocket,
15
19
  )
16
20
 
17
- from .nodes.types import (
21
+ from .types import (
18
22
  LINKABLE,
19
23
  SOCKET_COMPATIBILITY,
20
24
  SOCKET_TYPES,
21
25
  TYPE_INPUT_ALL,
22
26
  FloatInterfaceSubtypes,
23
27
  IntegerInterfaceSubtypes,
28
+ NodeBooleanMathItems,
24
29
  StringInterfaceSubtypes,
25
30
  VectorInterfaceSubtypes,
26
31
  _AttributeDomains,
@@ -52,6 +57,10 @@ def denormalize_name(attr_name: str) -> str:
52
57
  return attr_name.replace("_", " ").title()
53
58
 
54
59
 
60
+ class SocketError(Exception):
61
+ """Raised when a socket operation fails."""
62
+
63
+
55
64
  class SocketContext:
56
65
  _direction: Literal["INPUT", "OUTPUT"] | None
57
66
  _active_context: SocketContext | None = None
@@ -117,12 +126,10 @@ class TreeBuilder:
117
126
  just_added: "Node | None" = None
118
127
 
119
128
  def __init__(
120
- self, tree: "GeometryNodeTree | str | None" = None, arrange: bool = True
129
+ self, tree: GeometryNodeTree | str = "Geometry Nodes", arrange: bool = True
121
130
  ):
122
131
  if isinstance(tree, str):
123
132
  self.tree = bpy.data.node_groups.new(tree, "GeometryNodeTree")
124
- elif tree is None:
125
- self.tree = bpy.data.node_groups.new("GeometryNodeTree", "GeometryNodeTree")
126
133
  else:
127
134
  assert isinstance(tree, GeometryNodeTree)
128
135
  self.tree = tree
@@ -192,17 +199,27 @@ class TreeBuilder:
192
199
  if isinstance(socket2, SocketLinker):
193
200
  socket2 = socket2.socket
194
201
 
202
+ if (
203
+ socket1.type not in SOCKET_COMPATIBILITY.get(socket2.type, ())
204
+ and socket2.type != "CUSTOM"
205
+ ):
206
+ raise SocketError(
207
+ f"Incompatible socket types, {socket1.type} and {socket2.type}"
208
+ )
209
+
195
210
  link = self.tree.links.new(socket1, socket2, handle_dynamic_sockets=True)
196
211
 
197
212
  if any(socket.is_inactive for socket in [socket1, socket2]):
213
+ assert socket1.node
214
+ assert socket2.node
198
215
  # the warning message should report which sockets from which nodes were linked and which were innactive
199
216
  for socket in [socket1, socket2]:
200
217
  # 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 (
218
+ if socket.is_inactive and socket.node.bl_idname not in ( # type: ignore
202
219
  "GeometryNodeIndexSwitch",
203
220
  "GeometryNodeMenuSwitch",
204
221
  ):
205
- message = f"Socket {socket.name} from node {socket.node.name} is inactive."
222
+ message = f"Socket {socket.name} from node {socket.node.name} is inactive." # type: ignore
206
223
  message += f" It is linked to socket {socket2.name} from node {socket2.node.name}."
207
224
  message += " This link will be created by Blender but ignored when evaluated."
208
225
  message += f"Socket type: {socket.bl_idname}"
@@ -211,21 +228,19 @@ class TreeBuilder:
211
228
  return link
212
229
 
213
230
  def add(self, name: str) -> Node:
214
- self.just_added = self.tree.nodes.new(name) # type: ignore
215
- assert self.just_added is not None
216
- return self.just_added
231
+ return self.tree.nodes.new(name)
217
232
 
218
233
 
219
234
  class NodeBuilder:
220
235
  """Base class for all geometry node wrappers."""
221
236
 
222
237
  node: Any
238
+ _bl_idname: str
223
239
  _tree: "TreeBuilder"
224
240
  _link_target: str | None = None
225
241
  _from_socket: NodeSocket | None = None
226
242
  _default_input_id: str | None = None
227
243
  _default_output_id: str | None = None
228
- _socket_data_types = tuple(SOCKET_COMPATIBILITY.keys())
229
244
 
230
245
  def __init__(self):
231
246
  # Get active tree from context manager
@@ -241,7 +256,7 @@ class NodeBuilder:
241
256
  self._tree = tree
242
257
  self._link_target = None
243
258
  if self.__class__.name is not None:
244
- self.node = self._tree.add(self.__class__.name)
259
+ self.node = self._tree.add(self.__class__._bl_idname)
245
260
  else:
246
261
  raise ValueError(
247
262
  f"Class {self.__class__.__name__} must define a 'name' attribute"
@@ -273,7 +288,13 @@ class NodeBuilder:
273
288
  def _default_output_socket(self) -> NodeSocket:
274
289
  if self._default_output_id is not None:
275
290
  return self.node.outputs[self._output_idx(self._default_output_id)]
276
- return self.node.outputs[0]
291
+
292
+ counter = 0
293
+ socket = self.node.outputs[counter]
294
+ while not socket.is_icon_visible:
295
+ counter += 1
296
+ socket = self.node.outputs[counter]
297
+ return socket
277
298
 
278
299
  def _source_socket(self, node: LINKABLE | SocketLinker | NodeSocket) -> NodeSocket:
279
300
  assert node
@@ -297,286 +318,67 @@ class NodeBuilder:
297
318
  else:
298
319
  raise TypeError(f"Unsupported type: {type(node)}")
299
320
 
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]
321
+ @property
322
+ def _available_outputs(self) -> list[NodeSocket]:
323
+ return [socket for socket in self.node.outputs if socket.is_icon_visible]
347
324
 
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
325
+ @property
326
+ def _available_inputs(self) -> list[NodeSocket]:
327
+ return [
328
+ socket
329
+ for socket in self.node.inputs
330
+ # only sockets that are available, don't have a link already (unless multi-input)
331
+ if not socket.is_inactive
332
+ and socket.is_icon_visible
333
+ and (not socket.links or socket.is_multi_input)
402
334
  ]
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
335
 
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
336
+ def _best_output_socket(self, type: str) -> NodeSocket:
337
+ compatible = SOCKET_COMPATIBILITY.get(type, ())
338
+ possible = [
339
+ socket for socket in self._available_outputs if socket.type in compatible
340
+ ]
341
+ if possible:
342
+ possible.sort(key=lambda x: compatible.index(x.type))
343
+ return possible[0]
473
344
 
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
345
+ raise SocketError("No compatible output sockets found")
482
346
 
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, []
347
+ @staticmethod
348
+ def _find_best_socket_pair(
349
+ source: "NodeBuilder | NodeSocket", target: "NodeBuilder | NodeSocket"
350
+ ) -> tuple[NodeSocket, NodeSocket]:
351
+ """Find the best possible compatible pair of sockets between two nodes, looking only at the
352
+ the currently available outputs from the source and the inputs from the target"""
353
+ possible_combos = []
354
+ if isinstance(source, NodeBuilder):
355
+ outputs = source._available_outputs
356
+ else:
357
+ outputs = [source]
358
+ if isinstance(target, NodeBuilder):
359
+ inputs = target._available_inputs
360
+ else:
361
+ inputs = [target]
362
+ for output in outputs:
363
+ compat_sockets = SOCKET_COMPATIBILITY.get(output.type, ())
364
+ for input in inputs:
365
+ if input.type == output.type:
366
+ return input, output
367
+
368
+ if input.type in compat_sockets:
369
+ possible_combos.append(
370
+ (compat_sockets.index(input.type), (input, output))
529
371
  )
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
372
 
538
- def _smart_link_to(self, target_node: "NodeBuilder") -> "NodeBuilder":
539
- """Smart linking that creates compatible sockets when needed.
373
+ if possible_combos:
374
+ # sort by distance between compatible sockets and return the best match
375
+ return sorted(possible_combos, key=lambda x: x[0])[0][1]
540
376
 
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
377
+ raise SocketError(
378
+ f"Cannot link any output from {source.node.name} to any input of {target.node.name}. "
379
+ f"Available output types: {[f'{o.name}:{o.type}' for o in outputs]}, "
380
+ f"Available input types: {[f'{i.name}:{i.type}' for i in inputs]}"
571
381
  )
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
382
 
581
383
  def _input_idx(self, identifier: str) -> int:
582
384
  # currently there is a Blender bug that is preventing the lookup of sockets from identifiers on some
@@ -615,28 +417,15 @@ class NodeBuilder:
615
417
  def _link(
616
418
  self, source: LINKABLE | SocketLinker | NodeSocket, target: LINKABLE
617
419
  ) -> bpy.types.NodeLink:
618
- return self.tree.link(self._source_socket(source), self._target_socket(target))
619
-
620
- def _link_to(self, target: LINKABLE) -> bpy.types.NodeLink:
621
- return self.tree.link(self._default_output_socket, self._target_socket(target))
420
+ source_socket = self._source_socket(source)
421
+ target_socket = self._target_socket(target)
422
+ return self.tree.link(source_socket, target_socket)
622
423
 
623
424
  def _link_from(
624
425
  self,
625
426
  source: LINKABLE,
626
427
  input: LINKABLE | str,
627
428
  ):
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)
639
-
640
429
  if isinstance(input, str):
641
430
  try:
642
431
  self._link(source, self.node.inputs[input])
@@ -645,39 +434,6 @@ class NodeBuilder:
645
434
  else:
646
435
  self._link(source, input)
647
436
 
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
437
  def _set_input_default_value(self, input, value):
682
438
  """Set the default value for an input socket, handling type conversions."""
683
439
  if (
@@ -689,62 +445,28 @@ class NodeBuilder:
689
445
  else:
690
446
  input.default_value = value
691
447
 
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]
727
-
728
448
  def _establish_links(self, **kwargs: TYPE_INPUT_ALL):
729
449
  input_ids = [input.identifier for input in self.node.inputs]
730
450
  for name, value in kwargs.items():
731
451
  if value is None:
732
452
  continue
453
+ if isinstance(value, Node):
454
+ node = NodeBuilder()
455
+ node.node = value
456
+ value = node
733
457
 
734
458
  if value is ...:
735
459
  # Ellipsis indicates this input should receive links from >> operator
736
460
  # which can potentially target multiple inputs on the new node
737
461
  if self._from_socket is not None:
738
- self._link(
739
- self._from_socket, self.node.inputs[self._input_idx(name)]
740
- )
462
+ self._link_from(self._from_socket, name)
741
463
 
742
464
  elif isinstance(value, SocketLinker):
743
- self._link(value, self.node.inputs[self._input_idx(name)])
744
- # we can also provide just a default value for the socket to take if we aren't
745
- # providing a socket to link with
746
- elif isinstance(value, (NodeBuilder, NodeSocket, Node)):
747
- self._smart_link_from(value, name)
465
+ self._link_from(value, name)
466
+ elif isinstance(value, NodeSocket):
467
+ self._link_from(value, name)
468
+ elif isinstance(value, NodeBuilder):
469
+ self._link_from(value._best_output_socket(self._input(name).type), name)
748
470
  else:
749
471
  if name in input_ids:
750
472
  input = self.node.inputs[input_ids.index(name)]
@@ -766,31 +488,16 @@ class NodeBuilder:
766
488
  Returns the right-hand node to enable continued chaining.
767
489
  """
768
490
  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
491
+ source = self._default_output_socket
492
+ target = other.socket
493
+ other._from_socket = source
773
494
  else:
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
495
+ try:
496
+ source, target = self._find_best_socket_pair(self, other)
497
+ except SocketError:
498
+ source, target = other._find_best_socket_pair(self, other)
792
499
 
793
- self.tree.link(socket_out, socket_in)
500
+ self.tree.link(source, target)
794
501
  return other
795
502
 
796
503
  def _get_input_socket_by_name(self, node: "NodeBuilder", name: str) -> NodeSocket:
@@ -805,7 +512,7 @@ class NodeBuilder:
805
512
  self, other: Any, operation: str, reverse: bool = False
806
513
  ) -> "VectorMath | Math":
807
514
  """Apply a math operation with appropriate Math/VectorMath node."""
808
- from .nodes.converter import VectorMath
515
+ from .nodes import VectorMath
809
516
 
810
517
  values = (
811
518
  (self._default_output_socket, other)
@@ -813,14 +520,14 @@ class NodeBuilder:
813
520
  else (other, self._default_output_socket)
814
521
  )
815
522
 
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"
523
+ component_is_vector = False
524
+ for value in values:
525
+ if getattr(value, "type", None) == "VECTOR":
526
+ component_is_vector = True
527
+ break
821
528
 
822
529
  # Use VectorMath if either operand is a vector
823
- if self_is_vector or other_is_vector:
530
+ if component_is_vector:
824
531
  if operation == "multiply":
825
532
  # Handle special cases for vector multiplication where we might scale instead
826
533
  # of using the multiply method
@@ -832,7 +539,7 @@ class NodeBuilder:
832
539
  return VectorMath.multiply(*values)
833
540
  else:
834
541
  raise TypeError(
835
- f"Unsupported type for {operation} with VECTOR socket: {type(other)}"
542
+ f"Unsupported type for {operation} with VECTOR socket: {type(other)}, {other=}"
836
543
  )
837
544
  else:
838
545
  vector_method = getattr(VectorMath, operation)
@@ -852,11 +559,10 @@ class NodeBuilder:
852
559
  raise TypeError(
853
560
  f"Unsupported type for {operation} with VECTOR operand: {type(other)}"
854
561
  )
855
- else:
856
- # Both operands are scalar types, use regular Math
562
+ else: # Both operands are scalar types, use regular Math
857
563
  from .nodes.converter import IntegerMath, Math
858
564
 
859
- if isinstance(other, int):
565
+ if isinstance(other, int) and self._default_output_socket.type == "INT":
860
566
  return getattr(IntegerMath, operation)(*values)
861
567
  else:
862
568
  return getattr(Math, operation)(*values)
@@ -886,6 +592,78 @@ class NodeBuilder:
886
592
  return self._apply_math_operation(other, "subtract", reverse=True)
887
593
 
888
594
 
595
+ class DynamicInputsMixin:
596
+ _socket_data_types: tuple[str]
597
+ _type_map: dict[str, str] = {}
598
+
599
+ def _match_compatible_data(self, *sockets: NodeSocket) -> tuple[NodeSocket, str]:
600
+ possible = []
601
+ for socket in sockets:
602
+ compatible = SOCKET_COMPATIBILITY.get(socket.type, ())
603
+ for type in self._socket_data_types:
604
+ if type in compatible:
605
+ possible.append((socket, type, compatible.index(type)))
606
+
607
+ if len(possible) > 0:
608
+ possible.sort(key=lambda x: x[2])
609
+ best_value = possible[0]
610
+ return best_value[:2]
611
+
612
+ raise SocketError("No compatible socket found")
613
+
614
+ def _find_best_socket_pair(
615
+ self, source: NodeBuilder, target: NodeBuilder
616
+ ) -> tuple[NodeSocket, NodeSocket] | None:
617
+ try:
618
+ return super()._find_best_socket_pair(source, target)
619
+ except SocketError:
620
+ if target == self:
621
+ target_name, source_socket = list(target._add_inputs(source).items())[0]
622
+ return (source_socket, target.inputs[target_name].socket)
623
+ else:
624
+ target_name, source_socket = list(
625
+ source._add_inputs(*target.node.inputs).items()
626
+ )[0]
627
+ return (
628
+ source.outputs[target_name].socket,
629
+ target.inputs[target_name].socket,
630
+ )
631
+
632
+ for target_name, source_socket in new_sockets.items():
633
+ target_socket = target.inputs[target_name].socket
634
+ return (source_socket, target_socket)
635
+
636
+ # def _best_output_socket(self, type: str) -> NodeSocket:
637
+ # # compatible = SOCKET_COMPATIBILITY.get(type, ())
638
+ # # possible = [
639
+ # # socket for socket in self._available_outputs if socket.type in compatible
640
+ # # ]
641
+ # # if possible:
642
+ # # return sorted(possible, key=lambda x: compatible.index(x.type))[0]
643
+
644
+ # raise SocketError("No compatible output sockets found")
645
+
646
+ def _add_inputs(self, *args, **kwargs) -> dict[str, LINKABLE]:
647
+ """Dictionary with {new_socket.name: from_linkable} for link creation"""
648
+ new_sockets = {}
649
+ items = {}
650
+ for arg in args:
651
+ if isinstance(arg, bpy.types.NodeSocket):
652
+ name = arg.name
653
+ items[name] = arg
654
+ else:
655
+ items[arg._default_output_socket.name] = arg
656
+ items.update(kwargs)
657
+ for key, source in items.items():
658
+ socket_source, type = self._match_compatible_data(*source.node.outputs)
659
+ if type in self._type_map:
660
+ type = self._type_map[type]
661
+ socket = self._add_socket(name=key, type=type)
662
+ new_sockets[socket.name] = socket_source
663
+
664
+ return new_sockets
665
+
666
+
889
667
  class SocketLinker(NodeBuilder):
890
668
  def __init__(self, socket: NodeSocket):
891
669
  assert socket.node is not None
@@ -894,6 +672,10 @@ class SocketLinker(NodeBuilder):
894
672
  self._default_output_id = socket.identifier
895
673
  self._tree = TreeBuilder(socket.node.id_data) # type: ignore
896
674
 
675
+ @property
676
+ def _available_outputs(self) -> list[NodeSocket]:
677
+ return [self.socket]
678
+
897
679
  @property
898
680
  def type(self) -> SOCKET_TYPES:
899
681
  return self.socket.type # type: ignore