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 +195 -407
- nodebpy/nodes/__init__.py +6 -0
- nodebpy/nodes/attribute.py +98 -4
- nodebpy/nodes/color.py +6 -2
- nodebpy/nodes/converter.py +357 -54
- nodebpy/nodes/experimental.py +9 -3
- nodebpy/nodes/geometry.py +390 -94
- nodebpy/nodes/grid.py +90 -30
- nodebpy/nodes/group.py +3 -1
- nodebpy/nodes/input.py +239 -78
- nodebpy/nodes/interface.py +21 -7
- nodebpy/nodes/manual.py +46 -17
- nodebpy/nodes/output.py +8 -1
- nodebpy/nodes/texture.py +30 -10
- nodebpy/nodes/vector.py +41 -4
- nodebpy/nodes/zone.py +199 -204
- nodebpy/types.py +1 -1
- {nodebpy-0.2.1.dist-info → nodebpy-0.3.1.dist-info}/METADATA +14 -4
- nodebpy-0.3.1.dist-info/RECORD +26 -0
- nodebpy-0.2.1.dist-info/RECORD +0 -26
- {nodebpy-0.2.1.dist-info → nodebpy-0.3.1.dist-info}/WHEEL +0 -0
- {nodebpy-0.2.1.dist-info → nodebpy-0.3.1.dist-info}/entry_points.txt +0 -0
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
548
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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.
|
|
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.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
elif isinstance(value,
|
|
753
|
-
self.
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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(
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
|
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
|