nodebpy 520.5.0__tar.gz → 520.5.2__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-520.5.2/PKG-INFO +79 -0
- nodebpy-520.5.2/README.md +63 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/pyproject.toml +3 -1
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/builder/tree.py +18 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/export/__init__.py +2 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/export/codegen.py +39 -14
- nodebpy-520.5.2/src/nodebpy/export/web_render.py +113 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/_mixins.py +2 -2
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/compositor/assets.py +5 -5
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/assets.py +5 -5
- nodebpy-520.5.0/PKG-INFO +0 -159
- nodebpy-520.5.0/README.md +0 -143
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/__init__.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/assets/__init__.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/assets/__main__.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/assets/_codegen.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/builder/__init__.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/builder/_registry.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/builder/_utils.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/builder/accessor.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/builder/arrange.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/builder/asset.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/builder/items.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/builder/mixins.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/builder/node.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/builder/socket.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/export/diagram.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/lib/nodearrange/config.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/lib/nodearrange/utils.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/__init__.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/compositor/__init__.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/compositor/color.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/compositor/converter.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/compositor/distort.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/compositor/filter.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/compositor/group.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/compositor/input.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/compositor/interface.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/compositor/manual.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/compositor/matte.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/compositor/output.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/compositor/vector.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/__init__.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/attribute.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/color.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/converter.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/geometry.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/grid.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/group.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/groups.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/input.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/interface.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/manual.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/output.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/texture.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/utilities.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/vector.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/geometry/zone.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/shader/__init__.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/shader/assets.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/shader/color.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/shader/converter.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/shader/grid.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/shader/group.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/shader/input.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/shader/manual.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/shader/output.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/shader/script.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/shader/shader.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/shader/texture.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/nodes/shader/vector.py +0 -0
- {nodebpy-520.5.0 → nodebpy-520.5.2}/src/nodebpy/types.py +0 -0
nodebpy-520.5.2/PKG-INFO
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: nodebpy
|
|
3
|
+
Version: 520.5.2
|
|
4
|
+
Summary: Build nodes trees in Blender more elegantly with code
|
|
5
|
+
Author: Brady Johnston
|
|
6
|
+
Author-email: Brady Johnston <brady.johnston@me.com>
|
|
7
|
+
License: GPL-3.0-or-later
|
|
8
|
+
Requires-Dist: bpy==5.2.* ; extra == 'bpy'
|
|
9
|
+
Requires-Dist: ruff>=0.14.11 ; extra == 'format'
|
|
10
|
+
Requires-Dist: networkx>=3.6.1 ; extra == 'networkx'
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Provides-Extra: bpy
|
|
13
|
+
Provides-Extra: format
|
|
14
|
+
Provides-Extra: networkx
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# nodebpy
|
|
18
|
+
|
|
19
|
+
[](https://github.com/BradyAJohnston/nodebpy/actions/workflows/tests.yml)
|
|
21
|
+
[](https://codecov.io/gh/BradyAJohnston/nodebpy)
|
|
22
|
+
|
|
23
|
+
Build node trees in Blender more elegantly with Python code. Geometry Nodes, Shader Nodes and Compositor nodes are all fully supported, with type hints and IDE auto-completion throughout.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install nodebpy
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> A text-based version of nodes should bring the convenience of writing code with IDE auto-completion, type hinting, with overall compactness and readability, while staying as close as possible to what building a node tree via the GUI feels like.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from nodebpy import geometry as g
|
|
33
|
+
|
|
34
|
+
with g.tree("AnotherTree", collapse=True) as tree:
|
|
35
|
+
rotation = (
|
|
36
|
+
g.RandomValue.vector(min=-1, seed=2)
|
|
37
|
+
>> g.AlignRotationToVector()
|
|
38
|
+
>> g.RotateRotation(rotate_by=g.AxisAngleToRotation(angle=0.3))
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_ = (
|
|
42
|
+
tree.inputs.integer("Count", 10)
|
|
43
|
+
>> g.Points(position=g.RandomValue.vector(min=-1))
|
|
44
|
+
>> g.InstanceOnPoints(instance=g.Cube(), rotation=rotation)
|
|
45
|
+
>> g.SetPosition(
|
|
46
|
+
position=g.Position() * 2.0 + (0, 0.2, 0.3),
|
|
47
|
+
offset=(0, 0, 0.1),
|
|
48
|
+
)
|
|
49
|
+
>> g.RealizeInstances()
|
|
50
|
+
>> g.InstanceOnPoints(g.Cube(), instance=...)
|
|
51
|
+
>> tree.outputs.geometry("Instances")
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
- Every node is a typed class named after the node it creates — 'Random Value' becomes `g.RandomValue()` — and node subtypes are class methods: `g.RandomValue.vector()`.
|
|
58
|
+
- Trees are built inside a `with g.tree(...):` context; instantiating a node class adds it to the current tree, and the tree's interface is declared with `tree.inputs.*` / `tree.outputs.*`.
|
|
59
|
+
- Nodes are linked with the `>>` operator, which picks the most compatible socket pair automatically (much like Node Wrangler's <kbd>Alt</kbd> + <kbd>Right Click</kbd> drag) — or address sockets explicitly through the `.i.*` / `.o.*` accessors.
|
|
60
|
+
- Python's arithmetic, comparison and boolean operators create the matching `Math` / `VectorMath` / `Compare` / `BooleanMath` nodes: `g.Value(1.0) * 2 + (0, 0, 1)`.
|
|
61
|
+
- Existing node trees — including ones wired up by hand in the GUI — can be exported back to `nodebpy` source with `to_python()`.
|
|
62
|
+
|
|
63
|
+
## Documentation
|
|
64
|
+
|
|
65
|
+
Guides and the full node reference live at [bradyajohnston.github.io/nodebpy](https://bradyajohnston.github.io/nodebpy):
|
|
66
|
+
|
|
67
|
+
- [Writing node trees](https://bradyajohnston.github.io/nodebpy/introduction.html) — adding, linking and organising nodes
|
|
68
|
+
- [Math operators](https://bradyajohnston.github.io/nodebpy/operators.html) — arithmetic, comparison and boolean expressions as nodes
|
|
69
|
+
- [Node API design](https://bradyajohnston.github.io/nodebpy/node-api.html) — sockets, accessors, enum options and class methods
|
|
70
|
+
- [Custom node groups](https://bradyajohnston.github.io/nodebpy/custom-node-groups.html) and [asset node groups](https://bradyajohnston.github.io/nodebpy/assets.html) — reusable groups as Python classes
|
|
71
|
+
- [Nodes to code](https://bradyajohnston.github.io/nodebpy/nodes-to-code.html) — turn existing trees back into `nodebpy` code
|
|
72
|
+
- [Using `nodebpy` in your add-on](https://bradyajohnston.github.io/nodebpy/using.html) — installation, bundling and the Blender-tracking versioning scheme
|
|
73
|
+
- [Comparisons](https://bradyajohnston.github.io/nodebpy/comparisons.html) — how the API relates to [`geometry-script`](https://github.com/carson-katri/geometry-script), [`geonodes`](https://github.com/al1brn/geonodes), [NodeToPython](https://github.com/BrendanParmer/NodeToPython) and [TreeClipper](https://github.com/Algebraic-UG/tree_clipper)
|
|
74
|
+
|
|
75
|
+
Like [`databpy`](https://github.com/BradyAJohnston/databpy), this project started as an internal tool inside [`molecularnodes`](https://github.com/BradyAJohnston/MolecularNodes) before being broken out into its own robustly typed and tested package.
|
|
76
|
+
|
|
77
|
+
## Contributing
|
|
78
|
+
|
|
79
|
+
Development setup, running the tests and regenerating the node classes (most of `src/nodebpy/nodes/` is auto-generated by the `gen/` package) are covered in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# nodebpy
|
|
2
|
+
|
|
3
|
+
[](https://github.com/BradyAJohnston/nodebpy/actions/workflows/tests.yml)
|
|
5
|
+
[](https://codecov.io/gh/BradyAJohnston/nodebpy)
|
|
6
|
+
|
|
7
|
+
Build node trees in Blender more elegantly with Python code. Geometry Nodes, Shader Nodes and Compositor nodes are all fully supported, with type hints and IDE auto-completion throughout.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install nodebpy
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
> A text-based version of nodes should bring the convenience of writing code with IDE auto-completion, type hinting, with overall compactness and readability, while staying as close as possible to what building a node tree via the GUI feels like.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from nodebpy import geometry as g
|
|
17
|
+
|
|
18
|
+
with g.tree("AnotherTree", collapse=True) as tree:
|
|
19
|
+
rotation = (
|
|
20
|
+
g.RandomValue.vector(min=-1, seed=2)
|
|
21
|
+
>> g.AlignRotationToVector()
|
|
22
|
+
>> g.RotateRotation(rotate_by=g.AxisAngleToRotation(angle=0.3))
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_ = (
|
|
26
|
+
tree.inputs.integer("Count", 10)
|
|
27
|
+
>> g.Points(position=g.RandomValue.vector(min=-1))
|
|
28
|
+
>> g.InstanceOnPoints(instance=g.Cube(), rotation=rotation)
|
|
29
|
+
>> g.SetPosition(
|
|
30
|
+
position=g.Position() * 2.0 + (0, 0.2, 0.3),
|
|
31
|
+
offset=(0, 0, 0.1),
|
|
32
|
+
)
|
|
33
|
+
>> g.RealizeInstances()
|
|
34
|
+
>> g.InstanceOnPoints(g.Cube(), instance=...)
|
|
35
|
+
>> tree.outputs.geometry("Instances")
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
- Every node is a typed class named after the node it creates — 'Random Value' becomes `g.RandomValue()` — and node subtypes are class methods: `g.RandomValue.vector()`.
|
|
42
|
+
- Trees are built inside a `with g.tree(...):` context; instantiating a node class adds it to the current tree, and the tree's interface is declared with `tree.inputs.*` / `tree.outputs.*`.
|
|
43
|
+
- Nodes are linked with the `>>` operator, which picks the most compatible socket pair automatically (much like Node Wrangler's <kbd>Alt</kbd> + <kbd>Right Click</kbd> drag) — or address sockets explicitly through the `.i.*` / `.o.*` accessors.
|
|
44
|
+
- Python's arithmetic, comparison and boolean operators create the matching `Math` / `VectorMath` / `Compare` / `BooleanMath` nodes: `g.Value(1.0) * 2 + (0, 0, 1)`.
|
|
45
|
+
- Existing node trees — including ones wired up by hand in the GUI — can be exported back to `nodebpy` source with `to_python()`.
|
|
46
|
+
|
|
47
|
+
## Documentation
|
|
48
|
+
|
|
49
|
+
Guides and the full node reference live at [bradyajohnston.github.io/nodebpy](https://bradyajohnston.github.io/nodebpy):
|
|
50
|
+
|
|
51
|
+
- [Writing node trees](https://bradyajohnston.github.io/nodebpy/introduction.html) — adding, linking and organising nodes
|
|
52
|
+
- [Math operators](https://bradyajohnston.github.io/nodebpy/operators.html) — arithmetic, comparison and boolean expressions as nodes
|
|
53
|
+
- [Node API design](https://bradyajohnston.github.io/nodebpy/node-api.html) — sockets, accessors, enum options and class methods
|
|
54
|
+
- [Custom node groups](https://bradyajohnston.github.io/nodebpy/custom-node-groups.html) and [asset node groups](https://bradyajohnston.github.io/nodebpy/assets.html) — reusable groups as Python classes
|
|
55
|
+
- [Nodes to code](https://bradyajohnston.github.io/nodebpy/nodes-to-code.html) — turn existing trees back into `nodebpy` code
|
|
56
|
+
- [Using `nodebpy` in your add-on](https://bradyajohnston.github.io/nodebpy/using.html) — installation, bundling and the Blender-tracking versioning scheme
|
|
57
|
+
- [Comparisons](https://bradyajohnston.github.io/nodebpy/comparisons.html) — how the API relates to [`geometry-script`](https://github.com/carson-katri/geometry-script), [`geonodes`](https://github.com/al1brn/geonodes), [NodeToPython](https://github.com/BrendanParmer/NodeToPython) and [TreeClipper](https://github.com/Algebraic-UG/tree_clipper)
|
|
58
|
+
|
|
59
|
+
Like [`databpy`](https://github.com/BradyAJohnston/databpy), this project started as an internal tool inside [`molecularnodes`](https://github.com/BradyAJohnston/MolecularNodes) before being broken out into its own robustly typed and tested package.
|
|
60
|
+
|
|
61
|
+
## Contributing
|
|
62
|
+
|
|
63
|
+
Development setup, running the tests and regenerating the node classes (most of `src/nodebpy/nodes/` is auto-generated by the `gen/` package) are covered in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nodebpy"
|
|
3
|
-
version = "520.5.
|
|
3
|
+
version = "520.5.2"
|
|
4
4
|
description = "Build nodes trees in Blender more elegantly with code"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -31,6 +31,7 @@ build-backend = "uv_build"
|
|
|
31
31
|
[dependency-groups]
|
|
32
32
|
dev = [
|
|
33
33
|
"ipython>=8.0.0",
|
|
34
|
+
"ipykernel>=6.0.0",
|
|
34
35
|
"networkx>=3.6.1",
|
|
35
36
|
"fake-bpy-module>=20260501",
|
|
36
37
|
"jsondiff>=2.2.1",
|
|
@@ -38,6 +39,7 @@ dev = [
|
|
|
38
39
|
"pytest-cov>=7.0.0",
|
|
39
40
|
"quarto-cli>=1.9",
|
|
40
41
|
"quartodoc>=0.11.1",
|
|
42
|
+
"tree-clipper>=0.1.6",
|
|
41
43
|
"ruff>=0.14.11",
|
|
42
44
|
"syrupy>=5.0.0",
|
|
43
45
|
"pytest-xdist>=3.8.0",
|
|
@@ -15,6 +15,7 @@ from bpy.types import (
|
|
|
15
15
|
ShaderNodeTree,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
+
|
|
18
19
|
from ..types import (
|
|
19
20
|
SOCKET_COMPATIBILITY,
|
|
20
21
|
FloatInterfaceSubtypes,
|
|
@@ -846,6 +847,23 @@ class TreeBuilder(Generic[_TreeT]):
|
|
|
846
847
|
print(f"Mermaid diagram generation failed: {e}")
|
|
847
848
|
return None
|
|
848
849
|
|
|
850
|
+
def _repr_html_(self) -> str | None:
|
|
851
|
+
"""
|
|
852
|
+
Return an interactive geonodes-web-render graph for Jupyter/Quarto.
|
|
853
|
+
|
|
854
|
+
This is called when the TreeBuilder is the return value of a cell. The
|
|
855
|
+
tree is exported to the Tree Clipper format and embedded as a Blender-
|
|
856
|
+
styled, pan/zoomable graph. Returning ``None`` on failure lets the
|
|
857
|
+
Mermaid ``_repr_markdown_`` fallback take over.
|
|
858
|
+
"""
|
|
859
|
+
try:
|
|
860
|
+
from ..export import to_web_render_html
|
|
861
|
+
|
|
862
|
+
return to_web_render_html(self)
|
|
863
|
+
except Exception as e:
|
|
864
|
+
print(f"Web render generation failed: {e}")
|
|
865
|
+
return None
|
|
866
|
+
|
|
849
867
|
def _input_node(self) -> Node:
|
|
850
868
|
"""Get or create the Group Input node."""
|
|
851
869
|
try:
|
|
@@ -1057,20 +1057,25 @@ class EmitContext:
|
|
|
1057
1057
|
self.used_aliases.add(alias)
|
|
1058
1058
|
|
|
1059
1059
|
by_socket: dict[str, list[tuple[int, Expr]]] = {}
|
|
1060
|
+
multi_inputs: set[str] = set()
|
|
1060
1061
|
for link in self.incoming.get(node.name, ()):
|
|
1061
1062
|
if skip_input_id and link.to_socket.identifier == skip_input_id:
|
|
1062
1063
|
continue
|
|
1063
|
-
|
|
1064
|
+
name = _normalize(link.to_socket.identifier)
|
|
1065
|
+
if getattr(link.to_socket, "is_multi_input", False):
|
|
1066
|
+
multi_inputs.add(name)
|
|
1067
|
+
by_socket.setdefault(name, []).append(
|
|
1064
1068
|
(link.sort_id, self.upstream_expr(link))
|
|
1065
1069
|
)
|
|
1066
1070
|
socket_kwargs: dict[str, Expr] = {}
|
|
1067
1071
|
for name, entries in by_socket.items():
|
|
1068
|
-
if len(entries) == 1:
|
|
1072
|
+
if len(entries) == 1 and name not in multi_inputs:
|
|
1069
1073
|
socket_kwargs[name] = entries[0][1]
|
|
1070
1074
|
else:
|
|
1071
|
-
#
|
|
1072
|
-
#
|
|
1073
|
-
#
|
|
1075
|
+
# Links on a multi-input socket become a tuple in creation
|
|
1076
|
+
# order (descending sort_id) so the rebuilt tree gets the same
|
|
1077
|
+
# multi-input ordering — even a single link, since manual
|
|
1078
|
+
# classes (JoinGeometry, MeshBoolean, …) expect an iterable.
|
|
1074
1079
|
entries.sort(key=lambda e: e[0], reverse=True)
|
|
1075
1080
|
socket_kwargs[name] = TupleExpr([e[1] for e in entries])
|
|
1076
1081
|
|
|
@@ -1384,7 +1389,7 @@ def _non_default_props(node, cls: type) -> dict[str, Any]:
|
|
|
1384
1389
|
# (``Compare.float.less_than``). Parsing those bodies at runtime yields a
|
|
1385
1390
|
# reverse table from node state to factory path, so generated code uses the
|
|
1386
1391
|
# same idiomatic spellings users write — with no separate table to keep in
|
|
1387
|
-
# sync with
|
|
1392
|
+
# sync with the generator. Factories whose bodies are not statically analysable
|
|
1388
1393
|
# (positional args, helper indirection, **kwargs) are simply skipped and the
|
|
1389
1394
|
# node falls back to the plain constructor.
|
|
1390
1395
|
# ---------------------------------------------------------------------------
|
|
@@ -1817,7 +1822,9 @@ def _matrix_spec(method: str, output: str) -> SocketMethodSpec:
|
|
|
1817
1822
|
)
|
|
1818
1823
|
|
|
1819
1824
|
|
|
1820
|
-
def _math_unary_spec(
|
|
1825
|
+
def _math_unary_spec(
|
|
1826
|
+
operation: str, method: str, *args: str, always_args: int = 0
|
|
1827
|
+
) -> SocketMethodSpec:
|
|
1821
1828
|
"""A unary ``ShaderNodeMath`` op rendered as a float socket method —
|
|
1822
1829
|
``value.sqrt()`` / ``value.sign()``. ``receiver_socket_type="VALUE"`` keeps
|
|
1823
1830
|
the round-trip faithful: the method only re-derives a float Math node when
|
|
@@ -1826,14 +1833,17 @@ def _math_unary_spec(operation: str, method: str, *args: str) -> SocketMethodSpe
|
|
|
1826
1833
|
receiver="Value",
|
|
1827
1834
|
method=method,
|
|
1828
1835
|
output="Value",
|
|
1829
|
-
params=tuple((f"Value_{
|
|
1836
|
+
params=tuple((f"Value_{i + 1:03d}", arg) for i, arg in enumerate(args)),
|
|
1830
1837
|
require=(("operation", operation),),
|
|
1831
1838
|
consumed_props=("operation",),
|
|
1832
1839
|
receiver_socket_type="VALUE",
|
|
1840
|
+
always_args=always_args,
|
|
1833
1841
|
)
|
|
1834
1842
|
|
|
1835
1843
|
|
|
1836
|
-
def _int_math_unary_spec(
|
|
1844
|
+
def _int_math_unary_spec(
|
|
1845
|
+
operation: str, method: str, *args: str, always_args: int = 0
|
|
1846
|
+
) -> SocketMethodSpec:
|
|
1837
1847
|
"""A unary ``FunctionNodeIntegerMath`` op rendered as an integer socket
|
|
1838
1848
|
method — ``value.sign()``. ``ABSOLUTE``/``NEGATE`` stay as the ``abs(x)`` /
|
|
1839
1849
|
``-x`` lifts and so are deliberately absent here."""
|
|
@@ -1841,10 +1851,11 @@ def _int_math_unary_spec(operation: str, method: str, *args: str) -> SocketMetho
|
|
|
1841
1851
|
receiver="Value",
|
|
1842
1852
|
method=method,
|
|
1843
1853
|
output="Value",
|
|
1844
|
-
params=tuple((f"Value_{
|
|
1854
|
+
params=tuple((f"Value_{i + 1:03d}", arg) for i, arg in enumerate(args)),
|
|
1845
1855
|
require=(("operation", operation),),
|
|
1846
1856
|
consumed_props=("operation",),
|
|
1847
1857
|
receiver_socket_type="INT",
|
|
1858
|
+
always_args=always_args,
|
|
1848
1859
|
)
|
|
1849
1860
|
|
|
1850
1861
|
|
|
@@ -2033,12 +2044,26 @@ _SOCKET_METHODS: dict[str, list[SocketMethodSpec]] = {
|
|
|
2033
2044
|
("MINIMUM", "min", "value_001"),
|
|
2034
2045
|
("MAXIMUM", "max", "value_001"),
|
|
2035
2046
|
("MULTIPLY_ADD", "mul_add", "multiplier", "addend"),
|
|
2036
|
-
("WRAP", "wrap", "min", "max"),
|
|
2037
|
-
("MODULO", "modulo", "divisor"),
|
|
2038
2047
|
("PINGPONG", "ping_pong", "value"),
|
|
2039
2048
|
("LOGARITHM", "log", "base"),
|
|
2040
2049
|
("ARCTAN2", "atan2", "value"),
|
|
2041
2050
|
]
|
|
2051
|
+
]
|
|
2052
|
+
# No float modulo lift: the float mixin's modulo() builds FLOORED_MODULO,
|
|
2053
|
+
# which the % operator lift already covers, and plain MODULO has no
|
|
2054
|
+
# faithful method equivalent so it stays a factory call.
|
|
2055
|
+
+ [
|
|
2056
|
+
# wrap()'s node sockets are swapped relative to the signature: the
|
|
2057
|
+
# node's Value_001 is Max, Value_002 is Min.
|
|
2058
|
+
SocketMethodSpec(
|
|
2059
|
+
receiver="Value",
|
|
2060
|
+
method="wrap",
|
|
2061
|
+
output="Value",
|
|
2062
|
+
params=(("Value_002", "min"), ("Value_001", "max")),
|
|
2063
|
+
require=(("operation", "WRAP"),),
|
|
2064
|
+
consumed_props=("operation",),
|
|
2065
|
+
receiver_socket_type="VALUE",
|
|
2066
|
+
),
|
|
2042
2067
|
],
|
|
2043
2068
|
"FunctionNodeIntegerMath": [
|
|
2044
2069
|
_int_math_unary_spec(*args)
|
|
@@ -2046,9 +2071,9 @@ _SOCKET_METHODS: dict[str, list[SocketMethodSpec]] = {
|
|
|
2046
2071
|
("SIGN", "sign"),
|
|
2047
2072
|
# ("ABSOLUTE", "abs"),
|
|
2048
2073
|
("MULTIPLY_ADD", "mul_add", "multiplier", "addend"),
|
|
2049
|
-
("MODULO", "modulo", "divisor"),
|
|
2050
2074
|
)
|
|
2051
|
-
]
|
|
2075
|
+
]
|
|
2076
|
+
+ [_int_math_unary_spec("MODULO", "modulo", "divisor", always_args=1)],
|
|
2052
2077
|
"GeometryNodeListLength": [
|
|
2053
2078
|
_list_spec("list_length", "Length", type_prop="data_type"),
|
|
2054
2079
|
],
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Render node trees as interactive graphs via geonodes-web-render.
|
|
3
|
+
|
|
4
|
+
The live Blender node tree built by a :class:`~nodebpy.builder.tree.TreeBuilder`
|
|
5
|
+
is exported to the Tree Clipper JSON format (using the ``tree_clipper`` package)
|
|
6
|
+
and embedded into a small HTML snippet that mounts the ``geonodes-web-render``
|
|
7
|
+
web component in the browser. This is what powers the rich ``_repr_html_`` shown
|
|
8
|
+
for a tree in Jupyter and Quarto.
|
|
9
|
+
|
|
10
|
+
Nothing here needs network access at build time: the Tree Clipper payload is
|
|
11
|
+
inlined into the HTML, and only the (cached) JS/CSS are fetched from the CDN
|
|
12
|
+
when the page is viewed.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import uuid
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from ..builder.tree import TreeBuilder
|
|
23
|
+
|
|
24
|
+
#: geonodes-web-render version served from the CDN. A semver range (`"0.3"`)
|
|
25
|
+
#: lets esm.sh serve the latest matching patch, so web-render bug fixes ship to
|
|
26
|
+
#: the docs without re-pinning here. Use an exact version (e.g. "0.3.2") to lock.
|
|
27
|
+
DEFAULT_VERSION = "0.3"
|
|
28
|
+
|
|
29
|
+
#: Default ESM CDN. esm.sh transparently resolves the external react/react-dom.
|
|
30
|
+
DEFAULT_CDN = "https://esm.sh"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def to_tree_clipper_payload(builder: "TreeBuilder", *, compress: bool = True) -> str:
|
|
34
|
+
"""Export *builder*'s node tree to a Tree Clipper payload.
|
|
35
|
+
|
|
36
|
+
With ``compress=True`` (the default) the result is the ``TreeClipper::`` +
|
|
37
|
+
base64(gzip(json)) string consumed directly by geonodes-web-render. With
|
|
38
|
+
``compress=False`` it is indented JSON, handy for inspection.
|
|
39
|
+
|
|
40
|
+
Requires the optional ``tree_clipper`` package to be importable.
|
|
41
|
+
"""
|
|
42
|
+
from tree_clipper.export_nodes import ExportIntermediate, ExportParameters
|
|
43
|
+
from tree_clipper.specific_handlers import BUILT_IN_EXPORTER
|
|
44
|
+
|
|
45
|
+
# A MaterialBuilder owns a bpy.types.Material; a plain TreeBuilder owns a
|
|
46
|
+
# node group. Tree Clipper looks the root up by name in the matching
|
|
47
|
+
# collection, so use the live name (Blender may have de-duplicated it).
|
|
48
|
+
material = getattr(builder, "material", None)
|
|
49
|
+
is_material = material is not None
|
|
50
|
+
name = material.name if is_material else builder.tree.name
|
|
51
|
+
|
|
52
|
+
export = ExportIntermediate(
|
|
53
|
+
parameters=ExportParameters(
|
|
54
|
+
is_material=is_material,
|
|
55
|
+
name=name,
|
|
56
|
+
# BUILT_IN_EXPORTER is a dict literal upstream, so ty infers its key
|
|
57
|
+
# type as NoneType (invariant dict) rather than the runtime `type`.
|
|
58
|
+
specific_handlers=BUILT_IN_EXPORTER, # ty: ignore[invalid-argument-type]
|
|
59
|
+
export_sub_trees=True,
|
|
60
|
+
debug_prints=False,
|
|
61
|
+
write_from_roots=False,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
while export.step():
|
|
65
|
+
pass
|
|
66
|
+
export.set_external(
|
|
67
|
+
# get_pointee() is typed as bare bpy_struct upstream; external items are
|
|
68
|
+
# always ID datablocks, which do carry a `.name`.
|
|
69
|
+
(external_id, item.pointed_to_by.get_pointee().name) # ty: ignore[unresolved-attribute]
|
|
70
|
+
for external_id, item in export.get_external().items()
|
|
71
|
+
)
|
|
72
|
+
return export.export_to_str(compress=compress, json_indent=4)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def to_web_render_html(
|
|
76
|
+
builder: "TreeBuilder",
|
|
77
|
+
*,
|
|
78
|
+
height: str = "480px",
|
|
79
|
+
version: str = DEFAULT_VERSION,
|
|
80
|
+
cdn: str = DEFAULT_CDN,
|
|
81
|
+
) -> str:
|
|
82
|
+
"""Return a self-contained HTML snippet rendering *builder* as a graph.
|
|
83
|
+
|
|
84
|
+
The snippet mounts one geonodes-web-render instance into its own container,
|
|
85
|
+
so multiple trees can be shown on the same page. The stylesheet is added to
|
|
86
|
+
``<head>`` only once across all snippets.
|
|
87
|
+
"""
|
|
88
|
+
payload = to_tree_clipper_payload(builder, compress=True)
|
|
89
|
+
container_id = f"gnwr-{uuid.uuid4().hex}"
|
|
90
|
+
module_url = f"{cdn}/geonodes-web-render@{version}/embed"
|
|
91
|
+
css_url = f"{cdn}/geonodes-web-render@{version}/dist/embed.css"
|
|
92
|
+
# json.dumps keeps the payload a valid, safely-quoted JS string literal.
|
|
93
|
+
payload_js = json.dumps(payload)
|
|
94
|
+
css_id = "gnwr-stylesheet"
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
f'<div id="{container_id}" '
|
|
98
|
+
f'style="height: {height}; width: 100%; border-radius: 8px; overflow: hidden;">'
|
|
99
|
+
f"</div>\n"
|
|
100
|
+
f'<script type="module">\n'
|
|
101
|
+
f'if (!document.getElementById("{css_id}")) {{\n'
|
|
102
|
+
f' const link = document.createElement("link");\n'
|
|
103
|
+
f' link.id = "{css_id}";\n'
|
|
104
|
+
f' link.rel = "stylesheet";\n'
|
|
105
|
+
f' link.href = "{css_url}";\n'
|
|
106
|
+
f" document.head.appendChild(link);\n"
|
|
107
|
+
f"}}\n"
|
|
108
|
+
f'import {{ mountGraphView }} from "{module_url}";\n'
|
|
109
|
+
f'mountGraphView(document.getElementById("{container_id}"), '
|
|
110
|
+
f"{{ payload: {payload_js}, showCopyButton: false, "
|
|
111
|
+
f"allowSelection: false }});\n"
|
|
112
|
+
f"</script>"
|
|
113
|
+
)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Hand-written mixins attached to auto-generated node classes.
|
|
2
2
|
|
|
3
3
|
These hold reusable behaviour that the code generator cannot derive on its own
|
|
4
|
-
(ergonomic flag accessors, items helpers, …). ``
|
|
5
|
-
the generated classes via :class:`~
|
|
4
|
+
(ergonomic flag accessors, items helpers, …). The ``gen`` package wires them
|
|
5
|
+
onto the generated classes via :class:`~gen.NodeCustomization`, so the bulky
|
|
6
6
|
boilerplate (sockets, docstrings, property accessors) stays generated while the
|
|
7
7
|
bespoke behaviour lives here.
|
|
8
8
|
"""
|
|
@@ -144,8 +144,8 @@ class FilmGrain(AssetCompositorGroup):
|
|
|
144
144
|
"""Factor"""
|
|
145
145
|
preset: MenuSocket
|
|
146
146
|
"""Preset"""
|
|
147
|
-
|
|
148
|
-
"""Animated
|
|
147
|
+
animated: BooleanSocket
|
|
148
|
+
"""Animated"""
|
|
149
149
|
|
|
150
150
|
class _Outputs(SocketAccessor):
|
|
151
151
|
result: ColorSocket
|
|
@@ -162,15 +162,15 @@ class FilmGrain(AssetCompositorGroup):
|
|
|
162
162
|
self,
|
|
163
163
|
input: InputColor = None,
|
|
164
164
|
factor: InputFloat = 1.0,
|
|
165
|
-
preset: InputMenu = "Super
|
|
166
|
-
|
|
165
|
+
preset: InputMenu = "Super 8 mm",
|
|
166
|
+
animated: InputBoolean = False,
|
|
167
167
|
):
|
|
168
168
|
super().__init__(
|
|
169
169
|
**{
|
|
170
170
|
"Socket_1": input,
|
|
171
171
|
"Socket_7": factor,
|
|
172
172
|
"Socket_64": preset,
|
|
173
|
-
"Socket_10":
|
|
173
|
+
"Socket_10": animated,
|
|
174
174
|
}
|
|
175
175
|
)
|
|
176
176
|
|
|
@@ -200,7 +200,7 @@ class AttachmentInfo(AssetGeometryGroup):
|
|
|
200
200
|
attachment_uv: VectorSocket
|
|
201
201
|
"""Attachment UV"""
|
|
202
202
|
attachment_is_valid: BooleanSocket
|
|
203
|
-
"""Attachment
|
|
203
|
+
"""Attachment Is Valid"""
|
|
204
204
|
surface_normal: VectorSocket
|
|
205
205
|
"""Surface Normal"""
|
|
206
206
|
|
|
@@ -2137,8 +2137,8 @@ class ProjectWithDepth(AssetGeometryGroup):
|
|
|
2137
2137
|
"""Projection"""
|
|
2138
2138
|
transform: MatrixSocket
|
|
2139
2139
|
"""Transform"""
|
|
2140
|
-
|
|
2141
|
-
"""
|
|
2140
|
+
clip_start: FloatSocket
|
|
2141
|
+
"""Clip Start"""
|
|
2142
2142
|
clip_end: FloatSocket
|
|
2143
2143
|
"""Clip End"""
|
|
2144
2144
|
|
|
@@ -2159,7 +2159,7 @@ class ProjectWithDepth(AssetGeometryGroup):
|
|
|
2159
2159
|
depth: InputFloat = 1.0,
|
|
2160
2160
|
projection: InputMatrix = None,
|
|
2161
2161
|
transform: InputMatrix = None,
|
|
2162
|
-
|
|
2162
|
+
clip_start: InputFloat = 0.1,
|
|
2163
2163
|
clip_end: InputFloat = 100.0,
|
|
2164
2164
|
):
|
|
2165
2165
|
super().__init__(
|
|
@@ -2168,7 +2168,7 @@ class ProjectWithDepth(AssetGeometryGroup):
|
|
|
2168
2168
|
"Socket_2": depth,
|
|
2169
2169
|
"Socket_5": projection,
|
|
2170
2170
|
"Socket_6": transform,
|
|
2171
|
-
"Socket_0":
|
|
2171
|
+
"Socket_0": clip_start,
|
|
2172
2172
|
"Socket_1": clip_end,
|
|
2173
2173
|
}
|
|
2174
2174
|
)
|
nodebpy-520.5.0/PKG-INFO
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: nodebpy
|
|
3
|
-
Version: 520.5.0
|
|
4
|
-
Summary: Build nodes trees in Blender more elegantly with code
|
|
5
|
-
Author: Brady Johnston
|
|
6
|
-
Author-email: Brady Johnston <brady.johnston@me.com>
|
|
7
|
-
License: GPL-3.0-or-later
|
|
8
|
-
Requires-Dist: bpy==5.2.* ; extra == 'bpy'
|
|
9
|
-
Requires-Dist: ruff>=0.14.11 ; extra == 'format'
|
|
10
|
-
Requires-Dist: networkx>=3.6.1 ; extra == 'networkx'
|
|
11
|
-
Requires-Python: >=3.13
|
|
12
|
-
Provides-Extra: bpy
|
|
13
|
-
Provides-Extra: format
|
|
14
|
-
Provides-Extra: networkx
|
|
15
|
-
Description-Content-Type: text/markdown
|
|
16
|
-
|
|
17
|
-
# nodebpy
|
|
18
|
-
|
|
19
|
-
[](https://github.com/BradyAJohnston/nodebpy/actions/workflows/tests.yml)
|
|
21
|
-
[](https://codecov.io/gh/BradyAJohnston/nodebpy)
|
|
22
|
-
|
|
23
|
-
A package to build node trees in blender more elegantly with python code. Geometry Nodes, Shader Nodes and Compositor nodes are all fully supported. Look at the [documentation](https://bradyajohnston.github.io/nodebpy) for more examples.
|
|
24
|
-
|
|
25
|
-
## Creating Nodes With Code
|
|
26
|
-
|
|
27
|
-
> A text-based version of nodes should bring the convenience of writing code with IDE auto-completion, type hinting, with overall compactness and readability, while staying as close as possible to what building a node tree via the GUI feels like.
|
|
28
|
-
|
|
29
|
-
```python
|
|
30
|
-
from nodebpy import geometry as g
|
|
31
|
-
|
|
32
|
-
with g.tree("AnotherTree", collapse=True) as tree:
|
|
33
|
-
rotation = (
|
|
34
|
-
g.RandomValue.vector(min=-1, seed=2)
|
|
35
|
-
>> g.AlignRotationToVector()
|
|
36
|
-
>> g.RotateRotation(rotate_by=g.AxisAngleToRotation(angle=0.3))
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
_ = (
|
|
40
|
-
tree.inputs.integer("Count", 10)
|
|
41
|
-
>> g.Points(position=g.RandomValue.vector(min=-1))
|
|
42
|
-
>> g.InstanceOnPoints(instance=g.Cube(), rotation=rotation)
|
|
43
|
-
>> g.SetPosition(
|
|
44
|
-
position=g.Position() * 2.0 + (0, 0.2, 0.3),
|
|
45
|
-
offset=(0, 0, 0.1),
|
|
46
|
-
)
|
|
47
|
-
>> g.RealizeInstances()
|
|
48
|
-
>> g.InstanceOnPoints(g.Cube(), instance=...)
|
|
49
|
-
>> tree.outputs.geometry("Instances")
|
|
50
|
-
)
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-

|
|
54
|
-
|
|
55
|
-
Nodes are created by instantiating their classes. The node tree they are added to is determined by the context the code is executed in (while inside `with g.tree():`). The interface for the node tree is created with `tree.inputs` and `tree.inputs`, adding the sockets and returning the input or output socket for linking with other nodes.
|
|
56
|
-
|
|
57
|
-
Nodes are linked by overloading the `>>` operator, to link from the previous node on the left to the input on the right. Suitable socket pairs are automatically selected or explicitly supplied.
|
|
58
|
-
|
|
59
|
-
The layout / arrangement of the node tree is not important to Blender's evaluation of it - but an automatic layout algorithm is potentially applied upon exiting the node tree context.
|
|
60
|
-
|
|
61
|
-
### `nodebpy` and the `>>` operator
|
|
62
|
-
|
|
63
|
-
In `nodebpy` we use the `>>` operator to link from one node or socket into another.
|
|
64
|
-
This should feel and behave much like the <kbd>Alt</kbd> + <kbd>Right Click</kbd> drag between nodes in [Node Wrangler](https://docs.blender.org/manual/en/latest/addons/node/node_wrangler.html). It will use some smart logic to match the most compatible sockets between the nodes, but if you ever want to be explicit you do so. The input and output sockets of a node are accessible as properties via the `i.*` and `o.*` prefixes, or you can use the `...` placeholder to specify the particular input to be user, or pass in the previous node as a named argument.
|
|
65
|
-
|
|
66
|
-
```py
|
|
67
|
-
# vector output will be linked into the first vector input (position)
|
|
68
|
-
g.Vector() >> g.SetPosition()
|
|
69
|
-
# vector output will be linked into the offset input
|
|
70
|
-
g.Vector() >> g.SetPosition(offset=...)
|
|
71
|
-
g.SetPosition(offset=g.Vector())
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
The `>>` operator will always look for the _most_ compatible sockets first (matching data types) before looking for other compatible but not identical socket data types to link.
|
|
75
|
-
If a compatible match can't be found an error _will_ be thrown.
|
|
76
|
-
|
|
77
|
-
### Contexts
|
|
78
|
-
|
|
79
|
-
What node tree or node tree interface we are currently editing is determined based on contexts.
|
|
80
|
-
Instantiating a node class outside of a tree context will throw an error. The easiest way to enter and exit a tree context is to use the `with` statement.
|
|
81
|
-
|
|
82
|
-
Each time you instantiate a node class, a new node will be created and added to the current tree.
|
|
83
|
-
If these nodes are given as arguments to other nodes or used with the `>>` operator, they will be automatically linked to the appropriate sockets.
|
|
84
|
-
|
|
85
|
-
## Nodes
|
|
86
|
-
|
|
87
|
-
Documentation for all of the nodes can be found in the [API Reference](https://bradyajohnston.github.io/nodebpy/reference/). This is mostly built automatically from the existing Blender node classes.
|
|
88
|
-
|
|
89
|
-
Every node has all of it's input sockets and enum options exposed as arguments to the class constructor. Input sockets are prefixed with `.i.*` and output sockets are prefixed with `.o.*`. Properties that aren't exposed as sockets are available as class properties. Many properties are also available as class methods for convenience when constructing.
|
|
90
|
-
|
|
91
|
-
The basic math operators also automatically add relevant nodes with their operations and values set.
|
|
92
|
-
|
|
93
|
-
```py
|
|
94
|
-
# operation is exposed as a property
|
|
95
|
-
math = g.Math(1.0, 2.0, operation='ADD')
|
|
96
|
-
math.operation = "SUBTRACT"
|
|
97
|
-
|
|
98
|
-
# operation can be chose as a class method
|
|
99
|
-
math = g.Math.subtract(1.0, 2.0)
|
|
100
|
-
math = g.Value(1.0) - 2.0
|
|
101
|
-
math = g.Math.add(1.0, 2.0)
|
|
102
|
-
math = g.Value(1.0) + 2.0
|
|
103
|
-
# the 3.0 + 2.0 is evaluated as regular python code first,
|
|
104
|
-
# so the result with be a Math.add(g.Value(1.0), 5.0)
|
|
105
|
-
math = g.Value(1.0) 3.0 + 2.0
|
|
106
|
-
|
|
107
|
-
# these are equivalent, the g.Math.multiply is automatically added
|
|
108
|
-
g.Value(1.0) * 2
|
|
109
|
-
g.Math.multiply(g.Value(1.0), 2.0)
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
# Design Considerations
|
|
113
|
-
|
|
114
|
-
The top priority of `nodebpy` has been type hinting and IDE auto-complete.
|
|
115
|
-
Typical tooling that supports autoring regular python code should also support the authoring of node trees.
|
|
116
|
-
Much like [`databpy`](https://github.com/BradyAJohnston/databpy), this started as an internal tool used inside of [`molecularnodes`](https://github.com/BradyAJohnston/molecularnodes) but has since been broken out into it's own separate project.
|
|
117
|
-
This projects is robustly typed and tested, with the intent that it can be used internally for multiple other add-ons and projects.
|
|
118
|
-
|
|
119
|
-
- Node classes are named after nodes 'Random Value' -> `RandomValue()`
|
|
120
|
-
- Node 'subtypes' and methods should be accessible via dot (`.`) for easier IDE auto-complete and authoring:
|
|
121
|
-
- `RandomValue(data_type="FLOAT_VECTOR")` -> `RandomValue.vector()`
|
|
122
|
-
- Node properties are available on the top level, with inputs and outputs available behind `.i.*` and `.o.*` accessors
|
|
123
|
-
- Inputs and outputs from a node are prefixed with `i.*` and `o.*`:
|
|
124
|
-
- `AccumulateField().o.total` returns the output `Total` socket
|
|
125
|
-
- `AccumulateField().i.value` returns the input `Value` socket
|
|
126
|
-
|
|
127
|
-
## Building
|
|
128
|
-
|
|
129
|
-
Most of the code for classes are generated automatically with the `generate.py` script.
|
|
130
|
-
Some nodes are manually specified in the `src/nodebpy/nodes/geometry/manual.py` if they require special handling.
|
|
131
|
-
|
|
132
|
-
Run the build & format script as such:
|
|
133
|
-
|
|
134
|
-
```bash
|
|
135
|
-
uv run generate.py && uvx ruff format && uvx ruff check --fix && uvx ty check --fix src
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
## Other Projects
|
|
139
|
-
|
|
140
|
-
There are several other notable projects which have also attempted interfacing with node trees via code. They mostly fit into two categories of either storing & retrieving node trees via code (`.json` or the `bpy` API), or authoring of node trees with custom API and syntax. This project mostly fits in to the latter category.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
### Storing node trees as code:
|
|
145
|
-
Converting node trees to the python API calls or `.json` is great to have a robust storage method, but this approach falls down in human authorability / readability. These projects are great for storage but less useful when wanting to write / generate node trees froms scratch.
|
|
146
|
-
|
|
147
|
-
- [NodeToPython](https://github.com/BrendanParmer/NodeToPython)
|
|
148
|
-
- [TreeClipper](https://github.com/Algebraic-UG/tree_clipper) (used by this project for running tests & snapshots)
|
|
149
|
-
|
|
150
|
-
### Authoring node trees with code:
|
|
151
|
-
Two previous projects have made similar approaches to authoring node trees. `geometry-script` also auto-generated most of it's type hinting, code and docs. It uses the approach of method chaining with the `.` operator, but obfuscates some of the non-linear way of building node trees.
|
|
152
|
-
|
|
153
|
-
The other project `geonodes` uses a similar context system for creating and authoring node trees, but doesn't use the same method of exposing each individual node as it's own class that `nodebpy` does.
|
|
154
|
-
|
|
155
|
-
I personally found both of their APIs to _not quite_ fit how I wanted to work, leading to the creation of `nodebpy`.
|
|
156
|
-
In comparison, this project is also the only one that is also distributed on `PyPi` and insallable via `pip` for easier use in other projects.
|
|
157
|
-
|
|
158
|
-
- [geometry-script](https://github.com/carson-katri/geometry-script),
|
|
159
|
-
- [geonodes](https://github.com/al1brn/geonodes)
|
nodebpy-520.5.0/README.md
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
# nodebpy
|
|
2
|
-
|
|
3
|
-
[](https://github.com/BradyAJohnston/nodebpy/actions/workflows/tests.yml)
|
|
5
|
-
[](https://codecov.io/gh/BradyAJohnston/nodebpy)
|
|
6
|
-
|
|
7
|
-
A package to build node trees in blender more elegantly with python code. Geometry Nodes, Shader Nodes and Compositor nodes are all fully supported. Look at the [documentation](https://bradyajohnston.github.io/nodebpy) for more examples.
|
|
8
|
-
|
|
9
|
-
## Creating Nodes With Code
|
|
10
|
-
|
|
11
|
-
> A text-based version of nodes should bring the convenience of writing code with IDE auto-completion, type hinting, with overall compactness and readability, while staying as close as possible to what building a node tree via the GUI feels like.
|
|
12
|
-
|
|
13
|
-
```python
|
|
14
|
-
from nodebpy import geometry as g
|
|
15
|
-
|
|
16
|
-
with g.tree("AnotherTree", collapse=True) as tree:
|
|
17
|
-
rotation = (
|
|
18
|
-
g.RandomValue.vector(min=-1, seed=2)
|
|
19
|
-
>> g.AlignRotationToVector()
|
|
20
|
-
>> g.RotateRotation(rotate_by=g.AxisAngleToRotation(angle=0.3))
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
_ = (
|
|
24
|
-
tree.inputs.integer("Count", 10)
|
|
25
|
-
>> g.Points(position=g.RandomValue.vector(min=-1))
|
|
26
|
-
>> g.InstanceOnPoints(instance=g.Cube(), rotation=rotation)
|
|
27
|
-
>> g.SetPosition(
|
|
28
|
-
position=g.Position() * 2.0 + (0, 0.2, 0.3),
|
|
29
|
-
offset=(0, 0, 0.1),
|
|
30
|
-
)
|
|
31
|
-
>> g.RealizeInstances()
|
|
32
|
-
>> g.InstanceOnPoints(g.Cube(), instance=...)
|
|
33
|
-
>> tree.outputs.geometry("Instances")
|
|
34
|
-
)
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-

|
|
38
|
-
|
|
39
|
-
Nodes are created by instantiating their classes. The node tree they are added to is determined by the context the code is executed in (while inside `with g.tree():`). The interface for the node tree is created with `tree.inputs` and `tree.inputs`, adding the sockets and returning the input or output socket for linking with other nodes.
|
|
40
|
-
|
|
41
|
-
Nodes are linked by overloading the `>>` operator, to link from the previous node on the left to the input on the right. Suitable socket pairs are automatically selected or explicitly supplied.
|
|
42
|
-
|
|
43
|
-
The layout / arrangement of the node tree is not important to Blender's evaluation of it - but an automatic layout algorithm is potentially applied upon exiting the node tree context.
|
|
44
|
-
|
|
45
|
-
### `nodebpy` and the `>>` operator
|
|
46
|
-
|
|
47
|
-
In `nodebpy` we use the `>>` operator to link from one node or socket into another.
|
|
48
|
-
This should feel and behave much like the <kbd>Alt</kbd> + <kbd>Right Click</kbd> drag between nodes in [Node Wrangler](https://docs.blender.org/manual/en/latest/addons/node/node_wrangler.html). It will use some smart logic to match the most compatible sockets between the nodes, but if you ever want to be explicit you do so. The input and output sockets of a node are accessible as properties via the `i.*` and `o.*` prefixes, or you can use the `...` placeholder to specify the particular input to be user, or pass in the previous node as a named argument.
|
|
49
|
-
|
|
50
|
-
```py
|
|
51
|
-
# vector output will be linked into the first vector input (position)
|
|
52
|
-
g.Vector() >> g.SetPosition()
|
|
53
|
-
# vector output will be linked into the offset input
|
|
54
|
-
g.Vector() >> g.SetPosition(offset=...)
|
|
55
|
-
g.SetPosition(offset=g.Vector())
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
The `>>` operator will always look for the _most_ compatible sockets first (matching data types) before looking for other compatible but not identical socket data types to link.
|
|
59
|
-
If a compatible match can't be found an error _will_ be thrown.
|
|
60
|
-
|
|
61
|
-
### Contexts
|
|
62
|
-
|
|
63
|
-
What node tree or node tree interface we are currently editing is determined based on contexts.
|
|
64
|
-
Instantiating a node class outside of a tree context will throw an error. The easiest way to enter and exit a tree context is to use the `with` statement.
|
|
65
|
-
|
|
66
|
-
Each time you instantiate a node class, a new node will be created and added to the current tree.
|
|
67
|
-
If these nodes are given as arguments to other nodes or used with the `>>` operator, they will be automatically linked to the appropriate sockets.
|
|
68
|
-
|
|
69
|
-
## Nodes
|
|
70
|
-
|
|
71
|
-
Documentation for all of the nodes can be found in the [API Reference](https://bradyajohnston.github.io/nodebpy/reference/). This is mostly built automatically from the existing Blender node classes.
|
|
72
|
-
|
|
73
|
-
Every node has all of it's input sockets and enum options exposed as arguments to the class constructor. Input sockets are prefixed with `.i.*` and output sockets are prefixed with `.o.*`. Properties that aren't exposed as sockets are available as class properties. Many properties are also available as class methods for convenience when constructing.
|
|
74
|
-
|
|
75
|
-
The basic math operators also automatically add relevant nodes with their operations and values set.
|
|
76
|
-
|
|
77
|
-
```py
|
|
78
|
-
# operation is exposed as a property
|
|
79
|
-
math = g.Math(1.0, 2.0, operation='ADD')
|
|
80
|
-
math.operation = "SUBTRACT"
|
|
81
|
-
|
|
82
|
-
# operation can be chose as a class method
|
|
83
|
-
math = g.Math.subtract(1.0, 2.0)
|
|
84
|
-
math = g.Value(1.0) - 2.0
|
|
85
|
-
math = g.Math.add(1.0, 2.0)
|
|
86
|
-
math = g.Value(1.0) + 2.0
|
|
87
|
-
# the 3.0 + 2.0 is evaluated as regular python code first,
|
|
88
|
-
# so the result with be a Math.add(g.Value(1.0), 5.0)
|
|
89
|
-
math = g.Value(1.0) 3.0 + 2.0
|
|
90
|
-
|
|
91
|
-
# these are equivalent, the g.Math.multiply is automatically added
|
|
92
|
-
g.Value(1.0) * 2
|
|
93
|
-
g.Math.multiply(g.Value(1.0), 2.0)
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
# Design Considerations
|
|
97
|
-
|
|
98
|
-
The top priority of `nodebpy` has been type hinting and IDE auto-complete.
|
|
99
|
-
Typical tooling that supports autoring regular python code should also support the authoring of node trees.
|
|
100
|
-
Much like [`databpy`](https://github.com/BradyAJohnston/databpy), this started as an internal tool used inside of [`molecularnodes`](https://github.com/BradyAJohnston/molecularnodes) but has since been broken out into it's own separate project.
|
|
101
|
-
This projects is robustly typed and tested, with the intent that it can be used internally for multiple other add-ons and projects.
|
|
102
|
-
|
|
103
|
-
- Node classes are named after nodes 'Random Value' -> `RandomValue()`
|
|
104
|
-
- Node 'subtypes' and methods should be accessible via dot (`.`) for easier IDE auto-complete and authoring:
|
|
105
|
-
- `RandomValue(data_type="FLOAT_VECTOR")` -> `RandomValue.vector()`
|
|
106
|
-
- Node properties are available on the top level, with inputs and outputs available behind `.i.*` and `.o.*` accessors
|
|
107
|
-
- Inputs and outputs from a node are prefixed with `i.*` and `o.*`:
|
|
108
|
-
- `AccumulateField().o.total` returns the output `Total` socket
|
|
109
|
-
- `AccumulateField().i.value` returns the input `Value` socket
|
|
110
|
-
|
|
111
|
-
## Building
|
|
112
|
-
|
|
113
|
-
Most of the code for classes are generated automatically with the `generate.py` script.
|
|
114
|
-
Some nodes are manually specified in the `src/nodebpy/nodes/geometry/manual.py` if they require special handling.
|
|
115
|
-
|
|
116
|
-
Run the build & format script as such:
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
uv run generate.py && uvx ruff format && uvx ruff check --fix && uvx ty check --fix src
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## Other Projects
|
|
123
|
-
|
|
124
|
-
There are several other notable projects which have also attempted interfacing with node trees via code. They mostly fit into two categories of either storing & retrieving node trees via code (`.json` or the `bpy` API), or authoring of node trees with custom API and syntax. This project mostly fits in to the latter category.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
### Storing node trees as code:
|
|
129
|
-
Converting node trees to the python API calls or `.json` is great to have a robust storage method, but this approach falls down in human authorability / readability. These projects are great for storage but less useful when wanting to write / generate node trees froms scratch.
|
|
130
|
-
|
|
131
|
-
- [NodeToPython](https://github.com/BrendanParmer/NodeToPython)
|
|
132
|
-
- [TreeClipper](https://github.com/Algebraic-UG/tree_clipper) (used by this project for running tests & snapshots)
|
|
133
|
-
|
|
134
|
-
### Authoring node trees with code:
|
|
135
|
-
Two previous projects have made similar approaches to authoring node trees. `geometry-script` also auto-generated most of it's type hinting, code and docs. It uses the approach of method chaining with the `.` operator, but obfuscates some of the non-linear way of building node trees.
|
|
136
|
-
|
|
137
|
-
The other project `geonodes` uses a similar context system for creating and authoring node trees, but doesn't use the same method of exposing each individual node as it's own class that `nodebpy` does.
|
|
138
|
-
|
|
139
|
-
I personally found both of their APIs to _not quite_ fit how I wanted to work, leading to the creation of `nodebpy`.
|
|
140
|
-
In comparison, this project is also the only one that is also distributed on `PyPi` and insallable via `pip` for easier use in other projects.
|
|
141
|
-
|
|
142
|
-
- [geometry-script](https://github.com/carson-katri/geometry-script),
|
|
143
|
-
- [geonodes](https://github.com/al1brn/geonodes)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|