nodebpy 0.2.1__tar.gz → 0.3.0__tar.gz
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-0.2.1 → nodebpy-0.3.0}/PKG-INFO +4 -4
- {nodebpy-0.2.1 → nodebpy-0.3.0}/README.md +3 -3
- {nodebpy-0.2.1 → nodebpy-0.3.0}/pyproject.toml +1 -1
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/builder.py +175 -399
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/manual.py +40 -11
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/zone.py +81 -112
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/types.py +1 -1
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/__init__.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/arrange.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/__init__.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/attribute.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/color.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/converter.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/experimental.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/geometry.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/grid.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/group.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/input.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/interface.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/output.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/texture.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/nodes/vector.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/screenshot.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/screenshot_subprocess.py +0 -0
- {nodebpy-0.2.1 → nodebpy-0.3.0}/src/nodebpy/sockets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: nodebpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Build nodes in Blender with code
|
|
5
5
|
Author: Brady Johnston
|
|
6
6
|
Author-email: Brady Johnston <brady.johnston@me.com>
|
|
@@ -94,8 +94,8 @@ tree
|
|
|
94
94
|
graph LR
|
|
95
95
|
N0("NodeGroupInput"):::default-node
|
|
96
96
|
N1("RandomValue<br/><small>(-1,-1,-1) seed:2</small>"):::converter-node
|
|
97
|
-
N2("RandomValue<br/><small>(-1,-1,-1)
|
|
98
|
-
N3("AlignRotationToVector
|
|
97
|
+
N2("RandomValue<br/><small>(-1,-1,-1)</small>"):::converter-node
|
|
98
|
+
N3("AlignRotationToVector"):::converter-node
|
|
99
99
|
N4("AxisAngleToRotation<br/><small>(0,0,1)</small>"):::converter-node
|
|
100
100
|
N5("InputPosition"):::input-node
|
|
101
101
|
N6("Points"):::geometry-node
|
|
@@ -109,7 +109,7 @@ graph LR
|
|
|
109
109
|
N14("RealizeInstances"):::geometry-node
|
|
110
110
|
N15("InstanceOnPoints"):::geometry-node
|
|
111
111
|
N16("NodeGroupOutput"):::default-node
|
|
112
|
-
N1 -->|"Value>>
|
|
112
|
+
N1 -->|"Value>>Vector"| N3
|
|
113
113
|
N4 -->|"Rotation>>Rotate By"| N8
|
|
114
114
|
N3 -->|"Rotation>>Rotation"| N8
|
|
115
115
|
N2 -->|"Value>>Position"| N6
|
|
@@ -70,8 +70,8 @@ tree
|
|
|
70
70
|
graph LR
|
|
71
71
|
N0("NodeGroupInput"):::default-node
|
|
72
72
|
N1("RandomValue<br/><small>(-1,-1,-1) seed:2</small>"):::converter-node
|
|
73
|
-
N2("RandomValue<br/><small>(-1,-1,-1)
|
|
74
|
-
N3("AlignRotationToVector
|
|
73
|
+
N2("RandomValue<br/><small>(-1,-1,-1)</small>"):::converter-node
|
|
74
|
+
N3("AlignRotationToVector"):::converter-node
|
|
75
75
|
N4("AxisAngleToRotation<br/><small>(0,0,1)</small>"):::converter-node
|
|
76
76
|
N5("InputPosition"):::input-node
|
|
77
77
|
N6("Points"):::geometry-node
|
|
@@ -85,7 +85,7 @@ graph LR
|
|
|
85
85
|
N14("RealizeInstances"):::geometry-node
|
|
86
86
|
N15("InstanceOnPoints"):::geometry-node
|
|
87
87
|
N16("NodeGroupOutput"):::default-node
|
|
88
|
-
N1 -->|"Value>>
|
|
88
|
+
N1 -->|"Value>>Vector"| N3
|
|
89
89
|
N4 -->|"Rotation>>Rotate By"| N8
|
|
90
90
|
N3 -->|"Rotation>>Rotation"| N8
|
|
91
91
|
N2 -->|"Value>>Position"| N6
|
|
@@ -1,7 +1,11 @@
|
|
|
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
10
|
from .nodes import Math, VectorMath
|
|
7
11
|
|
|
@@ -21,6 +25,7 @@ from .types import (
|
|
|
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
|
|
@@ -190,6 +199,14 @@ class TreeBuilder:
|
|
|
190
199
|
if isinstance(socket2, SocketLinker):
|
|
191
200
|
socket2 = socket2.socket
|
|
192
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
|
+
|
|
193
210
|
link = self.tree.links.new(socket1, socket2, handle_dynamic_sockets=True)
|
|
194
211
|
|
|
195
212
|
if any(socket.is_inactive for socket in [socket1, socket2]):
|
|
@@ -224,7 +241,6 @@ class NodeBuilder:
|
|
|
224
241
|
_from_socket: NodeSocket | None = None
|
|
225
242
|
_default_input_id: str | None = None
|
|
226
243
|
_default_output_id: str | None = None
|
|
227
|
-
_socket_data_types = tuple(SOCKET_COMPATIBILITY.keys())
|
|
228
244
|
|
|
229
245
|
def __init__(self):
|
|
230
246
|
# Get active tree from context manager
|
|
@@ -276,7 +292,6 @@ class NodeBuilder:
|
|
|
276
292
|
counter = 0
|
|
277
293
|
socket = self.node.outputs[counter]
|
|
278
294
|
while not socket.is_icon_visible:
|
|
279
|
-
print(f"skipping inactive socket {socket.name}")
|
|
280
295
|
counter += 1
|
|
281
296
|
socket = self.node.outputs[counter]
|
|
282
297
|
return socket
|
|
@@ -303,286 +318,67 @@ class NodeBuilder:
|
|
|
303
318
|
else:
|
|
304
319
|
raise TypeError(f"Unsupported type: {type(node)}")
|
|
305
320
|
|
|
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]
|
|
321
|
+
@property
|
|
322
|
+
def _available_outputs(self) -> list[NodeSocket]:
|
|
323
|
+
return [socket for socket in self.node.outputs if socket.is_icon_visible]
|
|
353
324
|
|
|
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
|
|
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)
|
|
408
334
|
]
|
|
409
|
-
raise RuntimeError(
|
|
410
|
-
f"Cannot link any output from {self.node.name} to any input of {target_node.node.name}. "
|
|
411
|
-
f"Available output types: {available_outputs}, "
|
|
412
|
-
f"Available input types: {available_inputs}"
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
def _socket_type_from_linkable(self, linkable: LINKABLE):
|
|
416
|
-
assert linkable, "Linkable cannot be None"
|
|
417
|
-
# If it's a NodeBuilder, try to find a compatible output socket
|
|
418
|
-
if hasattr(linkable, "node") and hasattr(linkable, "_default_output_socket"):
|
|
419
|
-
compatible_socket = self._find_compatible_output_socket(linkable)
|
|
420
|
-
socket_type = compatible_socket.type
|
|
421
|
-
for comp in SOCKET_COMPATIBILITY[socket_type]:
|
|
422
|
-
if comp in self._socket_data_types:
|
|
423
|
-
return comp if comp != "VALUE" else "FLOAT"
|
|
424
|
-
|
|
425
|
-
# Fallback to original logic for other types
|
|
426
|
-
for comp in SOCKET_COMPATIBILITY[linkable.type]:
|
|
427
|
-
if comp in self._socket_data_types:
|
|
428
|
-
return comp if comp != "VALUE" else "FLOAT"
|
|
429
|
-
raise ValueError(
|
|
430
|
-
f"Unsupported socket type for linking: {linkable}, type: {linkable.type=}"
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
def _add_inputs(self, *args, **kwargs) -> dict[str, LINKABLE]:
|
|
434
|
-
"""Dictionary with {new_socket.name: from_linkable} for link creation"""
|
|
435
|
-
new_sockets = {}
|
|
436
|
-
items = {}
|
|
437
|
-
for arg in args:
|
|
438
|
-
if isinstance(arg, bpy.types.NodeSocket):
|
|
439
|
-
name = arg.name
|
|
440
|
-
items[name] = arg
|
|
441
|
-
else:
|
|
442
|
-
items[arg._default_output_socket.name] = arg
|
|
443
|
-
items.update(kwargs)
|
|
444
|
-
for key, value in items.items():
|
|
445
|
-
# For NodeBuilder objects, find the best compatible output socket
|
|
446
|
-
if hasattr(value, "node") and hasattr(value, "_default_output_socket"):
|
|
447
|
-
compatible_socket = self._find_compatible_output_socket(value)
|
|
448
|
-
# Create a SocketLinker to represent the specific socket we want to link from
|
|
449
|
-
# from . import SocketLinker
|
|
450
|
-
socket_linker = SocketLinker(compatible_socket)
|
|
451
|
-
type = self._socket_type_from_linkable(value)
|
|
452
|
-
socket = self._add_socket(name=key, type=type)
|
|
453
|
-
new_sockets[socket.name] = socket_linker
|
|
454
|
-
else:
|
|
455
|
-
type = self._socket_type_from_linkable(value)
|
|
456
|
-
socket = self._add_socket(name=key, type=type)
|
|
457
|
-
new_sockets[socket.name] = value
|
|
458
|
-
|
|
459
|
-
return new_sockets
|
|
460
335
|
|
|
461
|
-
def
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
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]
|
|
479
344
|
|
|
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
|
|
345
|
+
raise SocketError("No compatible output sockets found")
|
|
488
346
|
|
|
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
|
-
return None
|
|
514
|
-
else:
|
|
515
|
-
# Target type is directly supported
|
|
516
|
-
self._add_socket(name=target_type.title(), type=target_type)
|
|
517
|
-
else:
|
|
518
|
-
# No type restrictions, try to create the target type
|
|
519
|
-
self._add_socket(name=target_type.title(), type=target_type)
|
|
520
|
-
|
|
521
|
-
# Find the newly created output socket
|
|
522
|
-
if hasattr(self, "outputs"):
|
|
523
|
-
for name, socket_linker in self.outputs.items():
|
|
524
|
-
socket_compatibles = SOCKET_COMPATIBILITY.get(
|
|
525
|
-
socket_linker.socket.type, []
|
|
526
|
-
)
|
|
527
|
-
if target_type in socket_compatibles:
|
|
528
|
-
return socket_linker.socket
|
|
529
|
-
|
|
530
|
-
# Fallback: try to get the socket directly from the node
|
|
531
|
-
if hasattr(self.node, "outputs"):
|
|
532
|
-
for output_socket in self.node.outputs:
|
|
533
|
-
socket_compatibles = SOCKET_COMPATIBILITY.get(
|
|
534
|
-
output_socket.type, []
|
|
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))
|
|
535
371
|
)
|
|
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
372
|
|
|
547
|
-
|
|
548
|
-
|
|
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]
|
|
549
376
|
|
|
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
|
|
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]}"
|
|
577
381
|
)
|
|
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
382
|
|
|
587
383
|
def _input_idx(self, identifier: str) -> int:
|
|
588
384
|
# currently there is a Blender bug that is preventing the lookup of sockets from identifiers on some
|
|
@@ -621,28 +417,15 @@ class NodeBuilder:
|
|
|
621
417
|
def _link(
|
|
622
418
|
self, source: LINKABLE | SocketLinker | NodeSocket, target: LINKABLE
|
|
623
419
|
) -> bpy.types.NodeLink:
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
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)
|
|
628
423
|
|
|
629
424
|
def _link_from(
|
|
630
425
|
self,
|
|
631
426
|
source: LINKABLE,
|
|
632
427
|
input: LINKABLE | str,
|
|
633
428
|
):
|
|
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
429
|
if isinstance(input, str):
|
|
647
430
|
try:
|
|
648
431
|
self._link(source, self.node.inputs[input])
|
|
@@ -651,39 +434,6 @@ class NodeBuilder:
|
|
|
651
434
|
else:
|
|
652
435
|
self._link(source, input)
|
|
653
436
|
|
|
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
437
|
def _set_input_default_value(self, input, value):
|
|
688
438
|
"""Set the default value for an input socket, handling type conversions."""
|
|
689
439
|
if (
|
|
@@ -695,62 +445,28 @@ class NodeBuilder:
|
|
|
695
445
|
else:
|
|
696
446
|
input.default_value = value
|
|
697
447
|
|
|
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
448
|
def _establish_links(self, **kwargs: TYPE_INPUT_ALL):
|
|
735
449
|
input_ids = [input.identifier for input in self.node.inputs]
|
|
736
450
|
for name, value in kwargs.items():
|
|
737
451
|
if value is None:
|
|
738
452
|
continue
|
|
453
|
+
if isinstance(value, Node):
|
|
454
|
+
node = NodeBuilder()
|
|
455
|
+
node.node = value
|
|
456
|
+
value = node
|
|
739
457
|
|
|
740
458
|
if value is ...:
|
|
741
459
|
# Ellipsis indicates this input should receive links from >> operator
|
|
742
460
|
# which can potentially target multiple inputs on the new node
|
|
743
461
|
if self._from_socket is not None:
|
|
744
|
-
self.
|
|
745
|
-
self._from_socket, self.node.inputs[self._input_idx(name)]
|
|
746
|
-
)
|
|
462
|
+
self._link_from(self._from_socket, name)
|
|
747
463
|
|
|
748
464
|
elif isinstance(value, SocketLinker):
|
|
749
|
-
self.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
elif isinstance(value,
|
|
753
|
-
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)
|
|
754
470
|
else:
|
|
755
471
|
if name in input_ids:
|
|
756
472
|
input = self.node.inputs[input_ids.index(name)]
|
|
@@ -772,31 +488,16 @@ class NodeBuilder:
|
|
|
772
488
|
Returns the right-hand node to enable continued chaining.
|
|
773
489
|
"""
|
|
774
490
|
if isinstance(other, SocketLinker):
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
other._from_socket = socket_out
|
|
491
|
+
source = self._default_output_socket
|
|
492
|
+
target = other.socket
|
|
493
|
+
other._from_socket = source
|
|
779
494
|
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
|
|
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)
|
|
798
499
|
|
|
799
|
-
self.tree.link(
|
|
500
|
+
self.tree.link(source, target)
|
|
800
501
|
return other
|
|
801
502
|
|
|
802
503
|
def _get_input_socket_by_name(self, node: "NodeBuilder", name: str) -> NodeSocket:
|
|
@@ -819,14 +520,14 @@ class NodeBuilder:
|
|
|
819
520
|
else (other, self._default_output_socket)
|
|
820
521
|
)
|
|
821
522
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
523
|
+
component_is_vector = False
|
|
524
|
+
for value in values:
|
|
525
|
+
if getattr(value, "type", None) == "VECTOR":
|
|
526
|
+
component_is_vector = True
|
|
527
|
+
break
|
|
827
528
|
|
|
828
529
|
# Use VectorMath if either operand is a vector
|
|
829
|
-
if
|
|
530
|
+
if component_is_vector:
|
|
830
531
|
if operation == "multiply":
|
|
831
532
|
# Handle special cases for vector multiplication where we might scale instead
|
|
832
533
|
# of using the multiply method
|
|
@@ -838,7 +539,7 @@ class NodeBuilder:
|
|
|
838
539
|
return VectorMath.multiply(*values)
|
|
839
540
|
else:
|
|
840
541
|
raise TypeError(
|
|
841
|
-
f"Unsupported type for {operation} with VECTOR socket: {type(other)}"
|
|
542
|
+
f"Unsupported type for {operation} with VECTOR socket: {type(other)}, {other=}"
|
|
842
543
|
)
|
|
843
544
|
else:
|
|
844
545
|
vector_method = getattr(VectorMath, operation)
|
|
@@ -858,11 +559,10 @@ class NodeBuilder:
|
|
|
858
559
|
raise TypeError(
|
|
859
560
|
f"Unsupported type for {operation} with VECTOR operand: {type(other)}"
|
|
860
561
|
)
|
|
861
|
-
else:
|
|
862
|
-
# Both operands are scalar types, use regular Math
|
|
562
|
+
else: # Both operands are scalar types, use regular Math
|
|
863
563
|
from .nodes.converter import IntegerMath, Math
|
|
864
564
|
|
|
865
|
-
if isinstance(other, int):
|
|
565
|
+
if isinstance(other, int) and self._default_output_socket.type == "INT":
|
|
866
566
|
return getattr(IntegerMath, operation)(*values)
|
|
867
567
|
else:
|
|
868
568
|
return getattr(Math, operation)(*values)
|
|
@@ -892,6 +592,78 @@ class NodeBuilder:
|
|
|
892
592
|
return self._apply_math_operation(other, "subtract", reverse=True)
|
|
893
593
|
|
|
894
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
|
+
|
|
895
667
|
class SocketLinker(NodeBuilder):
|
|
896
668
|
def __init__(self, socket: NodeSocket):
|
|
897
669
|
assert socket.node is not None
|
|
@@ -900,6 +672,10 @@ class SocketLinker(NodeBuilder):
|
|
|
900
672
|
self._default_output_id = socket.identifier
|
|
901
673
|
self._tree = TreeBuilder(socket.node.id_data) # type: ignore
|
|
902
674
|
|
|
675
|
+
@property
|
|
676
|
+
def _available_outputs(self) -> list[NodeSocket]:
|
|
677
|
+
return [self.socket]
|
|
678
|
+
|
|
903
679
|
@property
|
|
904
680
|
def type(self) -> SOCKET_TYPES:
|
|
905
681
|
return self.socket.type # type: ignore
|
|
@@ -2,7 +2,13 @@ from typing import Any, Literal
|
|
|
2
2
|
|
|
3
3
|
import bpy
|
|
4
4
|
|
|
5
|
-
from ..builder import
|
|
5
|
+
from ..builder import (
|
|
6
|
+
DynamicInputsMixin,
|
|
7
|
+
NodeBuilder,
|
|
8
|
+
NodeSocket,
|
|
9
|
+
SocketError,
|
|
10
|
+
SocketLinker,
|
|
11
|
+
)
|
|
6
12
|
from ..types import (
|
|
7
13
|
LINKABLE,
|
|
8
14
|
SOCKET_TYPES,
|
|
@@ -77,7 +83,7 @@ __all__ = (
|
|
|
77
83
|
)
|
|
78
84
|
|
|
79
85
|
|
|
80
|
-
class Bake(NodeBuilder):
|
|
86
|
+
class Bake(NodeBuilder, DynamicInputsMixin):
|
|
81
87
|
"""Cache the incoming data so that it can be used without recomputation
|
|
82
88
|
|
|
83
89
|
TODO: properly handle Animation / Still bake opations and ability to bake to a file
|
|
@@ -171,12 +177,15 @@ class Value(NodeBuilder):
|
|
|
171
177
|
return self._output("Value")
|
|
172
178
|
|
|
173
179
|
|
|
174
|
-
class FormatString(NodeBuilder):
|
|
180
|
+
class FormatString(NodeBuilder, DynamicInputsMixin):
|
|
175
181
|
"""Insert values into a string using a Python and path template compatible formatting syntax"""
|
|
176
182
|
|
|
177
183
|
_bl_idname = "FunctionNodeFormatString"
|
|
178
184
|
node: bpy.types.FunctionNodeFormatString
|
|
179
185
|
_socket_data_types = ("VALUE", "INT", "STRING")
|
|
186
|
+
_type_map = {
|
|
187
|
+
"VALUE": "FLOAT",
|
|
188
|
+
}
|
|
180
189
|
|
|
181
190
|
def __init__(
|
|
182
191
|
self,
|
|
@@ -396,7 +405,10 @@ class JoinGeometry(NodeBuilder):
|
|
|
396
405
|
def __init__(self, *args: LINKABLE):
|
|
397
406
|
super().__init__()
|
|
398
407
|
for source in reversed(args):
|
|
399
|
-
|
|
408
|
+
try:
|
|
409
|
+
self._link(*self._find_best_socket_pair(source, self))
|
|
410
|
+
except SocketError:
|
|
411
|
+
self._link(*source._find_best_socket_pair(source, self))
|
|
400
412
|
|
|
401
413
|
@property
|
|
402
414
|
def i_geometry(self) -> SocketLinker:
|
|
@@ -769,11 +781,27 @@ def _domain_capture_attribute(domain: _AttributeDomains):
|
|
|
769
781
|
return method
|
|
770
782
|
|
|
771
783
|
|
|
772
|
-
class CaptureAttribute(NodeBuilder):
|
|
784
|
+
class CaptureAttribute(NodeBuilder, DynamicInputsMixin):
|
|
773
785
|
"""Store the result of a field on a geometry and output the data as a node socket. Allows remembering or interpolating data as the geometry changes, such as positions before deformation"""
|
|
774
786
|
|
|
775
787
|
_bl_idname = "GeometryNodeCaptureAttribute"
|
|
776
788
|
node: bpy.types.GeometryNodeCaptureAttribute
|
|
789
|
+
_socket_data_types = (
|
|
790
|
+
"VALUE",
|
|
791
|
+
"INT",
|
|
792
|
+
"BOOLEAN",
|
|
793
|
+
"VECTOR",
|
|
794
|
+
"RGBA",
|
|
795
|
+
"ROTATION",
|
|
796
|
+
"MATRIX",
|
|
797
|
+
)
|
|
798
|
+
_type_map = {
|
|
799
|
+
"VALUE": "FLOAT",
|
|
800
|
+
# "VECTOR": "FLOAT_VECTOR",
|
|
801
|
+
"RGBA": "FLOAT_COLOR",
|
|
802
|
+
"ROTATION": "QUATERNION",
|
|
803
|
+
"MATRIX": "FLOAT4X4",
|
|
804
|
+
}
|
|
777
805
|
point = _domain_capture_attribute("POINT")
|
|
778
806
|
edge = _domain_capture_attribute("EDGE")
|
|
779
807
|
face = _domain_capture_attribute("FACE")
|
|
@@ -851,7 +879,7 @@ class CaptureAttribute(NodeBuilder):
|
|
|
851
879
|
self.node.domain = value
|
|
852
880
|
|
|
853
881
|
|
|
854
|
-
class FieldToGrid(NodeBuilder):
|
|
882
|
+
class FieldToGrid(DynamicInputsMixin, NodeBuilder):
|
|
855
883
|
"""Create new grids by evaluating new values on an existing volume grid topology
|
|
856
884
|
|
|
857
885
|
New socket items for field evaluation are first created from *args then **kwargs to give specific names to the items.
|
|
@@ -873,7 +901,8 @@ class FieldToGrid(NodeBuilder):
|
|
|
873
901
|
|
|
874
902
|
_bl_idname = "GeometryNodeFieldToGrid"
|
|
875
903
|
node: bpy.types.GeometryNodeFieldToGrid
|
|
876
|
-
_socket_data_types = ("
|
|
904
|
+
_socket_data_types = ("VALUE", "INT", "VECTOR", "BOOLEAN")
|
|
905
|
+
_type_map = {"VALUE": "FLOAT"}
|
|
877
906
|
_default_input_id = "Topology"
|
|
878
907
|
|
|
879
908
|
def __init__(
|
|
@@ -988,7 +1017,7 @@ class SDFGridBoolean(NodeBuilder):
|
|
|
988
1017
|
for arg in args:
|
|
989
1018
|
if arg is None:
|
|
990
1019
|
continue
|
|
991
|
-
node._link_from(arg, "Grid 2")
|
|
1020
|
+
node._link_from(*node._find_best_socket_pair(arg, node._input("Grid 2")))
|
|
992
1021
|
return node
|
|
993
1022
|
|
|
994
1023
|
@classmethod
|
|
@@ -1000,7 +1029,7 @@ class SDFGridBoolean(NodeBuilder):
|
|
|
1000
1029
|
for arg in args:
|
|
1001
1030
|
if arg is None:
|
|
1002
1031
|
continue
|
|
1003
|
-
node._link_from(arg, "Grid 2")
|
|
1032
|
+
node._link_from(*node._find_best_socket_pair(arg, node._input("Grid 2")))
|
|
1004
1033
|
return node
|
|
1005
1034
|
|
|
1006
1035
|
@classmethod
|
|
@@ -1011,11 +1040,11 @@ class SDFGridBoolean(NodeBuilder):
|
|
|
1011
1040
|
) -> "SDFGridBoolean":
|
|
1012
1041
|
"""Create SDF Grid Boolean with operation 'Difference'."""
|
|
1013
1042
|
node = cls(operation="DIFFERENCE")
|
|
1014
|
-
node._link_from(grid_1, "Grid 1")
|
|
1043
|
+
node._link_from(*node._find_best_socket_pair(grid_1, node._input("Grid 1")))
|
|
1015
1044
|
for arg in args:
|
|
1016
1045
|
if arg is None:
|
|
1017
1046
|
continue
|
|
1018
|
-
node._link_from(arg, "Grid 2")
|
|
1047
|
+
node._link_from(*node._find_best_socket_pair(arg, node._input("Grid 2")))
|
|
1019
1048
|
return node
|
|
1020
1049
|
|
|
1021
1050
|
@property
|
|
@@ -2,9 +2,8 @@ from abc import ABC, abstractmethod
|
|
|
2
2
|
from typing import Literal
|
|
3
3
|
|
|
4
4
|
import bpy
|
|
5
|
-
from bpy.types import NodeSocket
|
|
6
5
|
|
|
7
|
-
from nodebpy.builder import NodeBuilder, SocketLinker
|
|
6
|
+
from nodebpy.builder import DynamicInputsMixin, NodeBuilder, SocketLinker
|
|
8
7
|
|
|
9
8
|
from ..types import (
|
|
10
9
|
LINKABLE,
|
|
@@ -16,7 +15,7 @@ from ..types import (
|
|
|
16
15
|
)
|
|
17
16
|
|
|
18
17
|
|
|
19
|
-
class BaseZone(NodeBuilder, ABC):
|
|
18
|
+
class BaseZone(DynamicInputsMixin, NodeBuilder, ABC):
|
|
20
19
|
_items_attribute: Literal["state_items", "repeat_items"]
|
|
21
20
|
|
|
22
21
|
@property
|
|
@@ -81,30 +80,6 @@ class BaseZoneInput(BaseZone, NodeBuilder, ABC):
|
|
|
81
80
|
item = self.items.new(type, name)
|
|
82
81
|
return self.inputs[item.name]
|
|
83
82
|
|
|
84
|
-
def __rshift__(self, other):
|
|
85
|
-
"""Custom zone input linking that creates sockets as needed"""
|
|
86
|
-
# Check if target is a zone output without inputs
|
|
87
|
-
if (
|
|
88
|
-
hasattr(other, "_default_input_socket")
|
|
89
|
-
and other._default_input_socket is None
|
|
90
|
-
):
|
|
91
|
-
# Target zone needs a socket - create one based on our output
|
|
92
|
-
from ..builder import SOCKET_COMPATIBILITY
|
|
93
|
-
|
|
94
|
-
source_socket = self._default_output_socket
|
|
95
|
-
source_type = source_socket.type
|
|
96
|
-
|
|
97
|
-
compatible_types = SOCKET_COMPATIBILITY.get(source_type, [source_type])
|
|
98
|
-
best_type = compatible_types[0] if compatible_types else source_type
|
|
99
|
-
|
|
100
|
-
# Create socket on target zone
|
|
101
|
-
target_socket = other._add_socket(name=best_type.title(), type=best_type)
|
|
102
|
-
self.tree.link(source_socket, target_socket)
|
|
103
|
-
return other
|
|
104
|
-
else:
|
|
105
|
-
# Use the general smart linking approach
|
|
106
|
-
return self._smart_link_to(other)
|
|
107
|
-
|
|
108
83
|
|
|
109
84
|
class BaseZoneOutput(BaseZone, NodeBuilder, ABC):
|
|
110
85
|
"""Base class for zone output nodes"""
|
|
@@ -122,69 +97,22 @@ class BaseZoneOutput(BaseZone, NodeBuilder, ABC):
|
|
|
122
97
|
item = self.items.new(type, name)
|
|
123
98
|
return self.node.inputs[item.name]
|
|
124
99
|
|
|
125
|
-
def __rshift__(self, other):
|
|
126
|
-
"""Custom zone output linking that creates sockets as needed"""
|
|
127
|
-
from ..builder import SOCKET_COMPATIBILITY
|
|
128
|
-
|
|
129
|
-
# Get the source socket type
|
|
130
|
-
source_socket = self._default_output_socket
|
|
131
|
-
source_type = source_socket.type
|
|
132
|
-
|
|
133
|
-
# Check if target has compatible inputs
|
|
134
|
-
if hasattr(other, "_default_input_socket") and other._default_input_socket:
|
|
135
|
-
# Normal linking
|
|
136
|
-
return super().__rshift__(other)
|
|
137
|
-
elif hasattr(other, "_add_socket"):
|
|
138
|
-
# Target is also a zone - create compatible socket
|
|
139
|
-
compatible_types = SOCKET_COMPATIBILITY.get(source_type, [source_type])
|
|
140
|
-
best_type = compatible_types[0] if compatible_types else source_type
|
|
141
|
-
|
|
142
|
-
# Create socket on target zone
|
|
143
|
-
target_socket = other._add_socket(name=best_type.title(), type=best_type)
|
|
144
|
-
self.tree.link(source_socket, target_socket)
|
|
145
|
-
return other
|
|
146
|
-
else:
|
|
147
|
-
# Normal NodeBuilder
|
|
148
|
-
return super().__rshift__(other)
|
|
149
|
-
|
|
150
|
-
@property
|
|
151
|
-
def _default_input_socket(self) -> NodeSocket:
|
|
152
|
-
"""Get default input socket, avoiding skip-type sockets"""
|
|
153
|
-
inputs = list(self.inputs.values())
|
|
154
|
-
if inputs:
|
|
155
|
-
return inputs[0].socket
|
|
156
|
-
else:
|
|
157
|
-
# No socket exists - this should be handled by zone-specific __rshift__ logic
|
|
158
|
-
# Return None to signal that a socket needs to be created
|
|
159
|
-
return None
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
class SimulationZone:
|
|
163
|
-
def __init__(self, *args: LINKABLE, **kwargs: LINKABLE):
|
|
164
|
-
self.input = SimulationInput()
|
|
165
|
-
self.output = SimulationOutput()
|
|
166
|
-
self.input.node.pair_with_output(self.output.node)
|
|
167
|
-
|
|
168
|
-
self.output.node.state_items.clear()
|
|
169
|
-
socket_lookup = self.output._add_inputs(*args, **kwargs)
|
|
170
|
-
for name, source in socket_lookup.items():
|
|
171
|
-
self.input._link_from(source, name)
|
|
172
|
-
|
|
173
|
-
def delta_time(self) -> SocketLinker:
|
|
174
|
-
return self.input.o_delta_time
|
|
175
|
-
|
|
176
|
-
def __getitem__(self, index: int):
|
|
177
|
-
match index:
|
|
178
|
-
case 0:
|
|
179
|
-
return self.input
|
|
180
|
-
case 1:
|
|
181
|
-
return self.output
|
|
182
|
-
case _:
|
|
183
|
-
raise IndexError("SimulationZone has only two items")
|
|
184
|
-
|
|
185
100
|
|
|
186
101
|
class BaseSimulationZone(BaseZone):
|
|
187
102
|
_items_attribute = "state_items"
|
|
103
|
+
_socket_data_types = (
|
|
104
|
+
"VALUE",
|
|
105
|
+
"INT",
|
|
106
|
+
"BOOLEAN",
|
|
107
|
+
"VECTOR",
|
|
108
|
+
"RGBA",
|
|
109
|
+
"ROTATION",
|
|
110
|
+
"MATRIX",
|
|
111
|
+
"STRING",
|
|
112
|
+
"GEOMETRY",
|
|
113
|
+
"BUNDLE",
|
|
114
|
+
)
|
|
115
|
+
_type_map = {"VALUE": "FLOAT"}
|
|
188
116
|
|
|
189
117
|
@property
|
|
190
118
|
def items(self) -> bpy.types.NodeGeometrySimulationOutputItems:
|
|
@@ -215,39 +143,49 @@ class SimulationOutput(BaseSimulationZone, BaseZoneOutput):
|
|
|
215
143
|
return self._input("Skip")
|
|
216
144
|
|
|
217
145
|
|
|
218
|
-
class
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
self, iterations: TYPE_INPUT_INT = 1, *args: LINKABLE, **kwargs: LINKABLE
|
|
223
|
-
):
|
|
224
|
-
self.input = RepeatInput(iterations)
|
|
225
|
-
self.output = RepeatOutput()
|
|
146
|
+
class SimulationZone:
|
|
147
|
+
def __init__(self, *args: LINKABLE, **kwargs: LINKABLE):
|
|
148
|
+
self.input = SimulationInput()
|
|
149
|
+
self.output = SimulationOutput()
|
|
226
150
|
self.input.node.pair_with_output(self.output.node)
|
|
227
151
|
|
|
228
|
-
self.output.node.
|
|
229
|
-
self.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def i(self) -> SocketLinker:
|
|
233
|
-
"""Input socket: Skip simluation frame"""
|
|
234
|
-
return self.input.o_iteration
|
|
152
|
+
self.output.node.state_items.clear()
|
|
153
|
+
socket_lookup = self.output._add_inputs(*args, **kwargs)
|
|
154
|
+
for name, source in socket_lookup.items():
|
|
155
|
+
self.input._link_from(source, name)
|
|
235
156
|
|
|
236
|
-
def
|
|
237
|
-
|
|
238
|
-
self._index = 0
|
|
239
|
-
return self
|
|
157
|
+
def delta_time(self) -> SocketLinker:
|
|
158
|
+
return self.input.o_delta_time
|
|
240
159
|
|
|
241
|
-
def
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
160
|
+
def __getitem__(self, index: int):
|
|
161
|
+
match index:
|
|
162
|
+
case 0:
|
|
163
|
+
return self.input
|
|
164
|
+
case 1:
|
|
165
|
+
return self.output
|
|
166
|
+
case _:
|
|
167
|
+
raise IndexError("SimulationZone has only two items")
|
|
247
168
|
|
|
248
169
|
|
|
249
170
|
class BaseRepeatZone(BaseZone):
|
|
250
171
|
_items_attribute = "repeat_items"
|
|
172
|
+
_socket_data_types = (
|
|
173
|
+
"FLOAT",
|
|
174
|
+
"INT",
|
|
175
|
+
"BOOLEAN",
|
|
176
|
+
"VECTOR",
|
|
177
|
+
"RGBA",
|
|
178
|
+
"ROTATION",
|
|
179
|
+
"MATRIX",
|
|
180
|
+
"STRING",
|
|
181
|
+
"OBJECT",
|
|
182
|
+
"IMAGE",
|
|
183
|
+
"GEOMETRY",
|
|
184
|
+
"COLLECTION",
|
|
185
|
+
"MATERIAL",
|
|
186
|
+
"BUNDLE",
|
|
187
|
+
"CLOSURE",
|
|
188
|
+
)
|
|
251
189
|
|
|
252
190
|
@property
|
|
253
191
|
def items(self) -> bpy.types.NodeGeometryRepeatOutputItems:
|
|
@@ -284,6 +222,37 @@ class RepeatOutput(BaseRepeatZone, BaseZoneOutput):
|
|
|
284
222
|
node: bpy.types.GeometryNodeRepeatOutput
|
|
285
223
|
|
|
286
224
|
|
|
225
|
+
class RepeatZone:
|
|
226
|
+
"""Wrapper that supports both direct unpacking and iteration"""
|
|
227
|
+
|
|
228
|
+
def __init__(
|
|
229
|
+
self, iterations: TYPE_INPUT_INT = 1, *args: LINKABLE, **kwargs: LINKABLE
|
|
230
|
+
):
|
|
231
|
+
self.input = RepeatInput(iterations)
|
|
232
|
+
self.output = RepeatOutput()
|
|
233
|
+
self.input.node.pair_with_output(self.output.node)
|
|
234
|
+
|
|
235
|
+
self.output.node.repeat_items.clear()
|
|
236
|
+
self.input._establish_links(**self.input._add_inputs(*args, **kwargs))
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def i(self) -> SocketLinker:
|
|
240
|
+
"""Input socket: Skip simluation frame"""
|
|
241
|
+
return self.input.o_iteration
|
|
242
|
+
|
|
243
|
+
def __iter__(self):
|
|
244
|
+
"""Support for loop: for i, input, output in RepeatZone(...)"""
|
|
245
|
+
self._index = 0
|
|
246
|
+
return self
|
|
247
|
+
|
|
248
|
+
def __next__(self):
|
|
249
|
+
"""Support for iteration: next(RepeatZone)"""
|
|
250
|
+
if self._index > 0:
|
|
251
|
+
raise StopIteration
|
|
252
|
+
self._index += 1
|
|
253
|
+
return self.i, self.input, self.output
|
|
254
|
+
|
|
255
|
+
|
|
287
256
|
class ForEachGeometryElementInput(NodeBuilder):
|
|
288
257
|
"""For Each Geometry Element Input node"""
|
|
289
258
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|