nodebpy 0.11.1__tar.gz → 0.13.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.
Files changed (72) hide show
  1. {nodebpy-0.11.1 → nodebpy-0.13.0}/PKG-INFO +1 -1
  2. {nodebpy-0.11.1 → nodebpy-0.13.0}/pyproject.toml +1 -1
  3. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/__init__.py +4 -3
  4. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/builder/__init__.py +14 -1
  5. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/builder/_utils.py +15 -6
  6. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/builder/accessor.py +1 -1
  7. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/builder/mixins.py +10 -2
  8. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/builder/node.py +153 -31
  9. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/builder/socket.py +62 -26
  10. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/builder/tree.py +45 -26
  11. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/__init__.py +29 -13
  12. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/converter.py +1 -1
  13. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/input.py +197 -0
  14. nodebpy-0.13.0/src/nodebpy/nodes/compositor/manual.py +336 -0
  15. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/__init__.py +16 -2
  16. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/geometry.py +0 -576
  17. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/groups.py +6 -8
  18. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/manual.py +749 -1
  19. nodebpy-0.13.0/src/nodebpy/nodes/geometry/utilities.py +69 -0
  20. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/zone.py +137 -5
  21. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/__init__.py +18 -14
  22. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/converter.py +1 -75
  23. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/manual.py +2 -1
  24. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/types.py +3 -0
  25. nodebpy-0.11.1/src/nodebpy/nodes/compositor/manual.py +0 -30
  26. nodebpy-0.11.1/src/nodebpy/nodes/shader/interface.py +0 -100
  27. {nodebpy-0.11.1 → nodebpy-0.13.0}/README.md +0 -0
  28. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/arrange.py +0 -0
  29. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/builder/_registry.py +0 -0
  30. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/builder/interface.py +0 -0
  31. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/diagram.py +0 -0
  32. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  33. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
  34. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  35. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  36. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  37. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  38. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  39. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  40. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  41. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  42. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  43. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  44. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/__init__.py +0 -0
  45. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/color.py +0 -0
  46. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
  47. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
  48. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/group.py +0 -0
  49. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
  50. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
  51. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/output.py +0 -0
  52. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
  53. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
  54. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/color.py +0 -0
  55. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
  56. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
  57. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/group.py +0 -0
  58. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/input.py +0 -0
  59. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
  60. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/output.py +0 -0
  61. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
  62. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
  63. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/color.py +0 -0
  64. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/grid.py +0 -0
  65. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/group.py +0 -0
  66. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/input.py +0 -0
  67. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/output.py +0 -0
  68. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/script.py +0 -0
  69. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/shader.py +0 -0
  70. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/texture.py +0 -0
  71. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/vector.py +0 -0
  72. {nodebpy-0.11.1 → nodebpy-0.13.0}/src/nodebpy/sockets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 0.11.1
3
+ Version: 0.13.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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nodebpy"
3
- version = "0.11.1"
3
+ version = "0.13.0"
4
4
  description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,5 +1,7 @@
1
- from . import nodes, diagram, sockets
2
- from .builder import TreeBuilder, NodeGroupBuilder
1
+ from . import diagram, nodes, sockets
2
+ from .builder import (
3
+ TreeBuilder,
4
+ )
3
5
  from .nodes import compositor, geometry, shader
4
6
 
5
7
  __all__ = [
@@ -10,5 +12,4 @@ __all__ = [
10
12
  "sockets",
11
13
  "diagram",
12
14
  "TreeBuilder",
13
- "NodeGroupBuilder",
14
15
  ]
@@ -27,7 +27,14 @@ from .interface import (
27
27
  SocketVector,
28
28
  )
29
29
  from .mixins import LinkingMixin, OperatorMixin
30
- from .node import BaseNode, DynamicInputsMixin, NodeGroupBuilder
30
+ from .node import (
31
+ BaseNode,
32
+ CustomCompositorGroup,
33
+ CustomGeometryGroup,
34
+ CustomShaderGroup,
35
+ DynamicInputsMixin,
36
+ NodeGroupBuilder,
37
+ )
31
38
  from .socket import (
32
39
  BooleanSocket,
33
40
  BundleSocket,
@@ -82,6 +89,12 @@ __all__ = [
82
89
  "DynamicInputsMixin",
83
90
  # Node groups
84
91
  "NodeGroupBuilder",
92
+ "CustomCompositorGroup",
93
+ "CustomGeometryGroup",
94
+ "CustomShaderGroup",
95
+ "GeometryNodeGroup",
96
+ "ShaderNodeGroup",
97
+ "CompositorNodeGroup",
85
98
  # Type-specific socket classes (runtime)
86
99
  "FloatSocket",
87
100
  "VectorSocket",
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
3
+ from typing import Any, Protocol, runtime_checkable
4
4
 
5
5
  import bpy
6
6
  from bpy.types import NodeSocket
@@ -67,15 +67,24 @@ def _resolve_promotion(
67
67
 
68
68
  if other_prec > self_prec:
69
69
  # Other side is dominant — swap so the linker wraps the vector/higher socket
70
- other_socket = other._default_output_socket
70
+ if isinstance(other, NodeSocket):
71
+ other_socket = other
72
+ else:
73
+ other_socket = other._default_output_socket
71
74
  return other_socket, self_socket, not reverse
72
75
 
73
76
  return self_socket, other, reverse
74
77
 
75
78
 
76
- class _NodeLike:
77
- """Marker base for objects that wrap a Blender node (have .node, .inputs, .outputs)."""
79
+ @runtime_checkable
80
+ class _NodeLike(Protocol):
81
+ """Protocol for objects that wrap a Blender node and expose an ``outputs`` accessor."""
78
82
 
83
+ outputs: Any # SocketAccessor at runtime; typed as Any to avoid circular import
79
84
 
80
- class _SocketLike:
81
- """Marker base for objects that wrap a single Blender NodeSocket (have .socket)."""
85
+
86
+ @runtime_checkable
87
+ class _SocketLike(Protocol):
88
+ """Protocol for objects that wrap a single Blender NodeSocket and expose ``.socket``."""
89
+
90
+ socket: NodeSocket
@@ -28,7 +28,7 @@ class SocketAccessor:
28
28
 
29
29
  def __init__(
30
30
  self,
31
- collection: bpy.types.NodeInputs | bpy.types.NodeOutputs,
31
+ collection: bpy.types.NodeInputs | bpy.types.NodeOutputs | list[NodeSocket],
32
32
  direction: Literal["input", "output"],
33
33
  ):
34
34
  self._direction = direction
@@ -100,9 +100,12 @@ class OperatorMixin:
100
100
  )
101
101
 
102
102
  def _apply_compare_operation(self, other: Any, operation: str) -> "Math":
103
- return _get_socket_linker(self._default_output_socket)._dispatch_compare( # type: ignore[attr-defined]
104
- other, operation
103
+ socket, other, _ = _resolve_promotion(
104
+ self._default_output_socket, # type: ignore[attr-defined]
105
+ other,
106
+ False,
105
107
  )
108
+ return _get_socket_linker(socket)._dispatch_compare(other, operation)
106
109
 
107
110
  def __lt__(self, other: Any) -> "Compare":
108
111
  return self._apply_compare_operation(other, "less_than")
@@ -236,6 +239,11 @@ class LinkingMixin:
236
239
  else:
237
240
  inputs = [target]
238
241
 
242
+ # NodeReroute adapts its type to whatever is linked — skip type matching
243
+ if getattr(getattr(target, "node", None), "bl_idname", None) == "NodeReroute":
244
+ if outputs and inputs:
245
+ return inputs[0], outputs[0]
246
+
239
247
  for output in outputs:
240
248
  compat_sockets = SOCKET_COMPATIBILITY.get(output.type, ())
241
249
  for input in inputs:
@@ -1,19 +1,47 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from typing import TYPE_CHECKING, Iterable, Literal, Self
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ Generic,
8
+ Iterable,
9
+ Literal,
10
+ Protocol,
11
+ Self,
12
+ TypeVar,
13
+ cast,
14
+ )
5
15
 
6
16
  import bpy
7
- from bpy.types import Node, NodeSocket
8
-
9
- from ..types import SOCKET_COMPATIBILITY, SOCKET_TYPES, InputAny, InputLinkable
17
+ from bpy.types import (
18
+ CompositorNodeGroup,
19
+ CompositorNodeTree,
20
+ GeometryNodeGroup,
21
+ GeometryNodeTree,
22
+ Node,
23
+ NodeSocket,
24
+ ShaderNodeGroup,
25
+ ShaderNodeTree,
26
+ )
27
+
28
+ from ..types import SOCKET_COMPATIBILITY, SOCKET_TYPES, InputAny
10
29
  from ._utils import SocketError, _NodeLike, _SocketLike
11
30
  from .accessor import SocketAccessor
12
31
  from .mixins import LinkingMixin, OperatorMixin
13
32
  from .tree import TreeBuilder
14
33
 
34
+ _T = TypeVar("_T", bound=bpy.types.NodeTree)
35
+
15
36
  if TYPE_CHECKING:
16
- pass
37
+
38
+ class _DynamicTarget(Protocol):
39
+ """Structural type for a node that supports dynamic socket addition."""
40
+
41
+ def _add_inputs(self, *args: Any, **kwargs: Any) -> dict[str, NodeSocket]: ...
42
+
43
+ @property
44
+ def inputs(self) -> SocketAccessor: ...
17
45
 
18
46
 
19
47
  class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
@@ -25,7 +53,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
25
53
  _default_output_id: str | None = None
26
54
  _placeholder_inputs: list[str]
27
55
 
28
- def __init__(self, node: bpy.types.Node | None = None):
56
+ def __init__(self, node: Node | None = None):
29
57
  tree = (
30
58
  TreeBuilder._tree_contexts[-1] if len(TreeBuilder._tree_contexts) else None
31
59
  )
@@ -72,7 +100,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
72
100
  return socket
73
101
 
74
102
  @classmethod
75
- def _from_node(cls, node: bpy.types.Node) -> Self:
103
+ def _from_node(cls, node: Node) -> Self:
76
104
  builder = cls()
77
105
  builder.tree.nodes.remove(builder.node)
78
106
  builder.node = node
@@ -86,7 +114,9 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
86
114
  assert link.to_node
87
115
  if link.to_node.bl_idname == cls._bl_idname:
88
116
  return cls._from_node(link.to_node)
89
- return cls(socket)
117
+ node = cls()
118
+ node.tree.link(socket, node.inputs._best_match(socket.type))
119
+ return node
90
120
  else:
91
121
  if socket.links:
92
122
  for link in socket.links:
@@ -98,8 +128,9 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
98
128
  node >> socket
99
129
  return node
100
130
 
101
- def _set_input_default_value(self, input, value):
131
+ def _set_input_default_value(self, input: NodeSocket, value: Any) -> None:
102
132
  """Set the default value for an input socket, handling type conversions."""
133
+ assert hasattr(input, "default_value")
103
134
  if (
104
135
  hasattr(input, "type")
105
136
  and input.type == "VECTOR"
@@ -115,7 +146,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
115
146
  if value is None or (
116
147
  "GridPrune" in self._bl_idname
117
148
  and name == "Threshold"
118
- and self.node.data_type == "BOOLEAN"
149
+ and getattr(self.node, "data_type", None) == "BOOLEAN"
119
150
  ):
120
151
  continue
121
152
  if isinstance(value, Node):
@@ -128,7 +159,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
128
159
  continue
129
160
 
130
161
  elif isinstance(value, _SocketLike):
131
- self._link_from(value, name)
162
+ self._link_from(value.socket, name)
132
163
  elif isinstance(value, NodeSocket):
133
164
  self._link_from(value, name)
134
165
  elif isinstance(value, _NodeLike):
@@ -165,7 +196,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
165
196
  return SocketAccessor(self.node.inputs, "input")
166
197
 
167
198
 
168
- class DynamicInputsMixin:
199
+ class DynamicInputsMixin(ABC):
169
200
  _socket_data_types: tuple[str, ...]
170
201
  _type_map: dict[str, str] = {}
171
202
 
@@ -192,10 +223,14 @@ class DynamicInputsMixin:
192
223
  try:
193
224
  return super()._find_best_socket_pair(source, target) # type: ignore
194
225
  except SocketError:
195
- target_name, source_socket = list(target._add_inputs(source).items())[0]
196
- return (source_socket, target.inputs[target_name].socket)
226
+ dyn = cast("_DynamicTarget", target)
227
+ target_name, source_socket = list(dyn._add_inputs(source).items())[0]
228
+ return (source_socket, dyn.inputs[target_name].socket)
229
+
230
+ @abstractmethod
231
+ def _add_socket(self, name: str, *args: Any, **kwargs: Any) -> NodeSocket: ...
197
232
 
198
- def _add_inputs(self, *args, **kwargs) -> dict[str, InputLinkable]:
233
+ def _add_inputs(self, *args, **kwargs) -> dict[str, NodeSocket]:
199
234
  """Dictionary with {new_socket.name: from_linkable} for link creation"""
200
235
  new_sockets = {}
201
236
  items = {}
@@ -212,14 +247,15 @@ class DynamicInputsMixin:
212
247
  return new_sockets
213
248
 
214
249
 
215
- class NodeGroupBuilder(BaseNode, ABC):
250
+ class NodeGroupBuilder(BaseNode, ABC, Generic[_T]):
216
251
  """Base class for custom node groups.
217
252
 
218
253
  Subclasses implement :meth:`_build_group` with the node-graph logic.
254
+ Subclass one of the editor-specific variants: :class:`GeometryNodeGroup`,
255
+ :class:`ShaderNodeGroup`, or :class:`CompositorNodeGroup`.
219
256
  """
220
257
 
221
258
  _name: str
222
- _bl_idname = "GeometryNodeGroup"
223
259
  _warning_propagation: Literal["ALL", "ERRORS_AND_WARNINGS", "ERRORS", "NONE"] = (
224
260
  "ALL"
225
261
  )
@@ -234,27 +270,113 @@ class NodeGroupBuilder(BaseNode, ABC):
234
270
  "TEXTURE",
235
271
  "VECTOR",
236
272
  ] = "NONE"
237
- node: bpy.types.GeometryNodeGroup
238
273
 
239
274
  def __init__(self, **kwargs):
240
275
  super().__init__()
241
- self.node.node_tree = self._get_or_create_group()
276
+ self._setup_node_group()
242
277
  self.node.show_options = False
243
- self.node.warning_propagation = self._warning_propagation
244
278
  self._establish_links(**kwargs)
245
279
 
246
- def _get_or_create_group(self) -> bpy.types.GeometryNodeTree:
247
- name = self._name
248
- if name in bpy.data.node_groups:
249
- return bpy.data.node_groups[name]
280
+ @property
281
+ @abstractmethod
282
+ def node_tree(self) -> _T:
283
+ """The internal node tree for this group node."""
284
+ ...
250
285
 
251
- with TreeBuilder(name) as tree:
252
- self._build_group(tree)
253
- tree.tree.color_tag = self._color_tag
286
+ @abstractmethod
287
+ def _setup_node_group(self) -> None:
288
+ """Set ``self.node.node_tree`` and any node-type-specific properties.
254
289
 
255
- return tree.tree
290
+ Called by ``__init__`` after the node is created but before links are
291
+ established. Concrete subclasses have a narrowed ``self.node`` type,
292
+ so the ``node_tree`` assignment is type-safe here rather than in the
293
+ base class where ``self.node`` is only ``bpy.types.Node``.
294
+ """
295
+ ...
256
296
 
257
- @classmethod
258
297
  @abstractmethod
259
- def _build_group(cls, tree: TreeBuilder) -> None:
260
- """Code that builds the node group internals and interface"""
298
+ def _build_group(self, tree: TreeBuilder) -> None:
299
+ """Build the node group internals and interface."""
300
+
301
+ def _get_or_create_tree(self) -> _T:
302
+ existing = bpy.data.node_groups[self._name]
303
+ if existing.bl_idname == self.tree.tree.bl_idname:
304
+ return cast(_T, existing)
305
+ raise TypeError(
306
+ f"Node group '{self._name}' already exists as "
307
+ f"{type(existing).__name__}, not {self._bl_idname}. "
308
+ f"Use a unique _name for this group."
309
+ )
310
+
311
+
312
+ class CustomGeometryGroup(NodeGroupBuilder[GeometryNodeTree]):
313
+ """Node group in a Geometry Nodes tree."""
314
+
315
+ _bl_idname = "GeometryNodeGroup"
316
+ node: GeometryNodeGroup
317
+
318
+ @property
319
+ def node_tree(self) -> GeometryNodeTree:
320
+ assert self.node.node_tree is not None
321
+ return self.node.node_tree
322
+
323
+ def _setup_node_group(self) -> None:
324
+ self.node.node_tree = self._get_or_create_group()
325
+ self.node.warning_propagation = self._warning_propagation
326
+
327
+ def _get_or_create_group(self) -> GeometryNodeTree:
328
+ try:
329
+ return self._get_or_create_tree()
330
+ except KeyError:
331
+ with TreeBuilder.geometry(self._name) as tree:
332
+ self._build_group(tree)
333
+ tree.tree.color_tag = self._color_tag
334
+ return tree.tree
335
+
336
+
337
+ class CustomShaderGroup(NodeGroupBuilder[ShaderNodeTree]):
338
+ """Node group in a Shader (Material) node tree."""
339
+
340
+ _bl_idname = "ShaderNodeGroup"
341
+ node: ShaderNodeGroup
342
+
343
+ @property
344
+ def node_tree(self) -> ShaderNodeTree:
345
+ assert self.node.node_tree is not None
346
+ return self.node.node_tree
347
+
348
+ def _setup_node_group(self) -> None:
349
+ self.node.node_tree = self._get_or_create_group()
350
+
351
+ def _get_or_create_group(self) -> ShaderNodeTree:
352
+ try:
353
+ return self._get_or_create_tree()
354
+ except KeyError:
355
+ with TreeBuilder.shader(self._name) as tree:
356
+ self._build_group(tree)
357
+ tree.tree.color_tag = self._color_tag
358
+ return tree.tree
359
+
360
+
361
+ class CustomCompositorGroup(NodeGroupBuilder[CompositorNodeTree]):
362
+ """Node group in a Compositor node tree."""
363
+
364
+ _bl_idname = "CompositorNodeGroup"
365
+ node: CompositorNodeGroup
366
+
367
+ @property
368
+ def node_tree(self) -> CompositorNodeTree:
369
+ assert self.node.node_tree is not None
370
+ return self.node.node_tree
371
+
372
+ def _setup_node_group(self) -> None:
373
+ self.node.node_tree = self._get_or_create_group()
374
+
375
+ def _get_or_create_group(self) -> CompositorNodeTree:
376
+ try:
377
+ return self._get_or_create_tree()
378
+ except KeyError:
379
+ with TreeBuilder.compositor(self._name) as tree:
380
+ self._build_group(tree)
381
+ tree.tree.color_tag = self._color_tag
382
+ return tree.tree
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Iterator, overload
3
+ from typing import TYPE_CHECKING, Any, Iterator, cast, overload
4
4
 
5
5
  import bpy
6
6
  from bpy.types import (
@@ -25,6 +25,7 @@ from bpy.types import (
25
25
  NodeSocketShader,
26
26
  NodeSocketString,
27
27
  NodeSocketVector,
28
+ NodeTree,
28
29
  )
29
30
  from mathutils import Euler
30
31
 
@@ -55,7 +56,7 @@ class Socket(_SocketLike, OperatorMixin, LinkingMixin):
55
56
  self.socket = socket
56
57
  self.node = socket.node
57
58
  self._default_output_id = socket.identifier
58
- self._tree = TreeBuilder(socket.node.id_data) # type: ignore
59
+ self._tree = TreeBuilder(cast(NodeTree, socket.node.id_data))
59
60
 
60
61
  @property
61
62
  def tree(self) -> TreeBuilder:
@@ -100,7 +101,7 @@ class Socket(_SocketLike, OperatorMixin, LinkingMixin):
100
101
 
101
102
  def _dispatch_math(
102
103
  self, other: Any, operation: str, reverse: bool = False
103
- ) -> "BaseNode":
104
+ ) -> "Math":
104
105
  """Scalar math dispatch (float). Uses the Math node."""
105
106
  from ..nodes.geometry.converter import Math
106
107
 
@@ -108,7 +109,7 @@ class Socket(_SocketLike, OperatorMixin, LinkingMixin):
108
109
  math_operation = "floored_modulo" if operation == "modulo" else operation
109
110
  return getattr(Math, math_operation)(*values)
110
111
 
111
- def _dispatch_unary(self, operation: str) -> "BaseNode":
112
+ def _dispatch_unary(self, operation: str) -> "Math":
112
113
  """Scalar unary dispatch (float). Uses the Math node."""
113
114
  from ..nodes.geometry.converter import Math
114
115
 
@@ -118,7 +119,7 @@ class Socket(_SocketLike, OperatorMixin, LinkingMixin):
118
119
  return Math.absolute(self.socket)
119
120
  raise ValueError(f"Unknown unary operation: {operation}")
120
121
 
121
- def _dispatch_floordiv(self, other: Any, reverse: bool = False) -> "BaseNode":
122
+ def _dispatch_floordiv(self, other: Any, reverse: bool = False) -> "Math":
122
123
  """Scalar floor division: divide then floor."""
123
124
  from ..nodes.geometry.converter import Math
124
125
 
@@ -126,7 +127,7 @@ class Socket(_SocketLike, OperatorMixin, LinkingMixin):
126
127
  divided = Math.divide(*values)
127
128
  return Math.floor(divided)
128
129
 
129
- def _dispatch_compare(self, other: Any, operation: str) -> "BaseNode":
130
+ def _dispatch_compare(self, other: Any, operation: str) -> "Compare | Math":
130
131
  """Scalar comparison dispatch."""
131
132
  if isinstance(self._tree.tree, GeometryNodeTree):
132
133
  from ..nodes.geometry.manual import Compare
@@ -140,9 +141,12 @@ class Socket(_SocketLike, OperatorMixin, LinkingMixin):
140
141
  "greater_than": ("greater_than", False),
141
142
  "less_equal": ("greater_than", True),
142
143
  "greater_equal": ("less_than", True),
144
+ "equal": ("compare", False),
143
145
  }
144
146
  math_op, negate = _MATH_COMPARE_MAP[operation]
145
147
  result = getattr(Math, math_op)(self.socket, other)
148
+ if operation == "equal":
149
+ result.i.value_002.default_value = 0.00001
146
150
  if negate:
147
151
  result = Math.subtract(1.0, result._default_output_socket)
148
152
  return result
@@ -167,8 +171,8 @@ class Socket(_SocketLike, OperatorMixin, LinkingMixin):
167
171
  def __gt__(self, other: Any) -> "Compare": ...
168
172
  def __le__(self, other: Any) -> "Compare": ...
169
173
  def __ge__(self, other: Any) -> "Compare": ...
170
- def __eq__(self, other: Any) -> "Compare": ... # type: ignore[override]
171
- def __ne__(self, other: Any) -> "Compare": ... # type: ignore[override]
174
+ def __eq__(self, other: Any) -> "Compare": ...
175
+ def __ne__(self, other: Any) -> "Compare": ...
172
176
 
173
177
 
174
178
  # ---------------------------------------------------------------------------
@@ -308,7 +312,7 @@ class _VectorMixin:
308
312
 
309
313
  return getattr(Compare.vector, operation)(self.socket, other)
310
314
  else:
311
- return Socket._dispatch_compare(self, other, operation) # type: ignore[arg-type]
315
+ return Socket._dispatch_compare(cast("Socket", self), other, operation)
312
316
 
313
317
  if TYPE_CHECKING:
314
318
 
@@ -324,12 +328,10 @@ class _VectorMixin:
324
328
  def __rfloordiv__(self, other: Any) -> "VectorMath": ...
325
329
  def __neg__(self) -> "VectorMath": ...
326
330
  def __abs__(self) -> "VectorMath": ...
327
- def __lt__(self, other: Any) -> "Compare": ...
328
- def __gt__(self, other: Any) -> "Compare": ...
329
- def __le__(self, other: Any) -> "Compare": ...
330
- def __ge__(self, other: Any) -> "Compare": ...
331
- def __eq__(self, other: Any) -> "Compare": ... # type: ignore[override]
332
- def __ne__(self, other: Any) -> "Compare": ... # type: ignore[override]
331
+ def __lt__(self, other: Any) -> "Compare[NodeSocketVector]": ...
332
+ def __gt__(self, other: Any) -> "Compare[NodeSocketVector]": ...
333
+ def __le__(self, other: Any) -> "Compare[NodeSocketVector]": ...
334
+ def __ge__(self, other: Any) -> "Compare[NodeSocketVector]": ...
333
335
 
334
336
 
335
337
  _SEPARATE_COLOR_IDNAMES = (
@@ -356,7 +358,9 @@ class _ColorMixin:
356
358
  return SeparateColor
357
359
 
358
360
  def _separated_channel(self, channel: str) -> Socket:
361
+ assert self.socket.links is not None
359
362
  for link in self.socket.links:
363
+ assert link.to_node is not None
360
364
  if link.to_node.bl_idname in _SEPARATE_COLOR_IDNAMES:
361
365
  return Socket(link.to_node.outputs[channel])
362
366
 
@@ -438,6 +442,40 @@ class _ColorMixin:
438
442
  def __len__(self) -> int:
439
443
  return 4
440
444
 
445
+ def _dispatch_math(
446
+ self, other: Any, operation: str, reverse: bool = False
447
+ ) -> "BaseNode":
448
+ from ..nodes.geometry import VectorMath
449
+
450
+ values = (self.socket, other) if not reverse else (other, self.socket)
451
+
452
+ if operation == "multiply":
453
+ if isinstance(other, (int, float)):
454
+ return VectorMath.scale(self.socket, other)
455
+ elif isinstance(other, NodeSocket) and other.type in (
456
+ "VALUE",
457
+ "FLOAT",
458
+ "INT",
459
+ ):
460
+ return VectorMath.scale(self.socket, other)
461
+ elif isinstance(other, (_SocketLike, _NodeLike)) and getattr(
462
+ other, "type", None
463
+ ) in ("VALUE", "FLOAT", "INT"):
464
+ return VectorMath.scale(self.socket, other._default_output_socket)
465
+ else:
466
+ return VectorMath.multiply(*values)
467
+ else:
468
+ vector_method = getattr(VectorMath, operation, None)
469
+ assert vector_method is not None
470
+ if isinstance(other, (int, float)):
471
+ scalar_vector = (other, other, other)
472
+ return (
473
+ vector_method(self.socket, scalar_vector)
474
+ if not reverse
475
+ else vector_method(scalar_vector, self.socket)
476
+ )
477
+ return vector_method(*values)
478
+
441
479
 
442
480
  class _IntegerMixin:
443
481
  """Integer-specific dispatch — uses IntegerMath in geometry trees."""
@@ -475,7 +513,7 @@ class _IntegerMixin:
475
513
 
476
514
  values = (self.socket, other) if not reverse else (other, self.socket)
477
515
  return getattr(IntegerMath, operation)(*values)
478
- return Socket._dispatch_math(self, other, operation, reverse) # type: ignore[arg-type]
516
+ return Socket._dispatch_math(cast("Socket", self), other, operation, reverse)
479
517
 
480
518
  def _dispatch_unary(self, operation: str) -> "BaseNode":
481
519
  if self._is_geometry_tree:
@@ -485,7 +523,7 @@ class _IntegerMixin:
485
523
  return IntegerMath.negate(self.socket)
486
524
  elif operation == "absolute":
487
525
  return IntegerMath.absolute(self.socket)
488
- return Socket._dispatch_unary(self, operation) # type: ignore[arg-type]
526
+ return Socket._dispatch_unary(cast("Socket", self), operation)
489
527
 
490
528
  def _dispatch_floordiv(self, other: Any, reverse: bool = False) -> "BaseNode":
491
529
  if self._is_geometry_tree and self._other_is_integer(other):
@@ -493,14 +531,14 @@ class _IntegerMixin:
493
531
 
494
532
  values = (self.socket, other) if not reverse else (other, self.socket)
495
533
  return IntegerMath.divide_floor(*values)
496
- return Socket._dispatch_floordiv(self, other, reverse) # type: ignore[arg-type]
534
+ return Socket._dispatch_floordiv(cast("Socket", self), other, reverse)
497
535
 
498
- def _dispatch_compare(self, other: Any, operation: str) -> "BaseNode":
536
+ def _dispatch_compare(self, other: Any, operation: str) -> "Compare | Math":
499
537
  if isinstance(self._tree.tree, GeometryNodeTree):
500
538
  from ..nodes.geometry.manual import Compare
501
539
 
502
540
  return getattr(Compare.integer, operation)(self.socket, other)
503
- return Socket._dispatch_compare(self, other, operation) # type: ignore[arg-type]
541
+ return Socket._dispatch_compare(cast("Socket", self), other, operation)
504
542
 
505
543
  if TYPE_CHECKING:
506
544
 
@@ -516,12 +554,10 @@ class _IntegerMixin:
516
554
  def __rfloordiv__(self, other: Any) -> "IntegerMath": ...
517
555
  def __neg__(self) -> "IntegerMath": ...
518
556
  def __abs__(self) -> "IntegerMath": ...
519
- def __lt__(self, other: Any) -> "Compare": ...
520
- def __gt__(self, other: Any) -> "Compare": ...
521
- def __le__(self, other: Any) -> "Compare": ...
522
- def __ge__(self, other: Any) -> "Compare": ...
523
- def __eq__(self, other: Any) -> "Compare": ... # type: ignore[override]
524
- def __ne__(self, other: Any) -> "Compare": ... # type: ignore[override]
557
+ def __lt__(self, other: Any) -> "Compare[NodeSocketInt]": ...
558
+ def __gt__(self, other: Any) -> "Compare[NodeSocketInt]": ...
559
+ def __le__(self, other: Any) -> "Compare[NodeSocketInt]": ...
560
+ def __ge__(self, other: Any) -> "Compare[NodeSocketInt]": ...
525
561
 
526
562
 
527
563
  # ---------------------------------------------------------------------------