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.
- {nodebpy-0.4.1 → nodebpy-0.6.0}/PKG-INFO +4 -4
- {nodebpy-0.4.1 → nodebpy-0.6.0}/README.md +1 -1
- {nodebpy-0.4.1 → nodebpy-0.6.0}/pyproject.toml +8 -3
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/__init__.py +5 -1
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/arrange.py +2 -1
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/builder.py +314 -15
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +2 -3
- nodebpy-0.6.0/src/nodebpy/nodes/__init__.py +3 -0
- nodebpy-0.6.0/src/nodebpy/nodes/compositor/__init__.py +249 -0
- nodebpy-0.6.0/src/nodebpy/nodes/compositor/color.py +923 -0
- nodebpy-0.6.0/src/nodebpy/nodes/compositor/converter.py +1044 -0
- nodebpy-0.6.0/src/nodebpy/nodes/compositor/distort.py +844 -0
- nodebpy-0.6.0/src/nodebpy/nodes/compositor/filter.py +1102 -0
- nodebpy-0.6.0/src/nodebpy/nodes/compositor/group.py +21 -0
- nodebpy-0.6.0/src/nodebpy/nodes/compositor/input.py +476 -0
- nodebpy-0.6.0/src/nodebpy/nodes/compositor/interface.py +117 -0
- nodebpy-0.6.0/src/nodebpy/nodes/compositor/manual.py +13 -0
- nodebpy-0.6.0/src/nodebpy/nodes/compositor/matte.py +873 -0
- nodebpy-0.6.0/src/nodebpy/nodes/compositor/output.py +99 -0
- nodebpy-0.6.0/src/nodebpy/nodes/compositor/texture.py +886 -0
- nodebpy-0.6.0/src/nodebpy/nodes/compositor/vector.py +35 -0
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/__init__.py +18 -10
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/attribute.py +16 -13
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/color.py +7 -3
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/converter.py +20 -15
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/geometry.py +112 -55
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/grid.py +67 -25
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/group.py +4 -1
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/input.py +9 -6
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/interface.py +81 -36
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/manual.py +379 -363
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/output.py +3 -1
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/texture.py +6 -3
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/vector.py +5 -2
- {nodebpy-0.4.1/src/nodebpy/nodes → nodebpy-0.6.0/src/nodebpy/nodes/geometry}/zone.py +1 -1
- nodebpy-0.6.0/src/nodebpy/nodes/shader/__init__.py +253 -0
- nodebpy-0.6.0/src/nodebpy/nodes/shader/color.py +187 -0
- nodebpy-0.6.0/src/nodebpy/nodes/shader/converter.py +503 -0
- nodebpy-0.6.0/src/nodebpy/nodes/shader/grid.py +406 -0
- nodebpy-0.6.0/src/nodebpy/nodes/shader/group.py +21 -0
- nodebpy-0.6.0/src/nodebpy/nodes/shader/input.py +898 -0
- nodebpy-0.6.0/src/nodebpy/nodes/shader/interface.py +72 -0
- nodebpy-0.6.0/src/nodebpy/nodes/shader/manual.py +15 -0
- nodebpy-0.6.0/src/nodebpy/nodes/shader/output.py +373 -0
- nodebpy-0.6.0/src/nodebpy/nodes/shader/script.py +74 -0
- nodebpy-0.6.0/src/nodebpy/nodes/shader/shader.py +1557 -0
- nodebpy-0.6.0/src/nodebpy/nodes/shader/texture.py +356 -0
- nodebpy-0.6.0/src/nodebpy/nodes/shader/vector.py +402 -0
- nodebpy-0.6.0/src/nodebpy/nodes/shader/zone.py +58 -0
- nodebpy-0.6.0/src/nodebpy/screenshot.py +228 -0
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/sockets.py +2 -0
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/types.py +4 -7
- nodebpy-0.4.1/src/nodebpy/nodes/experimental.py +0 -318
- nodebpy-0.4.1/src/nodebpy/screenshot.py +0 -532
- nodebpy-0.4.1/src/nodebpy/screenshot_subprocess.py +0 -422
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
- {nodebpy-0.4.1 → nodebpy-0.6.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
- {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
|
-
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.
|
|
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
|
|
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
|
|
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
|
-
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.
|
|
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,
|
|
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 =
|
|
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) ->
|
|
72
|
+
def tree(self) -> NodeTree:
|
|
68
73
|
tree = self.builder.tree
|
|
69
|
-
assert tree is not None
|
|
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
|
|
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:
|
|
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,
|
|
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(
|
|
649
|
+
vector_method(self._default_output_socket, scalar_vector)
|
|
560
650
|
if not reverse
|
|
561
|
-
else vector_method(self._default_output_socket
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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)
|