nodebpy 0.4.1__tar.gz → 0.6.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 (66) hide show
  1. {nodebpy-0.4.1 → nodebpy-0.6.0}/PKG-INFO +4 -4
  2. {nodebpy-0.4.1 → nodebpy-0.6.0}/README.md +1 -1
  3. {nodebpy-0.4.1 → nodebpy-0.6.0}/pyproject.toml +8 -3
  4. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/__init__.py +5 -1
  5. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/arrange.py +2 -1
  6. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/builder.py +314 -15
  7. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +2 -3
  8. nodebpy-0.6.0/src/nodebpy/nodes/__init__.py +3 -0
  9. nodebpy-0.6.0/src/nodebpy/nodes/compositor/__init__.py +249 -0
  10. nodebpy-0.6.0/src/nodebpy/nodes/compositor/color.py +923 -0
  11. nodebpy-0.6.0/src/nodebpy/nodes/compositor/converter.py +1044 -0
  12. nodebpy-0.6.0/src/nodebpy/nodes/compositor/distort.py +844 -0
  13. nodebpy-0.6.0/src/nodebpy/nodes/compositor/filter.py +1102 -0
  14. nodebpy-0.6.0/src/nodebpy/nodes/compositor/group.py +21 -0
  15. nodebpy-0.6.0/src/nodebpy/nodes/compositor/input.py +476 -0
  16. nodebpy-0.6.0/src/nodebpy/nodes/compositor/interface.py +117 -0
  17. nodebpy-0.6.0/src/nodebpy/nodes/compositor/manual.py +13 -0
  18. nodebpy-0.6.0/src/nodebpy/nodes/compositor/matte.py +873 -0
  19. nodebpy-0.6.0/src/nodebpy/nodes/compositor/output.py +99 -0
  20. nodebpy-0.6.0/src/nodebpy/nodes/compositor/texture.py +886 -0
  21. nodebpy-0.6.0/src/nodebpy/nodes/compositor/vector.py +35 -0
  22. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/__init__.py +18 -10
  23. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/attribute.py +16 -13
  24. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/color.py +7 -3
  25. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/converter.py +20 -15
  26. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/geometry.py +112 -55
  27. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/grid.py +67 -25
  28. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/group.py +4 -1
  29. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/input.py +9 -6
  30. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/interface.py +81 -36
  31. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/manual.py +379 -363
  32. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/output.py +3 -1
  33. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/texture.py +6 -3
  34. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/vector.py +5 -2
  35. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/zone.py +1 -1
  36. nodebpy-0.6.0/src/nodebpy/nodes/shader/__init__.py +253 -0
  37. nodebpy-0.6.0/src/nodebpy/nodes/shader/color.py +187 -0
  38. nodebpy-0.6.0/src/nodebpy/nodes/shader/converter.py +503 -0
  39. nodebpy-0.6.0/src/nodebpy/nodes/shader/grid.py +406 -0
  40. nodebpy-0.6.0/src/nodebpy/nodes/shader/group.py +21 -0
  41. nodebpy-0.6.0/src/nodebpy/nodes/shader/input.py +898 -0
  42. nodebpy-0.6.0/src/nodebpy/nodes/shader/interface.py +72 -0
  43. nodebpy-0.6.0/src/nodebpy/nodes/shader/manual.py +15 -0
  44. nodebpy-0.6.0/src/nodebpy/nodes/shader/output.py +373 -0
  45. nodebpy-0.6.0/src/nodebpy/nodes/shader/script.py +74 -0
  46. nodebpy-0.6.0/src/nodebpy/nodes/shader/shader.py +1557 -0
  47. nodebpy-0.6.0/src/nodebpy/nodes/shader/texture.py +356 -0
  48. nodebpy-0.6.0/src/nodebpy/nodes/shader/vector.py +402 -0
  49. nodebpy-0.6.0/src/nodebpy/nodes/shader/zone.py +58 -0
  50. nodebpy-0.6.0/src/nodebpy/screenshot.py +228 -0
  51. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/sockets.py +2 -0
  52. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/types.py +4 -7
  53. nodebpy-0.4.1/src/nodebpy/nodes/experimental.py +0 -318
  54. nodebpy-0.4.1/src/nodebpy/screenshot.py +0 -532
  55. nodebpy-0.4.1/src/nodebpy/screenshot_subprocess.py +0 -422
  56. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  57. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  58. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  59. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  60. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  61. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  62. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  63. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  64. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  65. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  66. {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 0.4.1
4
- Summary: Build nodes in Blender with code
3
+ Version: 0.6.0
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
  Requires-Dist: networkx>=3.6.1
8
- Requires-Dist: bpy>=5.0.0 ; extra == 'bpy'
8
+ Requires-Dist: bpy>=5.0.1 ; extra == 'bpy'
9
9
  Requires-Dist: fake-bpy-module>=20260113 ; extra == 'dev'
10
10
  Requires-Dist: jsondiff>=2.2.1 ; extra == 'dev'
11
11
  Requires-Dist: pytest>=9.0.2 ; extra == 'dev'
@@ -165,5 +165,5 @@ Most node classes are generated automatically with this. The nodes in
165
165
  complexities of particular nodes (usually lergacy).
166
166
 
167
167
  ``` bash
168
- uv run generate.py && ruff format && ruff check --fix --unsafe-fixes
168
+ uv run generate.py && ruff format && ruff check --fix
169
169
  ```
@@ -141,5 +141,5 @@ Most node classes are generated automatically with this. The nodes in
141
141
  complexities of particular nodes (usually lergacy).
142
142
 
143
143
  ``` bash
144
- uv run generate.py && ruff format && ruff check --fix --unsafe-fixes
144
+ uv run generate.py && ruff format && ruff check --fix
145
145
  ```
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "nodebpy"
3
- version = "0.4.1"
4
- description = "Build nodes in Blender with code"
3
+ version = "0.6.0"
4
+ description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
7
7
  { name = "Brady Johnston", email = "brady.johnston@me.com" }
@@ -16,7 +16,7 @@ nodebpy = "nodebpy:main"
16
16
 
17
17
  [project.optional-dependencies]
18
18
  bpy = [
19
- "bpy>=5.0.0",
19
+ "bpy>=5.0.1",
20
20
  ]
21
21
  jupyter = [
22
22
  "ipython>=8.0.0",
@@ -36,3 +36,8 @@ dev = [
36
36
  [build-system]
37
37
  requires = ["uv_build>=0.8.15,<0.9.0"]
38
38
  build-backend = "uv_build"
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "numpy<2.0",
43
+ ]
@@ -1,9 +1,13 @@
1
- from . import nodes, sockets, screenshot
1
+ from . import nodes, screenshot, sockets
2
2
  from .builder import TreeBuilder
3
+ from .nodes import compositor, geometry, shader
3
4
  from .screenshot import generate_mermaid_diagram, save_mermaid_diagram
4
5
 
5
6
  __all__ = [
6
7
  "nodes",
8
+ "compositor",
9
+ "geometry",
10
+ "shader",
7
11
  "sockets",
8
12
  "screenshot",
9
13
  "TreeBuilder",
@@ -1,5 +1,6 @@
1
1
  import typing
2
2
  from collections import Counter, deque
3
+
3
4
  import bpy
4
5
  from mathutils import Vector
5
6
 
@@ -245,7 +246,7 @@ def position_nodes_in_columns(
245
246
  spacing : tuple of float, optional
246
247
  Tuple of (horizontal, vertical) spacing between nodes, by default (50, 25)
247
248
  """
248
- interface_scale = bpy.context.preferences.view.ui_scale
249
+ interface_scale = 1.0
249
250
  non_geo_offset = 20 + 28 * 2 # header + 2 socket heights
250
251
 
251
252
  # position nodes column by column
@@ -3,14 +3,19 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any, ClassVar, Iterable, Literal
4
4
 
5
5
  if TYPE_CHECKING:
6
- from .nodes import Math, VectorMath
6
+ from .nodes.geometry import IntegerMath, Math, VectorMath
7
+ from .nodes.geometry.converter import BooleanMath, MultiplyMatrices
8
+ from .nodes.geometry.manual import Compare
7
9
 
8
10
  import bpy
9
11
  from bpy.types import (
12
+ CompositorNodeTree,
10
13
  GeometryNodeTree,
11
14
  Node,
12
15
  Nodes,
13
16
  NodeSocket,
17
+ NodeTree,
18
+ ShaderNodeTree,
14
19
  )
15
20
 
16
21
  from .lib.nodearrange import arrange
@@ -64,9 +69,9 @@ class SocketContext:
64
69
  self.builder = tree_builder
65
70
 
66
71
  @property
67
- def tree(self) -> GeometryNodeTree:
72
+ def tree(self) -> NodeTree:
68
73
  tree = self.builder.tree
69
- assert tree is not None and isinstance(tree, GeometryNodeTree)
74
+ assert tree is not None
70
75
  return tree
71
76
 
72
77
  @property
@@ -114,7 +119,10 @@ class OutputInterfaceContext(DirectionalContext):
114
119
 
115
120
 
116
121
  class TreeBuilder:
117
- """Builder for creating Blender geometry node trees with a clean Python API."""
122
+ """Builder for creating Blender node trees with a clean Python API.
123
+
124
+ Supports geometry, shader, and compositor node trees.
125
+ """
118
126
 
119
127
  _tree_contexts: ClassVar["list[TreeBuilder]"] = []
120
128
  just_added: "Node | None" = None
@@ -122,27 +130,94 @@ class TreeBuilder:
122
130
 
123
131
  def __init__(
124
132
  self,
125
- tree: GeometryNodeTree | str = "Geometry Nodes",
133
+ tree: NodeTree | str = "Geometry Nodes",
126
134
  *,
135
+ tree_type: Literal[
136
+ "GeometryNodeTree", "ShaderNodeTree", "CompositorNodeTree"
137
+ ] = "GeometryNodeTree",
127
138
  collapse: bool = False,
128
139
  arrange: bool = True,
140
+ fake_user: bool = False,
129
141
  ):
130
142
  if isinstance(tree, str):
131
- self.tree = bpy.data.node_groups.new(tree, "GeometryNodeTree")
143
+ self.tree = bpy.data.node_groups.new(tree, tree_type)
132
144
  else:
133
- assert isinstance(tree, GeometryNodeTree)
134
145
  self.tree = tree
135
146
 
147
+ self._menu_defaults: dict[str, str] = {}
136
148
  # Create socket accessors for named access
137
149
  self.inputs = InputInterfaceContext(self)
138
150
  self.outputs = OutputInterfaceContext(self)
139
151
  self._arrange = arrange
140
152
  self.collapse = collapse
153
+ self.fake_user = fake_user
154
+
155
+ @classmethod
156
+ def geometry(
157
+ cls,
158
+ name: GeometryNodeTree | str = "Geometry Nodes",
159
+ *,
160
+ collapse: bool = False,
161
+ arrange: bool = True,
162
+ fake_user: bool = False,
163
+ ) -> "TreeBuilder":
164
+ """Create a geometry node tree."""
165
+ return cls(
166
+ name,
167
+ tree_type="GeometryNodeTree",
168
+ collapse=collapse,
169
+ arrange=arrange,
170
+ fake_user=fake_user,
171
+ )
172
+
173
+ @classmethod
174
+ def shader(
175
+ cls,
176
+ name: ShaderNodeTree | str = "Shader Nodes",
177
+ *,
178
+ collapse: bool = False,
179
+ arrange: bool = True,
180
+ fake_user: bool = False,
181
+ ) -> "TreeBuilder":
182
+ """Create a shader node tree."""
183
+ return cls(
184
+ name,
185
+ tree_type="ShaderNodeTree",
186
+ collapse=collapse,
187
+ arrange=arrange,
188
+ fake_user=fake_user,
189
+ )
190
+
191
+ @classmethod
192
+ def compositor(
193
+ cls,
194
+ name: CompositorNodeTree | str = "Compositor Nodes",
195
+ *,
196
+ collapse: bool = False,
197
+ arrange: bool = True,
198
+ fake_user: bool = False,
199
+ ) -> "TreeBuilder":
200
+ """Create a compositor node tree."""
201
+ return cls(
202
+ name,
203
+ tree_type="CompositorNodeTree",
204
+ collapse=collapse,
205
+ arrange=arrange,
206
+ fake_user=fake_user,
207
+ )
141
208
 
142
209
  @property
143
210
  def nodes(self) -> Nodes:
144
211
  return self.tree.nodes
145
212
 
213
+ @property
214
+ def fake_user(self) -> bool:
215
+ return self.tree.use_fake_user
216
+
217
+ @fake_user.setter
218
+ def fake_user(self, value: bool) -> None:
219
+ self.tree.use_extra_user = value
220
+
146
221
  def activate_tree(self) -> None:
147
222
  """Make this tree the active tree for all new node creation."""
148
223
  TreeBuilder._tree_contexts.append(self)
@@ -158,8 +233,16 @@ class TreeBuilder:
158
233
  def __exit__(self, *args):
159
234
  if self._arrange:
160
235
  self.arrange()
236
+ self._apply_input_defaults()
161
237
  self.deactivate_tree()
162
238
 
239
+ def _apply_input_defaults(self) -> None:
240
+ for key, value in self._menu_defaults.items():
241
+ for item in self.tree.interface.items_tree:
242
+ if item.identifier == key:
243
+ item.default_value = value
244
+ break
245
+
163
246
  def __len__(self) -> int:
164
247
  return len(self.nodes)
165
248
 
@@ -247,6 +330,7 @@ class NodeBuilder:
247
330
  _from_socket: NodeSocket | None = None
248
331
  _default_input_id: str | None = None
249
332
  _default_output_id: str | None = None
333
+ __array_ufunc__ = None
250
334
 
251
335
  def __init__(self):
252
336
  # Get active tree from context manager
@@ -458,7 +542,13 @@ class NodeBuilder:
458
542
  def _establish_links(self, **kwargs: TYPE_INPUT_ALL):
459
543
  input_ids = [input.identifier for input in self.node.inputs]
460
544
  for name, value in kwargs.items():
461
- if value is None:
545
+ if value is None or (
546
+ # TODO: this is an ugly single-node exception for this particular case. I'd
547
+ # like to fine a cleaner way to handle this automatically instead.
548
+ "GridPrune" in self._bl_idname
549
+ and name == "Threshold"
550
+ and self.node.data_type == "BOOLEAN"
551
+ ):
462
552
  continue
463
553
  if isinstance(value, Node):
464
554
  node = NodeBuilder()
@@ -522,7 +612,7 @@ class NodeBuilder:
522
612
  self, other: Any, operation: str, reverse: bool = False
523
613
  ) -> "VectorMath | Math":
524
614
  """Apply a math operation with appropriate Math/VectorMath node."""
525
- from .nodes import VectorMath
615
+ from .nodes.geometry import VectorMath
526
616
 
527
617
  values = (
528
618
  (self._default_output_socket, other)
@@ -556,9 +646,9 @@ class NodeBuilder:
556
646
  if isinstance(other, (int, float)):
557
647
  scalar_vector = (other, other, other)
558
648
  return (
559
- vector_method(scalar_vector, self._default_output_socket)
649
+ vector_method(self._default_output_socket, scalar_vector)
560
650
  if not reverse
561
- else vector_method(self._default_output_socket, scalar_vector)
651
+ else vector_method(scalar_vector, self._default_output_socket)
562
652
  )
563
653
  elif (
564
654
  isinstance(other, (list, tuple)) and len(other) == 3
@@ -570,12 +660,16 @@ class NodeBuilder:
570
660
  f"Unsupported type for {operation} with VECTOR operand: {type(other)}"
571
661
  )
572
662
  else: # Both operands are scalar types, use regular Math
573
- from .nodes.converter import IntegerMath, Math
663
+ from .nodes.geometry.converter import IntegerMath, Math
574
664
 
575
665
  if isinstance(other, int) and self._default_output_socket.type == "INT":
576
666
  return getattr(IntegerMath, operation)(*values)
577
667
  else:
578
- return getattr(Math, operation)(*values)
668
+ # Math node uses 'floored_modulo' instead of 'modulo'
669
+ math_operation = (
670
+ "floored_modulo" if operation == "modulo" else operation
671
+ )
672
+ return getattr(Math, math_operation)(*values)
579
673
 
580
674
  def __mul__(self, other: Any) -> "VectorMath | Math":
581
675
  return self._apply_math_operation(other, "multiply")
@@ -601,9 +695,182 @@ class NodeBuilder:
601
695
  def __rsub__(self, other: Any) -> "VectorMath | Math":
602
696
  return self._apply_math_operation(other, "subtract", reverse=True)
603
697
 
698
+ def __pow__(self, other: Any) -> "VectorMath | Math":
699
+ return self._apply_math_operation(other, "power")
700
+
701
+ def __rpow__(self, other: Any) -> "VectorMath | Math":
702
+ return self._apply_math_operation(other, "power", reverse=True)
703
+
704
+ def __mod__(self, other: Any) -> "VectorMath | Math":
705
+ return self._apply_math_operation(other, "modulo")
706
+
707
+ def __rmod__(self, other: Any) -> "VectorMath | Math":
708
+ return self._apply_math_operation(other, "modulo", reverse=True)
709
+
710
+ def __floordiv__(self, other: Any) -> "VectorMath | Math | IntegerMath":
711
+ return self._apply_floordiv_operation(other)
712
+
713
+ def __rfloordiv__(self, other: Any) -> "VectorMath | Math | IntegerMath":
714
+ return self._apply_floordiv_operation(other, reverse=True)
715
+
716
+ def __neg__(self) -> "VectorMath | Math | IntegerMath":
717
+ from .nodes.geometry import VectorMath
718
+ from .nodes.geometry.converter import IntegerMath, Math
719
+
720
+ socket = self._default_output_socket
721
+ if socket.type == "VECTOR":
722
+ return VectorMath.scale(socket, -1)
723
+ elif socket.type == "INT":
724
+ return IntegerMath.negate(socket)
725
+ else:
726
+ return Math.multiply(socket, -1)
727
+
728
+ def __abs__(self) -> "VectorMath | Math | IntegerMath":
729
+ from .nodes.geometry import VectorMath
730
+ from .nodes.geometry.converter import IntegerMath, Math
731
+
732
+ socket = self._default_output_socket
733
+ if socket.type == "VECTOR":
734
+ return VectorMath.absolute(socket)
735
+ elif socket.type == "INT":
736
+ return IntegerMath.absolute(socket)
737
+ else:
738
+ return Math.absolute(socket)
739
+
740
+ def _apply_floordiv_operation(
741
+ self, other: Any, reverse: bool = False
742
+ ) -> "VectorMath | Math | IntegerMath":
743
+ """Apply floor division: divide then floor."""
744
+ from .nodes.geometry import VectorMath
745
+ from .nodes.geometry.converter import IntegerMath, Math
746
+
747
+ socket = self._default_output_socket
748
+ component_is_vector = (
749
+ socket.type == "VECTOR" or getattr(other, "type", None) == "VECTOR"
750
+ )
751
+
752
+ if not component_is_vector and isinstance(other, int) and socket.type == "INT":
753
+ values = (socket, other) if not reverse else (other, socket)
754
+ return IntegerMath.divide_floor(*values)
755
+
756
+ divided = self._apply_math_operation(other, "divide", reverse=reverse)
757
+ if component_is_vector:
758
+ return VectorMath.floor(divided)
759
+ else:
760
+ return Math.floor(divided)
761
+
762
+ def _apply_compare_operation(self, other: Any, operation: str) -> "Compare | Math":
763
+ """Apply a comparison operation.
764
+
765
+ Uses the Compare node in geometry trees (supports float, int, vector and
766
+ outputs a boolean). Falls back to Math.less_than / Math.greater_than in
767
+ compositor and shader trees which lack a Compare node. For <= and >= in
768
+ non-geometry trees, we swap the operands (a <= b == b >= a == !(a > b)
769
+ is equivalent to less_than(b, a) when treating the output as boolean).
770
+ """
771
+ if isinstance(self._tree.tree, GeometryNodeTree):
772
+ from .nodes.geometry.manual import Compare
773
+
774
+ socket = self._default_output_socket
775
+ values = (socket, other)
776
+
777
+ if socket.type == "VECTOR":
778
+ return getattr(Compare, operation).vector(*values)
779
+ elif socket.type == "INT":
780
+ return getattr(Compare, operation).integer(*values)
781
+ else:
782
+ return getattr(Compare, operation).float(*values)
783
+ else:
784
+ # Compositor / Shader trees only have Math.less_than and
785
+ # Math.greater_than (float output, no boolean). Map <= and >= by
786
+ # swapping operands: a <= b ≡ less_than(b, a) is wrong —
787
+ # but greater_than(b, a) gives 1 when b>a i.e. a<b.
788
+ # So: a <= b → 1 - greater_than(a, b) — needs two nodes.
789
+ # Simpler: a >= b ≡ !(a < b) ≡ 1 - less_than(a, b)
790
+ from .nodes.geometry.converter import Math
791
+
792
+ socket = self._default_output_socket
793
+ _MATH_COMPARE_MAP = {
794
+ "less_than": ("less_than", False),
795
+ "greater_than": ("greater_than", False),
796
+ "less_equal": ("greater_than", True), # a<=b → !(a>b) → 1-gt(a,b)
797
+ "greater_equal": ("less_than", True), # a>=b → !(a<b) → 1-lt(a,b)
798
+ }
799
+ math_op, negate = _MATH_COMPARE_MAP[operation]
800
+ result = getattr(Math, math_op)(socket, other)
801
+ if negate:
802
+ result = Math.subtract(1.0, result._default_output_socket)
803
+ return result
804
+
805
+ def __lt__(self, other: Any) -> "Compare":
806
+ return self._apply_compare_operation(other, "less_than")
807
+
808
+ def __gt__(self, other: Any) -> "Compare":
809
+ return self._apply_compare_operation(other, "greater_than")
810
+
811
+ def __le__(self, other: Any) -> "Compare":
812
+ return self._apply_compare_operation(other, "less_equal")
813
+
814
+ def __ge__(self, other: Any) -> "Compare":
815
+ return self._apply_compare_operation(other, "greater_equal")
816
+
817
+ def _apply_boolean_operation(self, other: Any, operation: str) -> "BooleanMath":
818
+ """Apply a boolean operation using the BooleanMath node."""
819
+ from .nodes.geometry.converter import BooleanMath
820
+
821
+ return getattr(BooleanMath, operation)(self, other)
822
+
823
+ def __and__(self, other: Any) -> "BooleanMath":
824
+ return self._apply_boolean_operation(other, "l_and")
825
+
826
+ def __rand__(self, other: Any) -> "BooleanMath":
827
+ from .nodes.geometry.converter import BooleanMath
828
+
829
+ return BooleanMath.l_and(other, self)
830
+
831
+ def __or__(self, other: Any) -> "BooleanMath":
832
+ return self._apply_boolean_operation(other, "l_or")
833
+
834
+ def __ror__(self, other: Any) -> "BooleanMath":
835
+ from .nodes.geometry.converter import BooleanMath
836
+
837
+ return BooleanMath.l_or(other, self)
838
+
839
+ def __xor__(self, other: Any) -> "BooleanMath":
840
+ return self._apply_boolean_operation(other, "not_equal")
841
+
842
+ def __rxor__(self, other: Any) -> "BooleanMath":
843
+ from .nodes.geometry.converter import BooleanMath
844
+
845
+ return BooleanMath.not_equal(other, self)
846
+
847
+ def __invert__(self) -> "BooleanMath":
848
+ from .nodes.geometry.converter import BooleanMath
849
+
850
+ return BooleanMath.l_not(self)
851
+
852
+ @staticmethod
853
+ def _cast_to_matrix(value):
854
+ from .nodes.geometry.converter import CombineMatrix
855
+
856
+ if hasattr(value, "shape") and value.shape == (4, 4):
857
+ return CombineMatrix(*value.ravel())
858
+ else:
859
+ return value
860
+
861
+ def __matmul__(self, other: Any) -> "MultiplyMatrices":
862
+ from .nodes.geometry.converter import MultiplyMatrices
863
+
864
+ return MultiplyMatrices(self, self._cast_to_matrix(other))
865
+
866
+ def __rmatmul__(self, other: Any) -> "MultiplyMatrices":
867
+ from .nodes.geometry.converter import MultiplyMatrices
868
+
869
+ return MultiplyMatrices(self._cast_to_matrix(other), self)
870
+
604
871
 
605
872
  class DynamicInputsMixin:
606
- _socket_data_types: tuple[str]
873
+ _socket_data_types: tuple[str, ...]
607
874
  _type_map: dict[str, str] = {}
608
875
 
609
876
  def _match_compatible_data(
@@ -732,7 +999,16 @@ class SocketBase(SocketLinker):
732
999
  for key, value in kwargs.items():
733
1000
  if value is None:
734
1001
  continue
735
- setattr(self.interface_socket, key, value)
1002
+ if (
1003
+ self.interface_socket.socket_type == "NodeSocketMenu"
1004
+ and key == "default_value"
1005
+ ):
1006
+ # we delay the setting of the default value until the menu is created
1007
+ # because it doesn't have potential enum values yet until the menu socket is
1008
+ # connected and the node tree is complete
1009
+ self.tree._menu_defaults[self.interface_socket.identifier] = value
1010
+ else:
1011
+ setattr(self.interface_socket, key, value)
736
1012
 
737
1013
  @property
738
1014
  def default_value(self):
@@ -1213,3 +1489,26 @@ class SocketClosure(SocketBase):
1213
1489
  hide_value=hide_value,
1214
1490
  hide_in_modifier=hide_in_modifier,
1215
1491
  )
1492
+
1493
+
1494
+ class SocketShader(SocketBase):
1495
+ """Shader that is the final output for a material"""
1496
+
1497
+ _bl_socket_type: str = "NodeSocketShader"
1498
+ socket: bpy.types.NodeTreeInterfaceSocketShader
1499
+
1500
+ def __init__(
1501
+ self,
1502
+ name: str = "Shader",
1503
+ description: str = "",
1504
+ *,
1505
+ optional_label: bool = False,
1506
+ hide_value: bool = False,
1507
+ hide_in_modifier: bool = False,
1508
+ ):
1509
+ super().__init__(name, description)
1510
+ self._set_values(
1511
+ optional_label=optional_label,
1512
+ hide_value=hide_value,
1513
+ hide_in_modifier=hide_in_modifier,
1514
+ )
@@ -480,9 +480,8 @@ class ClusterGraph:
480
480
 
481
481
 
482
482
  def get_socket_y(socket: NodeSocket) -> float:
483
- b_socket = bNodeSocket.from_address(socket.as_pointer())
484
- ui_scale = 1.0 # type: ignore
485
- return b_socket.runtime.contents.location[1] / ui_scale
483
+ bNodeSocket.from_address(socket.as_pointer())
484
+ return 1.0
486
485
 
487
486
 
488
487
  @dataclass(frozen=True)
@@ -0,0 +1,3 @@
1
+ from . import compositor, geometry, shader
2
+
3
+ __all__ = ["compositor", "geometry", "shader"]