nodebpy 520.0.1__tar.gz → 520.1.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-520.0.1 → nodebpy-520.1.0}/PKG-INFO +3 -1
- {nodebpy-520.0.1 → nodebpy-520.1.0}/pyproject.toml +5 -1
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/__init__.py +3 -5
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/builder/__init__.py +3 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/builder/_utils.py +25 -2
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/builder/accessor.py +20 -8
- nodebpy-520.1.0/src/nodebpy/builder/items.py +247 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/builder/mixins.py +10 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/builder/node.py +172 -79
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/builder/socket.py +557 -152
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/builder/tree.py +104 -19
- nodebpy-520.1.0/src/nodebpy/export/__init__.py +7 -0
- nodebpy-520.1.0/src/nodebpy/export/codegen.py +4347 -0
- {nodebpy-520.0.1/src/nodebpy → nodebpy-520.1.0/src/nodebpy/export}/diagram.py +64 -44
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/lib/nodearrange/utils.py +1 -1
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/converter.py +32 -5
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/groups.py +70 -7
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/manual.py +272 -244
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/zone.py +283 -156
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/shader/shader.py +2 -2
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/types.py +9 -1
- {nodebpy-520.0.1 → nodebpy-520.1.0}/README.md +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/builder/_registry.py +0 -0
- {nodebpy-520.0.1/src/nodebpy → nodebpy-520.1.0/src/nodebpy/builder}/arrange.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/__init__.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/compositor/color.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/compositor/group.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/compositor/input.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/compositor/output.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/color.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/group.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/input.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/output.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/shader/color.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/shader/converter.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/shader/grid.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/shader/group.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/shader/input.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/shader/manual.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/shader/output.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/shader/script.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/shader/texture.py +0 -0
- {nodebpy-520.0.1 → nodebpy-520.1.0}/src/nodebpy/nodes/shader/vector.py +0 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: nodebpy
|
|
3
|
-
Version: 520.0
|
|
3
|
+
Version: 520.1.0
|
|
4
4
|
Summary: Build nodes trees in Blender more elegantly with code
|
|
5
5
|
Author: Brady Johnston
|
|
6
6
|
Author-email: Brady Johnston <brady.johnston@me.com>
|
|
7
7
|
License: GPL-3.0-or-later
|
|
8
8
|
Requires-Dist: bpy==5.2.* ; extra == 'bpy'
|
|
9
|
+
Requires-Dist: ruff>=0.14.11 ; extra == 'format'
|
|
9
10
|
Requires-Dist: networkx>=3.6.1 ; extra == 'networkx'
|
|
10
11
|
Requires-Python: >=3.13
|
|
11
12
|
Provides-Extra: bpy
|
|
13
|
+
Provides-Extra: format
|
|
12
14
|
Provides-Extra: networkx
|
|
13
15
|
Description-Content-Type: text/markdown
|
|
14
16
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nodebpy"
|
|
3
|
-
version = "520.0
|
|
3
|
+
version = "520.1.0"
|
|
4
4
|
description = "Build nodes trees in Blender more elegantly with code"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -19,6 +19,10 @@ networkx = [
|
|
|
19
19
|
bpy = [
|
|
20
20
|
"bpy==5.2.*",
|
|
21
21
|
]
|
|
22
|
+
# Used by `to_python(format=True)` to tidy generated source via `ruff format`.
|
|
23
|
+
format = [
|
|
24
|
+
"ruff>=0.14.11",
|
|
25
|
+
]
|
|
22
26
|
|
|
23
27
|
[build-system]
|
|
24
28
|
requires = ["uv_build>=0.8.15,<0.9.0"]
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
from . import
|
|
2
|
-
from .builder import
|
|
3
|
-
TreeBuilder,
|
|
4
|
-
)
|
|
1
|
+
from . import export, nodes
|
|
2
|
+
from .builder import TreeBuilder
|
|
5
3
|
from .nodes import compositor, geometry, shader
|
|
6
4
|
|
|
7
5
|
__all__ = [
|
|
@@ -9,6 +7,6 @@ __all__ = [
|
|
|
9
7
|
"compositor",
|
|
10
8
|
"geometry",
|
|
11
9
|
"shader",
|
|
12
|
-
"
|
|
10
|
+
"export",
|
|
13
11
|
"TreeBuilder",
|
|
14
12
|
]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from ._utils import SocketError, denormalize_name, normalize_name
|
|
2
2
|
from .accessor import SocketAccessor
|
|
3
|
+
from .items import Item, ItemsMixin
|
|
3
4
|
from .mixins import LinkingMixin, OperatorMixin
|
|
4
5
|
from .node import (
|
|
5
6
|
BaseNode,
|
|
@@ -75,6 +76,8 @@ __all__ = [
|
|
|
75
76
|
"OperatorMixin",
|
|
76
77
|
"LinkingMixin",
|
|
77
78
|
"DynamicInputsMixin",
|
|
79
|
+
"Item",
|
|
80
|
+
"ItemsMixin",
|
|
78
81
|
# Node groups
|
|
79
82
|
"NodeGroupBuilder",
|
|
80
83
|
"CustomCompositorGroup",
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any, Protocol, runtime_checkable
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
4
4
|
|
|
5
5
|
import bpy
|
|
6
6
|
from bpy.types import NodeSocket
|
|
7
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..types import SOCKET_TYPES
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
class SocketError(Exception):
|
|
10
13
|
"""Raised when a socket operation fails."""
|
|
@@ -49,6 +52,12 @@ def _allow_innactive_sockets(node: bpy.types.Node) -> bool:
|
|
|
49
52
|
"ShaderNodeMixShader",
|
|
50
53
|
# "ShaderNodeMix",
|
|
51
54
|
"GeometryNodeSwitch",
|
|
55
|
+
# Group inputs that are only used behind an internal switch poll as
|
|
56
|
+
# inactive until the group is evaluated — including the input we are
|
|
57
|
+
# about to link, which would itself activate the socket.
|
|
58
|
+
"GeometryNodeGroup",
|
|
59
|
+
"ShaderNodeGroup",
|
|
60
|
+
"CompositorNodeGroup",
|
|
52
61
|
)
|
|
53
62
|
|
|
54
63
|
|
|
@@ -62,7 +71,7 @@ def _resolve_promotion(
|
|
|
62
71
|
|
|
63
72
|
Returns (dominant_socket, effective_other, effective_reverse).
|
|
64
73
|
"""
|
|
65
|
-
other_type =
|
|
74
|
+
other_type = _output_socket_type(other)
|
|
66
75
|
self_prec = _TYPE_PRECEDENCE.get(self_socket.type, 1)
|
|
67
76
|
other_prec = _TYPE_PRECEDENCE.get(other_type, -1) if other_type is not None else -1
|
|
68
77
|
|
|
@@ -89,3 +98,17 @@ class _SocketLike(Protocol):
|
|
|
89
98
|
"""Protocol for objects that wrap a single Blender NodeSocket and expose ``.socket``."""
|
|
90
99
|
|
|
91
100
|
socket: NodeSocket
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _output_socket_type(value: Any) -> "SOCKET_TYPES | None":
|
|
104
|
+
"""The Blender socket ``type`` of *value*'s default output socket.
|
|
105
|
+
|
|
106
|
+
Resolves a raw ``NodeSocket`` or any socket/node wrapper to the type string
|
|
107
|
+
Blender uses (e.g. ``"VECTOR"``, ``"VALUE"``). Returns ``None`` for plain
|
|
108
|
+
Python values (ints, floats, tuples) that carry no socket type.
|
|
109
|
+
"""
|
|
110
|
+
if isinstance(value, NodeSocket):
|
|
111
|
+
return value.type # type: ignore[return-value]
|
|
112
|
+
if isinstance(value, (_SocketLike, _NodeLike)):
|
|
113
|
+
return value._default_output_socket.type # type: ignore[return-value]
|
|
114
|
+
return None
|
|
@@ -48,6 +48,7 @@ class SocketAccessor:
|
|
|
48
48
|
if isinstance(key, int):
|
|
49
49
|
return key
|
|
50
50
|
ids = [s.identifier for s in self._collection]
|
|
51
|
+
names = [s.name for s in self._collection]
|
|
51
52
|
denorm = denormalize_name(key)
|
|
52
53
|
for candidate in (key, denorm):
|
|
53
54
|
if candidate in ids:
|
|
@@ -56,16 +57,27 @@ class SocketAccessor:
|
|
|
56
57
|
normalized_ids = [normalize_name(id) for id in ids]
|
|
57
58
|
if key in normalized_ids:
|
|
58
59
|
return normalized_ids.index(key)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if names.count(key) > 1:
|
|
60
|
+
for candidate in (key, denorm):
|
|
61
|
+
if candidate in names:
|
|
62
|
+
if names.count(candidate) > 1:
|
|
63
63
|
raise RuntimeError(
|
|
64
|
-
f"{self._direction.title()} name '{
|
|
65
|
-
f"{self._node.bl_idname} (appears {names.count(
|
|
66
|
-
f"Use the socket identifier instead."
|
|
64
|
+
f"{self._direction.title()} name '{candidate}' is ambiguous "
|
|
65
|
+
f"on {self._node.bl_idname} (appears {names.count(candidate)} "
|
|
66
|
+
f"times). Use the socket identifier instead."
|
|
67
67
|
)
|
|
68
|
-
return names.index(
|
|
68
|
+
return names.index(candidate)
|
|
69
|
+
# Normalized name match: 'flip_and_cyclic' matches name 'Flip and Cyclic'
|
|
70
|
+
# (denormalize_name can't recover the original capitalisation of small
|
|
71
|
+
# connector words like "and"/"to").
|
|
72
|
+
normalized_names = [normalize_name(n) for n in names]
|
|
73
|
+
if key in normalized_names:
|
|
74
|
+
if normalized_names.count(key) > 1:
|
|
75
|
+
raise RuntimeError(
|
|
76
|
+
f"{self._direction.title()} name '{key}' is ambiguous on "
|
|
77
|
+
f"{self._node.bl_idname} (appears {normalized_names.count(key)} "
|
|
78
|
+
f"times). Use the socket identifier instead."
|
|
79
|
+
)
|
|
80
|
+
return normalized_names.index(key)
|
|
69
81
|
raise RuntimeError(
|
|
70
82
|
f"{self._direction.title()} '{key}' not found on "
|
|
71
83
|
f"{self._node.bl_idname}. Available sockets (id: name): {list(zip(ids, names))}"
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Mapping, cast
|
|
4
|
+
|
|
5
|
+
from bpy.types import Node, NodeSocket
|
|
6
|
+
from mathutils import Euler
|
|
7
|
+
|
|
8
|
+
from ..types import _is_default_value
|
|
9
|
+
from ._registry import _wrap_socket
|
|
10
|
+
from ._utils import _SocketLike
|
|
11
|
+
from .node import DynamicInputsMixin
|
|
12
|
+
from .socket import Socket
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ..types import InputLinkable
|
|
16
|
+
from .tree import TreeBuilder
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _infer_value_type(value: Any) -> str | None:
|
|
20
|
+
"""Item ``socket_type`` for a plain default value, or None."""
|
|
21
|
+
match value:
|
|
22
|
+
case bool():
|
|
23
|
+
return "BOOLEAN"
|
|
24
|
+
case int():
|
|
25
|
+
return "INT"
|
|
26
|
+
case float():
|
|
27
|
+
return "FLOAT"
|
|
28
|
+
case str():
|
|
29
|
+
return "STRING"
|
|
30
|
+
case tuple() | list():
|
|
31
|
+
return "VECTOR"
|
|
32
|
+
case Euler():
|
|
33
|
+
return "ROTATION"
|
|
34
|
+
case _:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Item:
|
|
39
|
+
"""Handle for one item of an items-driven node.
|
|
40
|
+
|
|
41
|
+
Names the item's socket *roles* rather than socket plumbing: ``input``
|
|
42
|
+
is the node's input socket for the item, ``output`` the matching
|
|
43
|
+
output socket.
|
|
44
|
+
|
|
45
|
+
Holds the item's collection index rather than the bpy item itself —
|
|
46
|
+
bpy collection item references are invalidated when the collection
|
|
47
|
+
grows.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, owner: ItemsMixin, item: Any):
|
|
51
|
+
self._owner = owner
|
|
52
|
+
self._index = next(
|
|
53
|
+
i for i, candidate in enumerate(self._collection) if candidate == item
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def _collection(self):
|
|
58
|
+
return self._owner._items
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def _item(self):
|
|
62
|
+
return self._collection[self._index]
|
|
63
|
+
|
|
64
|
+
def __repr__(self) -> str:
|
|
65
|
+
return f"{type(self).__name__}({self.name!r}, {self.socket_type!r})"
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def name(self) -> str:
|
|
69
|
+
return self._item.name
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def socket_type(self) -> str:
|
|
73
|
+
# some collections (capture_items, grid_items) call this data_type
|
|
74
|
+
item = self._item
|
|
75
|
+
return getattr(item, "socket_type", None) or item.data_type
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def input(self) -> Socket:
|
|
79
|
+
"""The node's input socket for this item."""
|
|
80
|
+
return _wrap_socket(self._owner._item_socket(self._item))
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def output(self) -> Socket:
|
|
84
|
+
"""The node's output socket for this item."""
|
|
85
|
+
return _wrap_socket(self._owner._item_socket(self._item, output=True))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ItemsMixin(DynamicInputsMixin):
|
|
89
|
+
"""Socket machinery for nodes whose sockets are driven by a bpy item
|
|
90
|
+
collection (``capture_items``, ``bake_items``, ``format_items``, ...).
|
|
91
|
+
|
|
92
|
+
Subclasses declare class attributes instead of overriding methods:
|
|
93
|
+
|
|
94
|
+
- ``_items_collection``: name of the collection on ``_items_node``
|
|
95
|
+
- ``_socket_data_types``: socket types considered when inferring an
|
|
96
|
+
item's type from a source socket
|
|
97
|
+
- ``_type_map``: socket type -> item ``socket_type`` renames
|
|
98
|
+
(e.g. ``VALUE`` -> ``FLOAT``)
|
|
99
|
+
|
|
100
|
+
Must come *before* ``BaseNode`` in the bases so that
|
|
101
|
+
``_find_best_socket_pair`` (the ``>>``-implicit-add behaviour) takes
|
|
102
|
+
precedence over ``LinkingMixin``'s.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
_items_collection: str
|
|
106
|
+
|
|
107
|
+
if TYPE_CHECKING:
|
|
108
|
+
node: Node
|
|
109
|
+
tree: TreeBuilder
|
|
110
|
+
|
|
111
|
+
def _establish_links(self, **kwargs: Any) -> None: ...
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def _items_node(self) -> Node:
|
|
115
|
+
"""Node owning the items collection.
|
|
116
|
+
|
|
117
|
+
Zone input nodes override this to return ``paired_output``, where
|
|
118
|
+
the shared collection lives.
|
|
119
|
+
"""
|
|
120
|
+
return self.node
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def _items(self):
|
|
124
|
+
return getattr(self._items_node, self._items_collection)
|
|
125
|
+
|
|
126
|
+
def _new_item(self, name: str, type: str):
|
|
127
|
+
"""Create a new collection item.
|
|
128
|
+
|
|
129
|
+
Override to adapt collections whose ``.new()`` signature differs
|
|
130
|
+
from ``(socket_type, name)``.
|
|
131
|
+
"""
|
|
132
|
+
return self._items.new(socket_type=type, name=name)
|
|
133
|
+
|
|
134
|
+
def _item_socket(self, item, *, output: bool = False) -> NodeSocket:
|
|
135
|
+
"""The node socket belonging to ``item``."""
|
|
136
|
+
sockets = self.node.outputs if output else self.node.inputs
|
|
137
|
+
matches = [s for s in sockets if s.name == item.name]
|
|
138
|
+
if len(matches) == 1:
|
|
139
|
+
return matches[0]
|
|
140
|
+
# Name collides — e.g. a capture item named "Selection" alongside the
|
|
141
|
+
# built-in CaptureAttribute "Selection" socket. Item sockets are the
|
|
142
|
+
# trailing N sockets (one per collection item), so resolve by the
|
|
143
|
+
# item's position in the collection instead.
|
|
144
|
+
items = list(self._items)
|
|
145
|
+
idx = next(i for i, it in enumerate(items) if it == item)
|
|
146
|
+
real = [s for s in sockets if not s.identifier.startswith("__extend__")]
|
|
147
|
+
return real[len(real) - len(items) + idx]
|
|
148
|
+
|
|
149
|
+
def _add_socket(self, name: str, type: str) -> NodeSocket:
|
|
150
|
+
return self._item_socket(self._new_item(name, type))
|
|
151
|
+
|
|
152
|
+
def _resolve_capture(
|
|
153
|
+
self,
|
|
154
|
+
value: InputLinkable,
|
|
155
|
+
*,
|
|
156
|
+
name: str | None,
|
|
157
|
+
types: tuple[str, ...] | None = None,
|
|
158
|
+
) -> tuple[NodeSocket, str, str]:
|
|
159
|
+
"""Resolve the source socket, item type and item name for a capture."""
|
|
160
|
+
accessor = getattr(value, "o", None)
|
|
161
|
+
sources = (
|
|
162
|
+
[cast("NodeSocket", value)] if accessor is None else accessor._available
|
|
163
|
+
)
|
|
164
|
+
socket_source, type = self._match_compatible_data(sources, types)
|
|
165
|
+
if type in self._type_map:
|
|
166
|
+
type = self._type_map[type]
|
|
167
|
+
if name is None:
|
|
168
|
+
default_socket = getattr(value, "_default_output_socket", None)
|
|
169
|
+
name = socket_source.name if default_socket is None else default_socket.name
|
|
170
|
+
if isinstance(socket_source, _SocketLike):
|
|
171
|
+
socket_source = socket_source.socket
|
|
172
|
+
return socket_source, type, name
|
|
173
|
+
|
|
174
|
+
def _declared_item_type(self, value: Any) -> str | None:
|
|
175
|
+
"""The item ``socket_type`` if ``value`` is a socket-type string
|
|
176
|
+
(e.g. ``"FLOAT"``) valid for this node, else ``None``."""
|
|
177
|
+
if not isinstance(value, str):
|
|
178
|
+
return None
|
|
179
|
+
if value in self._socket_data_types:
|
|
180
|
+
return self._type_map.get(value, value)
|
|
181
|
+
if value in {self._type_map.get(t, t) for t in self._socket_data_types}:
|
|
182
|
+
return value
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
def _add_unlinked_input(self, name: str, value: Any) -> bool:
|
|
186
|
+
"""Items may also be declared with a plain default value
|
|
187
|
+
(``items={"label": "hello"}``) — the item type is inferred from
|
|
188
|
+
the Python type and the value becomes the socket default."""
|
|
189
|
+
if super()._add_unlinked_input(name, value):
|
|
190
|
+
return True
|
|
191
|
+
type = _infer_value_type(value)
|
|
192
|
+
if type is None:
|
|
193
|
+
return False
|
|
194
|
+
socket = self._add_socket(
|
|
195
|
+
name=name, type=self._declared_item_type(type) or type
|
|
196
|
+
)
|
|
197
|
+
socket.default_value = value # ty: ignore[unresolved-attribute]
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
def capture(self, value: InputLinkable, *, name: str | None = None) -> Socket:
|
|
201
|
+
"""Add an item linked from ``value`` and return its output socket.
|
|
202
|
+
|
|
203
|
+
The item is auto-named after the source socket unless ``name`` is
|
|
204
|
+
given.
|
|
205
|
+
"""
|
|
206
|
+
source, type, name = self._resolve_capture(value, name=name)
|
|
207
|
+
item = self._new_item(name, type)
|
|
208
|
+
self.tree.link(source, self._item_socket(item))
|
|
209
|
+
return _wrap_socket(self._item_socket(item, output=True))
|
|
210
|
+
|
|
211
|
+
def add_item(
|
|
212
|
+
self, name: str, value: Any = None, *, type: str | None = None
|
|
213
|
+
) -> Item:
|
|
214
|
+
"""Add a single item and return its handle.
|
|
215
|
+
|
|
216
|
+
``value`` may be a linkable (linked to the item's input) or a plain
|
|
217
|
+
default value; otherwise ``type`` (a socket-type string such as
|
|
218
|
+
``"FLOAT"``) declares the item unlinked.
|
|
219
|
+
"""
|
|
220
|
+
if value is not None and not _is_default_value(value):
|
|
221
|
+
source, inferred, _ = self._resolve_capture(value, name=name)
|
|
222
|
+
item = self._new_item(name, type or inferred)
|
|
223
|
+
self.tree.link(source, self._item_socket(item))
|
|
224
|
+
return Item(self, item)
|
|
225
|
+
if type is None:
|
|
226
|
+
type = _infer_value_type(value)
|
|
227
|
+
if type is None:
|
|
228
|
+
raise TypeError(f"item {name!r} requires a value or an explicit type=")
|
|
229
|
+
item = self._new_item(name, self._declared_item_type(type) or type)
|
|
230
|
+
if value is not None:
|
|
231
|
+
self._item_socket(item).default_value = value # ty: ignore[unresolved-attribute]
|
|
232
|
+
return Item(self, item)
|
|
233
|
+
|
|
234
|
+
def add_items(self, items: Mapping[str, InputLinkable | str]) -> dict[str, Item]:
|
|
235
|
+
"""Add an item per mapping entry and return their handles by name.
|
|
236
|
+
|
|
237
|
+
Values may be linkables (linked to the new item's input) or
|
|
238
|
+
socket-type strings such as ``"FLOAT"`` (declare an unlinked item).
|
|
239
|
+
"""
|
|
240
|
+
handles = {}
|
|
241
|
+
for key, value in items.items():
|
|
242
|
+
type = self._declared_item_type(value)
|
|
243
|
+
if type is not None:
|
|
244
|
+
handles[key] = self.add_item(key, type=type)
|
|
245
|
+
else:
|
|
246
|
+
handles[key] = self.add_item(key, cast("InputLinkable", value))
|
|
247
|
+
return handles
|
|
@@ -330,6 +330,16 @@ class LinkingMixin:
|
|
|
330
330
|
if possible_combos:
|
|
331
331
|
return sorted(possible_combos, key=lambda x: x[0])[0][1]
|
|
332
332
|
|
|
333
|
+
# A node with a virtual ``__extend__`` input (Viewer, …) accepts any
|
|
334
|
+
# source: linking to it makes Blender create a typed socket. Fall back
|
|
335
|
+
# to it when nothing else matched, pairing with the source's first
|
|
336
|
+
# available output.
|
|
337
|
+
extend = next(
|
|
338
|
+
(i for i in inputs if i.identifier.startswith("__extend__")), None
|
|
339
|
+
)
|
|
340
|
+
if extend is not None and outputs:
|
|
341
|
+
return extend, outputs[0]
|
|
342
|
+
|
|
333
343
|
src_name = getattr(getattr(source, "node", None), "name", repr(source))
|
|
334
344
|
tgt_name = getattr(getattr(target, "node", None), "name", repr(target))
|
|
335
345
|
raise SocketError(
|