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 +193 -411
- nodebpy/nodes/__init__.py +352 -335
- nodebpy/nodes/attribute.py +362 -307
- nodebpy/nodes/color.py +30 -34
- nodebpy/nodes/converter.py +1987 -2978
- nodebpy/nodes/experimental.py +201 -203
- nodebpy/nodes/geometry.py +4189 -3644
- nodebpy/nodes/grid.py +932 -447
- nodebpy/nodes/group.py +7 -10
- nodebpy/nodes/input.py +1496 -1308
- nodebpy/nodes/interface.py +236 -117
- nodebpy/nodes/manual.py +2051 -0
- nodebpy/nodes/output.py +85 -0
- nodebpy/nodes/texture.py +867 -7
- nodebpy/nodes/vector.py +528 -0
- nodebpy/nodes/zone.py +88 -119
- nodebpy/{nodes/types.py → types.py} +15 -2
- {nodebpy-0.2.0.dist-info → nodebpy-0.3.0.dist-info}/METADATA +5 -5
- nodebpy-0.3.0.dist-info/RECORD +26 -0
- nodebpy/nodes/mesh.py +0 -17
- nodebpy-0.2.0.dist-info/RECORD +0 -25
- {nodebpy-0.2.0.dist-info → nodebpy-0.3.0.dist-info}/WHEEL +0 -0
- {nodebpy-0.2.0.dist-info → nodebpy-0.3.0.dist-info}/entry_points.txt +0 -0
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
|
|
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 .
|
|
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:
|
|
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
|
-
|
|
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__.
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
539
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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.
|
|
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.
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
elif isinstance(value,
|
|
747
|
-
self.
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
|
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
|