nodebpy 0.12.0__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.12.0 → nodebpy-0.13.0}/PKG-INFO +1 -1
  2. {nodebpy-0.12.0 → nodebpy-0.13.0}/pyproject.toml +1 -1
  3. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/__init__.py +0 -8
  4. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/builder/__init__.py +3 -0
  5. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/builder/_utils.py +4 -1
  6. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/builder/accessor.py +1 -1
  7. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/builder/mixins.py +10 -2
  8. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/builder/node.py +29 -5
  9. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/builder/socket.py +62 -26
  10. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/builder/tree.py +7 -1
  11. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/__init__.py +29 -13
  12. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/converter.py +1 -1
  13. {nodebpy-0.12.0 → 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.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/__init__.py +16 -2
  16. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/geometry.py +0 -576
  17. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/manual.py +749 -1
  18. nodebpy-0.13.0/src/nodebpy/nodes/geometry/utilities.py +69 -0
  19. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/zone.py +137 -5
  20. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/__init__.py +18 -14
  21. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/converter.py +1 -75
  22. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/manual.py +2 -1
  23. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/types.py +3 -0
  24. nodebpy-0.12.0/src/nodebpy/nodes/compositor/manual.py +0 -30
  25. nodebpy-0.12.0/src/nodebpy/nodes/shader/interface.py +0 -100
  26. {nodebpy-0.12.0 → nodebpy-0.13.0}/README.md +0 -0
  27. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/arrange.py +0 -0
  28. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/builder/_registry.py +0 -0
  29. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/builder/interface.py +0 -0
  30. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/diagram.py +0 -0
  31. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  32. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
  33. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  34. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  35. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  36. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  37. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  38. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  39. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  40. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  41. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  42. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  43. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/__init__.py +0 -0
  44. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/color.py +0 -0
  45. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
  46. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
  47. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/group.py +0 -0
  48. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
  49. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
  50. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/output.py +0 -0
  51. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
  52. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
  53. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/color.py +0 -0
  54. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
  55. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
  56. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/group.py +0 -0
  57. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
  58. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/input.py +0 -0
  59. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
  60. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/output.py +0 -0
  61. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
  62. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
  63. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/color.py +0 -0
  64. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/grid.py +0 -0
  65. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/group.py +0 -0
  66. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/input.py +0 -0
  67. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/output.py +0 -0
  68. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/script.py +0 -0
  69. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/shader.py +0 -0
  70. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/texture.py +0 -0
  71. {nodebpy-0.12.0 → nodebpy-0.13.0}/src/nodebpy/nodes/shader/vector.py +0 -0
  72. {nodebpy-0.12.0 → 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.12.0
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.12.0"
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,9 +1,5 @@
1
1
  from . import diagram, nodes, sockets
2
2
  from .builder import (
3
- CustomCompositorGroup,
4
- CustomGeometryGroup,
5
- CustomShaderGroup,
6
- NodeGroupBuilder,
7
3
  TreeBuilder,
8
4
  )
9
5
  from .nodes import compositor, geometry, shader
@@ -16,8 +12,4 @@ __all__ = [
16
12
  "sockets",
17
13
  "diagram",
18
14
  "TreeBuilder",
19
- "NodeGroupBuilder",
20
- "GeometryNodeGroup",
21
- "ShaderNodeGroup",
22
- "CompositorNodeGroup",
23
15
  ]
@@ -89,6 +89,9 @@ __all__ = [
89
89
  "DynamicInputsMixin",
90
90
  # Node groups
91
91
  "NodeGroupBuilder",
92
+ "CustomCompositorGroup",
93
+ "CustomGeometryGroup",
94
+ "CustomShaderGroup",
92
95
  "GeometryNodeGroup",
93
96
  "ShaderNodeGroup",
94
97
  "CompositorNodeGroup",
@@ -67,7 +67,10 @@ 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
@@ -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:
@@ -15,10 +15,13 @@ from typing import (
15
15
 
16
16
  import bpy
17
17
  from bpy.types import (
18
+ CompositorNodeGroup,
18
19
  CompositorNodeTree,
20
+ GeometryNodeGroup,
19
21
  GeometryNodeTree,
20
22
  Node,
21
23
  NodeSocket,
24
+ ShaderNodeGroup,
22
25
  ShaderNodeTree,
23
26
  )
24
27
 
@@ -50,7 +53,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
50
53
  _default_output_id: str | None = None
51
54
  _placeholder_inputs: list[str]
52
55
 
53
- def __init__(self, node: bpy.types.Node | None = None):
56
+ def __init__(self, node: Node | None = None):
54
57
  tree = (
55
58
  TreeBuilder._tree_contexts[-1] if len(TreeBuilder._tree_contexts) else None
56
59
  )
@@ -97,7 +100,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
97
100
  return socket
98
101
 
99
102
  @classmethod
100
- def _from_node(cls, node: bpy.types.Node) -> Self:
103
+ def _from_node(cls, node: Node) -> Self:
101
104
  builder = cls()
102
105
  builder.tree.nodes.remove(builder.node)
103
106
  builder.node = node
@@ -274,6 +277,12 @@ class NodeGroupBuilder(BaseNode, ABC, Generic[_T]):
274
277
  self.node.show_options = False
275
278
  self._establish_links(**kwargs)
276
279
 
280
+ @property
281
+ @abstractmethod
282
+ def node_tree(self) -> _T:
283
+ """The internal node tree for this group node."""
284
+ ...
285
+
277
286
  @abstractmethod
278
287
  def _setup_node_group(self) -> None:
279
288
  """Set ``self.node.node_tree`` and any node-type-specific properties.
@@ -304,7 +313,12 @@ class CustomGeometryGroup(NodeGroupBuilder[GeometryNodeTree]):
304
313
  """Node group in a Geometry Nodes tree."""
305
314
 
306
315
  _bl_idname = "GeometryNodeGroup"
307
- node: bpy.types.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
308
322
 
309
323
  def _setup_node_group(self) -> None:
310
324
  self.node.node_tree = self._get_or_create_group()
@@ -324,7 +338,12 @@ class CustomShaderGroup(NodeGroupBuilder[ShaderNodeTree]):
324
338
  """Node group in a Shader (Material) node tree."""
325
339
 
326
340
  _bl_idname = "ShaderNodeGroup"
327
- node: bpy.types.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
328
347
 
329
348
  def _setup_node_group(self) -> None:
330
349
  self.node.node_tree = self._get_or_create_group()
@@ -343,7 +362,12 @@ class CustomCompositorGroup(NodeGroupBuilder[CompositorNodeTree]):
343
362
  """Node group in a Compositor node tree."""
344
363
 
345
364
  _bl_idname = "CompositorNodeGroup"
346
- node: bpy.types.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
347
371
 
348
372
  def _setup_node_group(self) -> None:
349
373
  self.node.node_tree = self._get_or_create_group()
@@ -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
  # ---------------------------------------------------------------------------
@@ -823,8 +823,14 @@ class TreeBuilder(Generic[_TreeT]):
823
823
  if not isinstance(socket2, NodeSocket):
824
824
  socket2 = socket2.socket # type: ignore[attr-defined]
825
825
 
826
+ is_reroute = (
827
+ getattr(socket1.node, "bl_idname", None) == "NodeReroute"
828
+ or getattr(socket2.node, "bl_idname", None) == "NodeReroute"
829
+ )
826
830
  if (
827
- socket1.type not in SOCKET_COMPATIBILITY.get(socket2.type, ())
831
+ not is_reroute
832
+ and socket1.type not in SOCKET_COMPATIBILITY.get(socket2.type, ())
833
+ and socket1.type != "CUSTOM"
828
834
  and socket2.type != "CUSTOM"
829
835
  ):
830
836
  raise SocketError(
@@ -4,6 +4,9 @@ from .manual import (
4
4
  MenuSwitch,
5
5
  Frame,
6
6
  tree,
7
+ Float,
8
+ Image,
9
+ Cryptomatte,
7
10
  )
8
11
  from ..geometry.color import (
9
12
  Gamma,
@@ -41,6 +44,9 @@ from ..geometry.texture import (
41
44
  WaveTexture,
42
45
  WhiteNoiseTexture,
43
46
  )
47
+ from ..geometry.utilities import (
48
+ Reroute,
49
+ )
44
50
  from ..geometry.vector import (
45
51
  RadialTiling,
46
52
  VectorCurves,
@@ -80,6 +86,20 @@ from .filter import (
80
86
  Pixelate,
81
87
  VectorBlur,
82
88
  )
89
+ from .input import (
90
+ BokehImage,
91
+ Color,
92
+ ImageCoordinates,
93
+ ImageInfo,
94
+ Mask,
95
+ MovieClip,
96
+ Normal,
97
+ RenderLayers,
98
+ SceneTime,
99
+ SequencerStripInfo,
100
+ TimeCurve,
101
+ TrackPosition,
102
+ )
83
103
  from .matte import (
84
104
  BoxMask,
85
105
  ChannelKey,
@@ -103,7 +123,7 @@ from .converter import (
103
123
  IndexSwitch,
104
124
  Levels,
105
125
  Mix,
106
- RGBToBw,
126
+ RGBToBW,
107
127
  RelativeToPixel,
108
128
  SeparateColor,
109
129
  SetAlpha,
@@ -129,17 +149,6 @@ from .distort import (
129
149
  from .group import (
130
150
  Group,
131
151
  )
132
- from .input import (
133
- Color,
134
- Mask,
135
- MovieClip,
136
- Normal,
137
- RenderLayers,
138
- SceneTime,
139
- SequencerStripInfo,
140
- TimeCurve,
141
- TrackPosition,
142
- )
143
152
  from .vector import (
144
153
  Normalize,
145
154
  )
@@ -159,6 +168,7 @@ __all__ = (
159
168
  "Blackbody",
160
169
  "Blur",
161
170
  "BokehBlur",
171
+ "BokehImage",
162
172
  "Boolean",
163
173
  "BoxMask",
164
174
  "BrickTexture",
@@ -180,6 +190,7 @@ __all__ = (
180
190
  "Convolve",
181
191
  "CornerPin",
182
192
  "Crop",
193
+ "Cryptomatte",
183
194
  "Defocus",
184
195
  "Denoise",
185
196
  "DepthCombine",
@@ -196,6 +207,7 @@ __all__ = (
196
207
  "FileOutput",
197
208
  "Filter",
198
209
  "Flip",
210
+ "Float",
199
211
  "FloatCurve",
200
212
  "Frame",
201
213
  "GaborTexture",
@@ -208,6 +220,9 @@ __all__ = (
208
220
  "HueCorrect",
209
221
  "Huesaturationvalue",
210
222
  "IDMask",
223
+ "Image",
224
+ "ImageCoordinates",
225
+ "ImageInfo",
211
226
  "IndexSwitch",
212
227
  "Inpaint",
213
228
  "Integer",
@@ -235,10 +250,11 @@ __all__ = (
235
250
  "PlaneTrackDeform",
236
251
  "Posterize",
237
252
  "RGBCurves",
238
- "RGBToBw",
253
+ "RGBToBW",
239
254
  "RadialTiling",
240
255
  "RelativeToPixel",
241
256
  "RenderLayers",
257
+ "Reroute",
242
258
  "Rotate",
243
259
  "Scale",
244
260
  "SceneTime",
@@ -1111,7 +1111,7 @@ class Mix(NodeBuilder):
1111
1111
  self.node.clamp_result = value
1112
1112
 
1113
1113
 
1114
- class RGBToBw(NodeBuilder):
1114
+ class RGBToBW(NodeBuilder):
1115
1115
  """
1116
1116
  Convert RGB input into grayscale using luminance
1117
1117