nodebpy 0.4.1__tar.gz → 0.5.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 (63) hide show
  1. {nodebpy-0.4.1 → nodebpy-0.5.0}/PKG-INFO +4 -4
  2. {nodebpy-0.4.1 → nodebpy-0.5.0}/README.md +1 -1
  3. {nodebpy-0.4.1 → nodebpy-0.5.0}/pyproject.toml +8 -3
  4. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/__init__.py +5 -1
  5. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/arrange.py +2 -1
  6. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/builder.py +294 -13
  7. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +1 -1
  8. nodebpy-0.5.0/src/nodebpy/nodes/__init__.py +3 -0
  9. nodebpy-0.5.0/src/nodebpy/nodes/compositor/__init__.py +245 -0
  10. nodebpy-0.5.0/src/nodebpy/nodes/compositor/color.py +923 -0
  11. nodebpy-0.5.0/src/nodebpy/nodes/compositor/converter.py +1215 -0
  12. nodebpy-0.5.0/src/nodebpy/nodes/compositor/distort.py +844 -0
  13. nodebpy-0.5.0/src/nodebpy/nodes/compositor/filter.py +1102 -0
  14. nodebpy-0.5.0/src/nodebpy/nodes/compositor/group.py +21 -0
  15. nodebpy-0.5.0/src/nodebpy/nodes/compositor/input.py +496 -0
  16. nodebpy-0.5.0/src/nodebpy/nodes/compositor/interface.py +117 -0
  17. nodebpy-0.5.0/src/nodebpy/nodes/compositor/matte.py +873 -0
  18. nodebpy-0.5.0/src/nodebpy/nodes/compositor/output.py +99 -0
  19. nodebpy-0.5.0/src/nodebpy/nodes/compositor/texture.py +886 -0
  20. nodebpy-0.5.0/src/nodebpy/nodes/compositor/vector.py +35 -0
  21. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/__init__.py +18 -10
  22. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/attribute.py +16 -13
  23. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/color.py +7 -3
  24. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/converter.py +20 -15
  25. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/geometry.py +112 -55
  26. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/grid.py +67 -25
  27. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/group.py +4 -1
  28. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/input.py +9 -6
  29. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/interface.py +81 -36
  30. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/manual.py +6 -6
  31. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/output.py +3 -1
  32. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/texture.py +6 -3
  33. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/vector.py +5 -2
  34. {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.5.0/src/nodebpy/nodes/geometry}/zone.py +1 -1
  35. nodebpy-0.5.0/src/nodebpy/nodes/shader/__init__.py +251 -0
  36. nodebpy-0.5.0/src/nodebpy/nodes/shader/color.py +187 -0
  37. nodebpy-0.5.0/src/nodebpy/nodes/shader/converter.py +731 -0
  38. nodebpy-0.5.0/src/nodebpy/nodes/shader/grid.py +406 -0
  39. nodebpy-0.5.0/src/nodebpy/nodes/shader/group.py +21 -0
  40. nodebpy-0.5.0/src/nodebpy/nodes/shader/input.py +918 -0
  41. nodebpy-0.5.0/src/nodebpy/nodes/shader/interface.py +73 -0
  42. nodebpy-0.5.0/src/nodebpy/nodes/shader/output.py +373 -0
  43. nodebpy-0.5.0/src/nodebpy/nodes/shader/script.py +74 -0
  44. nodebpy-0.5.0/src/nodebpy/nodes/shader/shader.py +1557 -0
  45. nodebpy-0.5.0/src/nodebpy/nodes/shader/texture.py +356 -0
  46. nodebpy-0.5.0/src/nodebpy/nodes/shader/vector.py +402 -0
  47. nodebpy-0.5.0/src/nodebpy/nodes/shader/zone.py +58 -0
  48. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/screenshot.py +1 -1
  49. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/sockets.py +2 -0
  50. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/types.py +3 -7
  51. nodebpy-0.4.1/src/nodebpy/nodes/experimental.py +0 -318
  52. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  53. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  54. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  55. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  56. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  57. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  58. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  59. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  60. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  61. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  62. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  63. {nodebpy-0.4.1 → nodebpy-0.5.0}/src/nodebpy/screenshot_subprocess.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.5.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.5.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,15 +130,18 @@ 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
 
136
147
  # Create socket accessors for named access
@@ -138,11 +149,74 @@ class TreeBuilder:
138
149
  self.outputs = OutputInterfaceContext(self)
139
150
  self._arrange = arrange
140
151
  self.collapse = collapse
152
+ self.fake_user = fake_user
153
+
154
+ @classmethod
155
+ def geometry(
156
+ cls,
157
+ name: GeometryNodeTree | str = "Geometry Nodes",
158
+ *,
159
+ collapse: bool = False,
160
+ arrange: bool = True,
161
+ fake_user: bool = False,
162
+ ) -> "TreeBuilder":
163
+ """Create a geometry node tree."""
164
+ return cls(
165
+ name,
166
+ tree_type="GeometryNodeTree",
167
+ collapse=collapse,
168
+ arrange=arrange,
169
+ fake_user=fake_user,
170
+ )
171
+
172
+ @classmethod
173
+ def shader(
174
+ cls,
175
+ name: ShaderNodeTree | str = "Shader Nodes",
176
+ *,
177
+ collapse: bool = False,
178
+ arrange: bool = True,
179
+ fake_user: bool = False,
180
+ ) -> "TreeBuilder":
181
+ """Create a shader node tree."""
182
+ return cls(
183
+ name,
184
+ tree_type="ShaderNodeTree",
185
+ collapse=collapse,
186
+ arrange=arrange,
187
+ fake_user=fake_user,
188
+ )
189
+
190
+ @classmethod
191
+ def compositor(
192
+ cls,
193
+ name: CompositorNodeTree | str = "Compositor Nodes",
194
+ *,
195
+ collapse: bool = False,
196
+ arrange: bool = True,
197
+ fake_user: bool = False,
198
+ ) -> "TreeBuilder":
199
+ """Create a compositor node tree."""
200
+ return cls(
201
+ name,
202
+ tree_type="CompositorNodeTree",
203
+ collapse=collapse,
204
+ arrange=arrange,
205
+ fake_user=fake_user,
206
+ )
141
207
 
142
208
  @property
143
209
  def nodes(self) -> Nodes:
144
210
  return self.tree.nodes
145
211
 
212
+ @property
213
+ def fake_user(self) -> bool:
214
+ return self.tree.use_fake_user
215
+
216
+ @fake_user.setter
217
+ def fake_user(self, value: bool) -> None:
218
+ self.tree.use_extra_user = value
219
+
146
220
  def activate_tree(self) -> None:
147
221
  """Make this tree the active tree for all new node creation."""
148
222
  TreeBuilder._tree_contexts.append(self)
@@ -247,6 +321,7 @@ class NodeBuilder:
247
321
  _from_socket: NodeSocket | None = None
248
322
  _default_input_id: str | None = None
249
323
  _default_output_id: str | None = None
324
+ __array_ufunc__ = None
250
325
 
251
326
  def __init__(self):
252
327
  # Get active tree from context manager
@@ -458,7 +533,13 @@ class NodeBuilder:
458
533
  def _establish_links(self, **kwargs: TYPE_INPUT_ALL):
459
534
  input_ids = [input.identifier for input in self.node.inputs]
460
535
  for name, value in kwargs.items():
461
- if value is None:
536
+ if value is None or (
537
+ # TODO: this is an ugly single-node exception for this particular case. I'd
538
+ # like to fine a cleaner way to handle this automatically instead.
539
+ "GridPrune" in self._bl_idname
540
+ and name == "Threshold"
541
+ and self.node.data_type == "BOOLEAN"
542
+ ):
462
543
  continue
463
544
  if isinstance(value, Node):
464
545
  node = NodeBuilder()
@@ -522,7 +603,7 @@ class NodeBuilder:
522
603
  self, other: Any, operation: str, reverse: bool = False
523
604
  ) -> "VectorMath | Math":
524
605
  """Apply a math operation with appropriate Math/VectorMath node."""
525
- from .nodes import VectorMath
606
+ from .nodes.geometry import VectorMath
526
607
 
527
608
  values = (
528
609
  (self._default_output_socket, other)
@@ -556,9 +637,9 @@ class NodeBuilder:
556
637
  if isinstance(other, (int, float)):
557
638
  scalar_vector = (other, other, other)
558
639
  return (
559
- vector_method(scalar_vector, self._default_output_socket)
640
+ vector_method(self._default_output_socket, scalar_vector)
560
641
  if not reverse
561
- else vector_method(self._default_output_socket, scalar_vector)
642
+ else vector_method(scalar_vector, self._default_output_socket)
562
643
  )
563
644
  elif (
564
645
  isinstance(other, (list, tuple)) and len(other) == 3
@@ -570,12 +651,16 @@ class NodeBuilder:
570
651
  f"Unsupported type for {operation} with VECTOR operand: {type(other)}"
571
652
  )
572
653
  else: # Both operands are scalar types, use regular Math
573
- from .nodes.converter import IntegerMath, Math
654
+ from .nodes.geometry.converter import IntegerMath, Math
574
655
 
575
656
  if isinstance(other, int) and self._default_output_socket.type == "INT":
576
657
  return getattr(IntegerMath, operation)(*values)
577
658
  else:
578
- return getattr(Math, operation)(*values)
659
+ # Math node uses 'floored_modulo' instead of 'modulo'
660
+ math_operation = (
661
+ "floored_modulo" if operation == "modulo" else operation
662
+ )
663
+ return getattr(Math, math_operation)(*values)
579
664
 
580
665
  def __mul__(self, other: Any) -> "VectorMath | Math":
581
666
  return self._apply_math_operation(other, "multiply")
@@ -601,6 +686,179 @@ class NodeBuilder:
601
686
  def __rsub__(self, other: Any) -> "VectorMath | Math":
602
687
  return self._apply_math_operation(other, "subtract", reverse=True)
603
688
 
689
+ def __pow__(self, other: Any) -> "VectorMath | Math":
690
+ return self._apply_math_operation(other, "power")
691
+
692
+ def __rpow__(self, other: Any) -> "VectorMath | Math":
693
+ return self._apply_math_operation(other, "power", reverse=True)
694
+
695
+ def __mod__(self, other: Any) -> "VectorMath | Math":
696
+ return self._apply_math_operation(other, "modulo")
697
+
698
+ def __rmod__(self, other: Any) -> "VectorMath | Math":
699
+ return self._apply_math_operation(other, "modulo", reverse=True)
700
+
701
+ def __floordiv__(self, other: Any) -> "VectorMath | Math | IntegerMath":
702
+ return self._apply_floordiv_operation(other)
703
+
704
+ def __rfloordiv__(self, other: Any) -> "VectorMath | Math | IntegerMath":
705
+ return self._apply_floordiv_operation(other, reverse=True)
706
+
707
+ def __neg__(self) -> "VectorMath | Math | IntegerMath":
708
+ from .nodes.geometry import VectorMath
709
+ from .nodes.geometry.converter import IntegerMath, Math
710
+
711
+ socket = self._default_output_socket
712
+ if socket.type == "VECTOR":
713
+ return VectorMath.scale(socket, -1)
714
+ elif socket.type == "INT":
715
+ return IntegerMath.negate(socket)
716
+ else:
717
+ return Math.multiply(socket, -1)
718
+
719
+ def __abs__(self) -> "VectorMath | Math | IntegerMath":
720
+ from .nodes.geometry import VectorMath
721
+ from .nodes.geometry.converter import IntegerMath, Math
722
+
723
+ socket = self._default_output_socket
724
+ if socket.type == "VECTOR":
725
+ return VectorMath.absolute(socket)
726
+ elif socket.type == "INT":
727
+ return IntegerMath.absolute(socket)
728
+ else:
729
+ return Math.absolute(socket)
730
+
731
+ def _apply_floordiv_operation(
732
+ self, other: Any, reverse: bool = False
733
+ ) -> "VectorMath | Math | IntegerMath":
734
+ """Apply floor division: divide then floor."""
735
+ from .nodes.geometry import VectorMath
736
+ from .nodes.geometry.converter import IntegerMath, Math
737
+
738
+ socket = self._default_output_socket
739
+ component_is_vector = (
740
+ socket.type == "VECTOR" or getattr(other, "type", None) == "VECTOR"
741
+ )
742
+
743
+ if not component_is_vector and isinstance(other, int) and socket.type == "INT":
744
+ values = (socket, other) if not reverse else (other, socket)
745
+ return IntegerMath.divide_floor(*values)
746
+
747
+ divided = self._apply_math_operation(other, "divide", reverse=reverse)
748
+ if component_is_vector:
749
+ return VectorMath.floor(divided)
750
+ else:
751
+ return Math.floor(divided)
752
+
753
+ def _apply_compare_operation(self, other: Any, operation: str) -> "Compare | Math":
754
+ """Apply a comparison operation.
755
+
756
+ Uses the Compare node in geometry trees (supports float, int, vector and
757
+ outputs a boolean). Falls back to Math.less_than / Math.greater_than in
758
+ compositor and shader trees which lack a Compare node. For <= and >= in
759
+ non-geometry trees, we swap the operands (a <= b == b >= a == !(a > b)
760
+ is equivalent to less_than(b, a) when treating the output as boolean).
761
+ """
762
+ if isinstance(self._tree.tree, GeometryNodeTree):
763
+ from .nodes.geometry.manual import Compare
764
+
765
+ socket = self._default_output_socket
766
+ values = (socket, other)
767
+
768
+ if socket.type == "VECTOR":
769
+ return getattr(Compare, operation).vector(*values)
770
+ elif socket.type == "INT":
771
+ return getattr(Compare, operation).integer(*values)
772
+ else:
773
+ return getattr(Compare, operation).float(*values)
774
+ else:
775
+ # Compositor / Shader trees only have Math.less_than and
776
+ # Math.greater_than (float output, no boolean). Map <= and >= by
777
+ # swapping operands: a <= b ≡ less_than(b, a) is wrong —
778
+ # but greater_than(b, a) gives 1 when b>a i.e. a<b.
779
+ # So: a <= b → 1 - greater_than(a, b) — needs two nodes.
780
+ # Simpler: a >= b ≡ !(a < b) ≡ 1 - less_than(a, b)
781
+ from .nodes.geometry.converter import Math
782
+
783
+ socket = self._default_output_socket
784
+ _MATH_COMPARE_MAP = {
785
+ "less_than": ("less_than", False),
786
+ "greater_than": ("greater_than", False),
787
+ "less_equal": ("greater_than", True), # a<=b → !(a>b) → 1-gt(a,b)
788
+ "greater_equal": ("less_than", True), # a>=b → !(a<b) → 1-lt(a,b)
789
+ }
790
+ math_op, negate = _MATH_COMPARE_MAP[operation]
791
+ result = getattr(Math, math_op)(socket, other)
792
+ if negate:
793
+ result = Math.subtract(1.0, result._default_output_socket)
794
+ return result
795
+
796
+ def __lt__(self, other: Any) -> "Compare":
797
+ return self._apply_compare_operation(other, "less_than")
798
+
799
+ def __gt__(self, other: Any) -> "Compare":
800
+ return self._apply_compare_operation(other, "greater_than")
801
+
802
+ def __le__(self, other: Any) -> "Compare":
803
+ return self._apply_compare_operation(other, "less_equal")
804
+
805
+ def __ge__(self, other: Any) -> "Compare":
806
+ return self._apply_compare_operation(other, "greater_equal")
807
+
808
+ def _apply_boolean_operation(self, other: Any, operation: str) -> "BooleanMath":
809
+ """Apply a boolean operation using the BooleanMath node."""
810
+ from .nodes.geometry.converter import BooleanMath
811
+
812
+ return getattr(BooleanMath, operation)(self, other)
813
+
814
+ def __and__(self, other: Any) -> "BooleanMath":
815
+ return self._apply_boolean_operation(other, "l_and")
816
+
817
+ def __rand__(self, other: Any) -> "BooleanMath":
818
+ from .nodes.geometry.converter import BooleanMath
819
+
820
+ return BooleanMath.l_and(other, self)
821
+
822
+ def __or__(self, other: Any) -> "BooleanMath":
823
+ return self._apply_boolean_operation(other, "l_or")
824
+
825
+ def __ror__(self, other: Any) -> "BooleanMath":
826
+ from .nodes.geometry.converter import BooleanMath
827
+
828
+ return BooleanMath.l_or(other, self)
829
+
830
+ def __xor__(self, other: Any) -> "BooleanMath":
831
+ return self._apply_boolean_operation(other, "not_equal")
832
+
833
+ def __rxor__(self, other: Any) -> "BooleanMath":
834
+ from .nodes.geometry.converter import BooleanMath
835
+
836
+ return BooleanMath.not_equal(other, self)
837
+
838
+ def __invert__(self) -> "BooleanMath":
839
+ from .nodes.geometry.converter import BooleanMath
840
+
841
+ return BooleanMath.l_not(self)
842
+
843
+ @staticmethod
844
+ def _cast_to_matrix(value):
845
+ from .nodes.geometry.converter import CombineMatrix
846
+
847
+ if hasattr(value, "shape") and value.shape == (4, 4):
848
+ return CombineMatrix(*value.ravel())
849
+ else:
850
+ return value
851
+
852
+ def __matmul__(self, other: Any) -> "MultiplyMatrices":
853
+ from .nodes.geometry.converter import MultiplyMatrices
854
+
855
+ return MultiplyMatrices(self, self._cast_to_matrix(other))
856
+
857
+ def __rmatmul__(self, other: Any) -> "MultiplyMatrices":
858
+ from .nodes.geometry.converter import MultiplyMatrices
859
+
860
+ return MultiplyMatrices(self._cast_to_matrix(other), self)
861
+
604
862
 
605
863
  class DynamicInputsMixin:
606
864
  _socket_data_types: tuple[str]
@@ -1213,3 +1471,26 @@ class SocketClosure(SocketBase):
1213
1471
  hide_value=hide_value,
1214
1472
  hide_in_modifier=hide_in_modifier,
1215
1473
  )
1474
+
1475
+
1476
+ class SocketShader(SocketBase):
1477
+ """Shader that is the final output for a material"""
1478
+
1479
+ _bl_socket_type: str = "NodeSocketShader"
1480
+ socket: bpy.types.NodeTreeInterfaceSocketShader
1481
+
1482
+ def __init__(
1483
+ self,
1484
+ name: str = "Shader",
1485
+ description: str = "",
1486
+ *,
1487
+ optional_label: bool = False,
1488
+ hide_value: bool = False,
1489
+ hide_in_modifier: bool = False,
1490
+ ):
1491
+ super().__init__(name, description)
1492
+ self._set_values(
1493
+ optional_label=optional_label,
1494
+ hide_value=hide_value,
1495
+ hide_in_modifier=hide_in_modifier,
1496
+ )
@@ -482,7 +482,7 @@ class ClusterGraph:
482
482
  def get_socket_y(socket: NodeSocket) -> float:
483
483
  b_socket = bNodeSocket.from_address(socket.as_pointer())
484
484
  ui_scale = 1.0 # type: ignore
485
- return b_socket.runtime.contents.location[1] / ui_scale
485
+ return 1.0
486
486
 
487
487
 
488
488
  @dataclass(frozen=True)
@@ -0,0 +1,3 @@
1
+ from . import compositor, geometry, shader
2
+
3
+ __all__ = ["compositor", "geometry", "shader"]