nodebpy 520.2.0__tar.gz → 520.4.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-520.2.0 → nodebpy-520.4.0}/PKG-INFO +1 -1
- {nodebpy-520.2.0 → nodebpy-520.4.0}/pyproject.toml +2 -2
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/__init__.py +3 -1
- nodebpy-520.4.0/src/nodebpy/assets/__main__.py +113 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/assets/_codegen.py +21 -8
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/_utils.py +16 -2
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/asset.py +1 -1
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/mixins.py +12 -2
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/socket.py +115 -4
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/tree.py +3 -1
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/export/codegen.py +332 -64
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +1 -1
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/_mixins.py +1 -1
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/assets.py +2 -2
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/assets.py +2 -2
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/converter.py +3 -3
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/groups.py +8 -9
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/manual.py +4 -5
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/zone.py +2 -2
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/assets.py +2 -2
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/types.py +2 -3
- nodebpy-520.2.0/src/nodebpy/assets/__main__.py +0 -57
- {nodebpy-520.2.0 → nodebpy-520.4.0}/README.md +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/assets/__init__.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/__init__.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/_registry.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/accessor.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/arrange.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/items.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/node.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/export/__init__.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/export/diagram.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/__init__.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/color.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/group.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/input.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/output.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/__init__.py +208 -208
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/color.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/group.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/input.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/output.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/color.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/converter.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/grid.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/group.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/input.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/manual.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/output.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/script.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/shader.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/texture.py +0 -0
- {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/vector.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nodebpy"
|
|
3
|
-
version = "520.
|
|
3
|
+
version = "520.4.0"
|
|
4
4
|
description = "Build nodes trees in Blender more elegantly with code"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -42,5 +42,5 @@ dev = [
|
|
|
42
42
|
"syrupy>=5.0.0",
|
|
43
43
|
"pytest-xdist>=3.8.0",
|
|
44
44
|
"griffe<2.0.0",
|
|
45
|
-
"ty
|
|
45
|
+
"ty==0.0.51",
|
|
46
46
|
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from . import export, nodes
|
|
1
|
+
from . import builder, export, nodes, types
|
|
2
2
|
from .builder import TreeBuilder
|
|
3
3
|
from .nodes import compositor, geometry, shader
|
|
4
4
|
|
|
@@ -8,5 +8,7 @@ __all__ = [
|
|
|
8
8
|
"geometry",
|
|
9
9
|
"shader",
|
|
10
10
|
"export",
|
|
11
|
+
"types",
|
|
12
|
+
"builder",
|
|
11
13
|
"TreeBuilder",
|
|
12
14
|
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Regenerate the bundled-essentials asset APIs.
|
|
2
|
+
|
|
3
|
+
``python -m nodebpy.assets`` introspects Blender's bundled node-group asset
|
|
4
|
+
libraries and writes typed classes to ``nodebpy/nodes/{geometry,shader,
|
|
5
|
+
compositor}/assets.py``, where ``python -m gen`` re-exports them so they are
|
|
6
|
+
available alongside the built-in nodes (e.g. ``g.SmoothByAngle()``). Run *before*
|
|
7
|
+
``python -m gen`` and through the ruff/ty post-processing (see the Makefile).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from ..builder import BundledLibrary, PackageLibrary
|
|
17
|
+
from ._codegen import generate_asset_api
|
|
18
|
+
|
|
19
|
+
# Bundled libraries shipped with Blender, grouped by output module. Each library
|
|
20
|
+
# holds node groups of a single tree type.
|
|
21
|
+
_ESSENTIALS: dict[str, tuple[str, ...]] = {
|
|
22
|
+
"geometry": (
|
|
23
|
+
"geometry_nodes_essentials.blend",
|
|
24
|
+
"geometry_nodes_dynamics_assets.blend",
|
|
25
|
+
"procedural_hair_node_assets.blend",
|
|
26
|
+
"principal_components.blend",
|
|
27
|
+
),
|
|
28
|
+
"shader": ("shading_nodes_essentials.blend",),
|
|
29
|
+
"compositor": ("compositing_nodes_essentials.blend",),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_essentials(
|
|
34
|
+
nodes_dir: Path, nodebpy_pkg: str = ".."
|
|
35
|
+
) -> dict[str, list[str]]:
|
|
36
|
+
"""Generate the bundled-essentials asset modules into
|
|
37
|
+
``<nodes_dir>/<tree>/assets.py``; returns the class names written per tree
|
|
38
|
+
(libraries not present in this Blender install are skipped)."""
|
|
39
|
+
written: dict[str, list[str]] = {}
|
|
40
|
+
for tree, filenames in _ESSENTIALS.items():
|
|
41
|
+
libraries = [
|
|
42
|
+
BundledLibrary(f)
|
|
43
|
+
for f in filenames
|
|
44
|
+
if os.path.exists(BundledLibrary(f).path())
|
|
45
|
+
]
|
|
46
|
+
if not libraries: # pragma: no cover - depends on the Blender install
|
|
47
|
+
print(f" {tree}: no bundled libraries present, skipping")
|
|
48
|
+
continue
|
|
49
|
+
names = generate_asset_api(
|
|
50
|
+
libraries,
|
|
51
|
+
Path(nodes_dir) / tree / "assets.py",
|
|
52
|
+
nodebpy_pkg=nodebpy_pkg,
|
|
53
|
+
)
|
|
54
|
+
written[tree] = names
|
|
55
|
+
print(f" nodes/{tree}/assets.py: {len(names)} asset classes")
|
|
56
|
+
return written
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_args() -> argparse.Namespace:
|
|
60
|
+
parser = argparse.ArgumentParser()
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--blend-file",
|
|
63
|
+
"-b",
|
|
64
|
+
type=Path,
|
|
65
|
+
help="Optional custom .blend asset library to generate from.",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--output",
|
|
69
|
+
"--output-dir",
|
|
70
|
+
"-o",
|
|
71
|
+
dest="output",
|
|
72
|
+
type=Path,
|
|
73
|
+
help="Path of the assets.py module to write.",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--nodebpy-pkg",
|
|
77
|
+
default="nodebpy",
|
|
78
|
+
help=(
|
|
79
|
+
"Import anchor for nodebpy in the generated module. Defaults to the "
|
|
80
|
+
"absolute 'nodebpy'. When nodebpy is vendored inside another package, "
|
|
81
|
+
"pass the path that reaches it relative to the generated module's "
|
|
82
|
+
"package — e.g. '..lib.nodebpy'."
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
return parser.parse_args()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main() -> None: # pragma: no cover - CLI wrapper
|
|
89
|
+
args = parse_args()
|
|
90
|
+
output = (
|
|
91
|
+
args.output
|
|
92
|
+
if args.output
|
|
93
|
+
else Path(__file__).parent.parent / "nodes" / "custom" / "assets.py"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if args.blend_file is not None:
|
|
97
|
+
# PackageLibrary resolves ``relative`` against the generated module's
|
|
98
|
+
# directory (``__file__``), so express the .blend relative to the output
|
|
99
|
+
# module — not the CWD the command happened to run from.
|
|
100
|
+
blend = args.blend_file.resolve()
|
|
101
|
+
relative = Path(os.path.relpath(blend, output.resolve().parent)).as_posix()
|
|
102
|
+
generate_asset_api(
|
|
103
|
+
[PackageLibrary(str(output), relative)],
|
|
104
|
+
output,
|
|
105
|
+
nodebpy_pkg=args.nodebpy_pkg,
|
|
106
|
+
)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
generate_essentials(Path(__file__).parent.parent / "nodes")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__": # pragma: no cover
|
|
113
|
+
main()
|
|
@@ -144,7 +144,7 @@ def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetCla
|
|
|
144
144
|
path = library.path()
|
|
145
145
|
library_source = _library_source(library)
|
|
146
146
|
|
|
147
|
-
with bpy.data.libraries.load(path, link=False, assets_only=True) as (src, _):
|
|
147
|
+
with bpy.data.libraries.load(path, link=False, assets_only=True) as (src, _): # ty: ignore[invalid-context-manager]
|
|
148
148
|
available = list(src.node_groups)
|
|
149
149
|
wanted = [n for n in available if names is None or n in names]
|
|
150
150
|
|
|
@@ -155,7 +155,7 @@ def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetCla
|
|
|
155
155
|
# compositor "Combine Spherical" both exist) and we'd introspect the
|
|
156
156
|
# wrong one. Blender renames the appended copy on a clash; that's fine,
|
|
157
157
|
# we only read its interface (the emitted _asset_name uses the original).
|
|
158
|
-
with bpy.data.libraries.load(path, link=False, assets_only=True) as (
|
|
158
|
+
with bpy.data.libraries.load(path, link=False, assets_only=True) as ( # ty: ignore[invalid-context-manager]
|
|
159
159
|
src,
|
|
160
160
|
dst,
|
|
161
161
|
):
|
|
@@ -170,7 +170,7 @@ def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetCla
|
|
|
170
170
|
"CompositorNodeTree": "CompositorNodeGroup",
|
|
171
171
|
}[group.bl_idname]
|
|
172
172
|
node = host.nodes.new(node_type)
|
|
173
|
-
node.node_tree = group
|
|
173
|
+
node.node_tree = group # ty: ignore[unresolved-attribute]
|
|
174
174
|
classes.append(
|
|
175
175
|
_AssetClass(
|
|
176
176
|
class_name=_class_name(name),
|
|
@@ -194,7 +194,11 @@ def _library_source(library: AssetLibrary) -> str:
|
|
|
194
194
|
from ..builder import PackageLibrary
|
|
195
195
|
|
|
196
196
|
if isinstance(library, PackageLibrary):
|
|
197
|
-
|
|
197
|
+
# Emit a plain forward-slash string literal (never ``PosixPath(...)``),
|
|
198
|
+
# so the generated module imports cleanly and stays cross-platform even
|
|
199
|
+
# when ``relative`` was passed as a ``Path``.
|
|
200
|
+
relative = Path(library.relative).as_posix()
|
|
201
|
+
return f"PackageLibrary(__file__, {relative!r})"
|
|
198
202
|
raise TypeError(f"Cannot serialise asset library: {library!r}")
|
|
199
203
|
|
|
200
204
|
|
|
@@ -245,7 +249,7 @@ def _render_class(cls: _AssetClass) -> str:
|
|
|
245
249
|
'''
|
|
246
250
|
|
|
247
251
|
|
|
248
|
-
def _render_module(classes: list[_AssetClass]) -> str:
|
|
252
|
+
def _render_module(classes: list[_AssetClass], nodebpy_pkg: str = "nodebpy") -> str:
|
|
249
253
|
socket_classes = sorted(
|
|
250
254
|
{s.socket_class for c in classes for s in c.inputs + c.outputs}
|
|
251
255
|
)
|
|
@@ -268,8 +272,8 @@ def _render_module(classes: list[_AssetClass]) -> str:
|
|
|
268
272
|
"# Auto-generated by nodebpy.assets.generate_asset_api — do not edit manually.",
|
|
269
273
|
"from typing import TYPE_CHECKING",
|
|
270
274
|
"",
|
|
271
|
-
f"from
|
|
272
|
-
f"from
|
|
275
|
+
f"from {nodebpy_pkg}.builder import (\n {',\n '.join(builder_imports)},\n)",
|
|
276
|
+
f"from {nodebpy_pkg}.types import (\n {',\n '.join(input_types)},\n)"
|
|
273
277
|
if input_types
|
|
274
278
|
else "",
|
|
275
279
|
]
|
|
@@ -288,6 +292,7 @@ def generate_asset_api(
|
|
|
288
292
|
output_path: str | Path,
|
|
289
293
|
*,
|
|
290
294
|
names: set[str] | None = None,
|
|
295
|
+
nodebpy_pkg: str = "nodebpy",
|
|
291
296
|
) -> list[str]:
|
|
292
297
|
"""Generate typed asset classes for ``libraries`` into ``output_path``.
|
|
293
298
|
|
|
@@ -302,6 +307,12 @@ def generate_asset_api(
|
|
|
302
307
|
The ``.py`` file to write.
|
|
303
308
|
names:
|
|
304
309
|
Restrict generation to these asset (node-group) names; defaults to all.
|
|
310
|
+
nodebpy_pkg:
|
|
311
|
+
Import anchor for nodebpy in the generated module. Defaults to the
|
|
312
|
+
absolute ``"nodebpy"``. When nodebpy is vendored inside another package,
|
|
313
|
+
pass the path that reaches it *relative to the generated module's
|
|
314
|
+
package* — e.g. ``"..vendor.nodebpy"`` — so the emitted imports stay
|
|
315
|
+
relative to the install/vendor location.
|
|
305
316
|
|
|
306
317
|
Returns the list of generated class names.
|
|
307
318
|
"""
|
|
@@ -314,5 +325,7 @@ def generate_asset_api(
|
|
|
314
325
|
|
|
315
326
|
output_path = Path(output_path)
|
|
316
327
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
317
|
-
output_path.write_text(
|
|
328
|
+
output_path.write_text(
|
|
329
|
+
_render_module(classes, nodebpy_pkg=nodebpy_pkg), encoding="utf-8"
|
|
330
|
+
)
|
|
318
331
|
return [c.class_name for c in classes]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import keyword
|
|
4
|
+
import re
|
|
3
5
|
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
4
6
|
|
|
5
7
|
import bpy
|
|
@@ -34,9 +36,21 @@ GEO_NODE_NAMES = (
|
|
|
34
36
|
)
|
|
35
37
|
|
|
36
38
|
|
|
39
|
+
_NON_IDENTIFIER = re.compile(r"[^0-9a-z]+")
|
|
40
|
+
|
|
41
|
+
|
|
37
42
|
def normalize_name(name: str) -> str:
|
|
38
|
-
"""Convert 'Geometry' or 'My Socket' to
|
|
39
|
-
|
|
43
|
+
"""Convert 'Geometry' or 'My Socket' to a valid lower-case Python identifier
|
|
44
|
+
('geometry', 'my_socket'). Spaces, punctuation and other non-identifier
|
|
45
|
+
characters (e.g. the '⟂'/'(' in 'BA⟂(BC)') collapse to underscores so the
|
|
46
|
+
result is always usable as an attribute or parameter name."""
|
|
47
|
+
text = name.lower().replace("é", "e")
|
|
48
|
+
cleaned = _NON_IDENTIFIER.sub("_", text).strip("_")
|
|
49
|
+
if cleaned and cleaned[0].isdigit():
|
|
50
|
+
cleaned = "_" + cleaned
|
|
51
|
+
if keyword.iskeyword(cleaned):
|
|
52
|
+
cleaned += "_"
|
|
53
|
+
return cleaned or "_"
|
|
40
54
|
|
|
41
55
|
|
|
42
56
|
def denormalize_name(attr_name: str) -> str:
|
|
@@ -93,7 +93,7 @@ class _AssetGroupMixin:
|
|
|
93
93
|
path = cls._library.path()
|
|
94
94
|
if not os.path.exists(path):
|
|
95
95
|
raise FileNotFoundError(f"Asset library not found: {path}")
|
|
96
|
-
with bpy.data.libraries.load(path, link=False, assets_only=True) as (src, dst):
|
|
96
|
+
with bpy.data.libraries.load(path, link=False, assets_only=True) as (src, dst): # ty: ignore[invalid-context-manager]
|
|
97
97
|
if cls._asset_name not in src.node_groups:
|
|
98
98
|
raise KeyError(f"Node group {cls._asset_name!r} not found in {path}")
|
|
99
99
|
dst.node_groups = [cls._asset_name]
|
|
@@ -367,7 +367,11 @@ class LinkingMixin:
|
|
|
367
367
|
else:
|
|
368
368
|
self._link(source, input)
|
|
369
369
|
|
|
370
|
-
|
|
370
|
+
@overload
|
|
371
|
+
def __rshift__(self, other: None) -> Self: ...
|
|
372
|
+
@overload
|
|
373
|
+
def __rshift__(self, other: _RShiftT) -> _RShiftT: ...
|
|
374
|
+
def __rshift__(self, other: _RShiftT | None) -> _RShiftT | Self:
|
|
371
375
|
"""Chain nodes using >> operator. Links output to input.
|
|
372
376
|
|
|
373
377
|
Usage:
|
|
@@ -377,8 +381,14 @@ class LinkingMixin:
|
|
|
377
381
|
If the target node has an ellipsis placeholder (...), links to that specific input.
|
|
378
382
|
Otherwise, finds the best compatible socket pair based on type compatibility.
|
|
379
383
|
|
|
380
|
-
Returns the right-hand node to enable continued chaining.
|
|
384
|
+
Returns the right-hand node to enable continued chaining. A ``None``
|
|
385
|
+
target is a no-op passthrough — nothing is linked and ``self`` is
|
|
386
|
+
returned so an optional node can be conditionally skipped::
|
|
387
|
+
|
|
388
|
+
src >> SetPosition() >> (Transform() if trans else None) >> out
|
|
381
389
|
"""
|
|
390
|
+
if other is None:
|
|
391
|
+
return self
|
|
382
392
|
if isinstance(other, _SocketLike):
|
|
383
393
|
source = self._default_output_socket
|
|
384
394
|
target = other.socket
|
|
@@ -472,7 +472,9 @@ class _FloatGridOperatorMixin(Socket):
|
|
|
472
472
|
"""Generate a mesh on the "surface" of a volume grid."""
|
|
473
473
|
from ..nodes.geometry import GridToMesh
|
|
474
474
|
|
|
475
|
-
return GridToMesh(
|
|
475
|
+
return GridToMesh(
|
|
476
|
+
self.socket, threshold=threshold, adaptivity=adaptivity
|
|
477
|
+
).o.mesh
|
|
476
478
|
|
|
477
479
|
|
|
478
480
|
class _VectorGridOperatorMixin(Socket):
|
|
@@ -675,7 +677,6 @@ class _AccumulateField(_EvaluateField[_T]):
|
|
|
675
677
|
def _accumulate(
|
|
676
678
|
self, output: Literal["leading", "trailing", "total"], group_index: InputInteger
|
|
677
679
|
) -> "_T":
|
|
678
|
-
|
|
679
680
|
from ..nodes.geometry import AccumulateField
|
|
680
681
|
|
|
681
682
|
node = getattr(
|
|
@@ -1504,16 +1505,91 @@ class _FloatMixin(BaseSocket, Generic[_IntegerResult]):
|
|
|
1504
1505
|
|
|
1505
1506
|
return Clamp.min_max(self.socket, min, max).o.result # ty: ignore[invalid-return-type]
|
|
1506
1507
|
|
|
1508
|
+
def min(self, value: InputFloat = 0.0) -> Self:
|
|
1509
|
+
"""Create Math with operation 'Minimum'. The minimum from self and value"""
|
|
1510
|
+
self._assert_output("min")
|
|
1511
|
+
return self._math.minimum(self.socket, value).o.value # ty: ignore[invalid-return-type]
|
|
1512
|
+
|
|
1513
|
+
def max(self, value: InputFloat = 1.0) -> Self:
|
|
1514
|
+
"""Create Math with operation 'Maximum'. The maximum from self and value"""
|
|
1515
|
+
self._assert_output("max")
|
|
1516
|
+
return self._math.maximum(self.socket, value).o.value # ty: ignore[invalid-return-type]
|
|
1517
|
+
|
|
1518
|
+
def sin(self) -> Self:
|
|
1519
|
+
"Create a Math node with operation 'Sine'. The sine of self"
|
|
1520
|
+
self._assert_output("sin")
|
|
1521
|
+
return self._math.sine(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1522
|
+
|
|
1523
|
+
def cos(self) -> Self:
|
|
1524
|
+
"Create a Math node with operation 'Cosine'. The cosine of self"
|
|
1525
|
+
self._assert_output("cos")
|
|
1526
|
+
return self._math.cosine(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1527
|
+
|
|
1528
|
+
def tan(self) -> Self:
|
|
1529
|
+
"Create a Math node with operation 'Tangent'. The tangent of self"
|
|
1530
|
+
self._assert_output("tan")
|
|
1531
|
+
return self._math.tangent(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1532
|
+
|
|
1533
|
+
def asin(self) -> Self:
|
|
1534
|
+
"Create a Math node with operation 'ArcSine'. The arcsine of self"
|
|
1535
|
+
self._assert_output("asin")
|
|
1536
|
+
return self._math.arcsine(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1537
|
+
|
|
1538
|
+
def acos(self) -> Self:
|
|
1539
|
+
"Create a Math node with operation 'ArcCosine'. The arccosine of self"
|
|
1540
|
+
self._assert_output("acos")
|
|
1541
|
+
return self._math.arccosine(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1542
|
+
|
|
1543
|
+
def atan(self) -> Self:
|
|
1544
|
+
"Create a Math node with operation 'ArcTangent'. The arctangent of self"
|
|
1545
|
+
self._assert_output("atan")
|
|
1546
|
+
return self._math.arctangent(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1547
|
+
|
|
1548
|
+
def sinh(self) -> Self:
|
|
1549
|
+
"Create a Math node with operation 'Hyperbolic Sine'. The hyperbolic sine of self"
|
|
1550
|
+
self._assert_output("sinh")
|
|
1551
|
+
return self._math.hyperbolic_sine(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1552
|
+
|
|
1553
|
+
def cosh(self) -> Self:
|
|
1554
|
+
"Create a Math node with operation 'Hyperbolic Cosine'. The hyperbolic cosine of self"
|
|
1555
|
+
self._assert_output("cosh")
|
|
1556
|
+
return self._math.hyperbolic_cosine(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1557
|
+
|
|
1558
|
+
def tanh(self) -> Self:
|
|
1559
|
+
"Create a Math node with operation 'Hyperbolic Tangent'. The hyperbolic tangent of self"
|
|
1560
|
+
self._assert_output("tanh")
|
|
1561
|
+
return self._math.hyperbolic_tangent(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1562
|
+
|
|
1563
|
+
def exp(self) -> Self:
|
|
1564
|
+
"Create a Math node with operation 'Exponent'. The exponent of self"
|
|
1565
|
+
self._assert_output("exp")
|
|
1566
|
+
return self._math.exponent(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1567
|
+
|
|
1568
|
+
def snap(self, increment: InputFloat = 0.5) -> Self:
|
|
1569
|
+
"Create a Math node with operation 'Snap'. The snap of self"
|
|
1570
|
+
self._assert_output("snap")
|
|
1571
|
+
return self._math.snap(self.socket, increment).o.value # ty: ignore[invalid-return-type]
|
|
1572
|
+
|
|
1573
|
+
def atan2(self, value: InputFloat = 0.5) -> Self:
|
|
1574
|
+
"Create a Math node with operation 'ArcTan2'. The arctangent of self"
|
|
1575
|
+
self._assert_output("atan2")
|
|
1576
|
+
return self._math.arctan2(self.socket, value).o.value # ty: ignore[invalid-return-type]
|
|
1577
|
+
|
|
1507
1578
|
def sqrt(self) -> Self:
|
|
1508
1579
|
"""Return the square root of this value."""
|
|
1509
1580
|
self._assert_output("sqrt")
|
|
1510
1581
|
return self._math.square_root(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1511
1582
|
|
|
1512
|
-
def power(self, exponent: InputFloat) -> Self:
|
|
1583
|
+
def power(self, exponent: InputFloat = 2.0) -> Self:
|
|
1513
1584
|
"""Raise this value to *exponent*."""
|
|
1514
1585
|
self._assert_output("power")
|
|
1515
1586
|
return self._math.power(self.socket, exponent).o.value # ty: ignore[invalid-return-type]
|
|
1516
1587
|
|
|
1588
|
+
def log(self, base: InputFloat = 2.0) -> Self:
|
|
1589
|
+
"""Return the logarithm of this value to *base*."""
|
|
1590
|
+
self._assert_output("log")
|
|
1591
|
+
return self._math.logarithm(self.socket, base).o.value # ty: ignore[invalid-return-type]
|
|
1592
|
+
|
|
1517
1593
|
def floor(self) -> Self:
|
|
1518
1594
|
"""Round down to the nearest integer."""
|
|
1519
1595
|
self._assert_output("floor")
|
|
@@ -1524,23 +1600,48 @@ class _FloatMixin(BaseSocket, Generic[_IntegerResult]):
|
|
|
1524
1600
|
self._assert_output("ceil")
|
|
1525
1601
|
return self._math.ceil(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1526
1602
|
|
|
1603
|
+
def truncate(self) -> Self:
|
|
1604
|
+
"""The integer part of of the value, removing fractional digits"""
|
|
1605
|
+
self._assert_output("truncate")
|
|
1606
|
+
return self._math.truncate(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1607
|
+
|
|
1608
|
+
def fraction(self) -> Self:
|
|
1609
|
+
"""The fractional part of the vlaue"""
|
|
1610
|
+
self._assert_output("fraction")
|
|
1611
|
+
return self._math.fraction(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1612
|
+
|
|
1527
1613
|
def round(self) -> Self:
|
|
1528
1614
|
"""Round to the nearest integer."""
|
|
1529
1615
|
self._assert_output("round")
|
|
1530
1616
|
return self._math.round(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1531
1617
|
|
|
1618
|
+
def ping_pong(self, value: InputFloat = 1.0) -> Self:
|
|
1619
|
+
"""Input ping-pongs between 0 and *value*."""
|
|
1620
|
+
self._assert_output("ping_pong")
|
|
1621
|
+
return self._math.ping_pong(self.socket, value).o.value # ty: ignore[invalid-return-type]
|
|
1622
|
+
|
|
1532
1623
|
def modulo(self, divisor: InputFloat) -> Self:
|
|
1533
1624
|
"""Floored modulo — remainder after dividing by *divisor*, always non-negative."""
|
|
1534
1625
|
self._assert_output("modulo")
|
|
1535
1626
|
return self._math.floored_modulo(self.socket, divisor).o.value # ty: ignore[invalid-return-type]
|
|
1536
1627
|
|
|
1537
|
-
def
|
|
1628
|
+
def abs(self) -> Self:
|
|
1629
|
+
"""Absolute value of the input"""
|
|
1630
|
+
self._assert_output("abs")
|
|
1631
|
+
return self._math.absolute(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1632
|
+
|
|
1633
|
+
def wrap(self, min: InputFloat = 0.0, max: InputFloat = 1.0) -> Self:
|
|
1538
1634
|
"""Wrap the value into the *[min, max]* range, repeating cyclically."""
|
|
1539
1635
|
self._assert_output("wrap")
|
|
1540
1636
|
# the wrap method has different order of arguments with max being first
|
|
1541
1637
|
# compared to other nodes that are defined.
|
|
1542
1638
|
return self._math.wrap(self.socket, value_001=max, value_002=min).o.value # ty: ignore[invalid-return-type]
|
|
1543
1639
|
|
|
1640
|
+
def mul_add(self, multiplier: InputFloat = 0.5, addend: InputFloat = 0.5) -> Self:
|
|
1641
|
+
"""Multiply and then add a value. More efficient as it is a single CPU instruction."""
|
|
1642
|
+
self._assert_output("mul_add")
|
|
1643
|
+
return self._math.multiply_add(self.socket, multiplier, addend).o.value # ty: ignore[invalid-return-type]
|
|
1644
|
+
|
|
1544
1645
|
def to_radians(self) -> Self:
|
|
1545
1646
|
"""Convert degrees to radians."""
|
|
1546
1647
|
self._assert_output("to_radians")
|
|
@@ -1602,6 +1703,16 @@ class _IntegerMixin(BaseSocket, Generic[_FloatResult]):
|
|
|
1602
1703
|
self._assert_output("negate")
|
|
1603
1704
|
return self._imath.negate(self.socket).o.value # ty: ignore[invalid-return-type]
|
|
1604
1705
|
|
|
1706
|
+
def mul_add(self, multiplier: InputInteger = 0, addend: InputInteger = 0) -> Self:
|
|
1707
|
+
"Multiply and then add a value. More efficient as it is a single CPU instruction."
|
|
1708
|
+
self._assert_output("mul_add")
|
|
1709
|
+
return self._imath.multiply_add(self.socket, multiplier, addend).o.value # ty: ignore[invalid-return-type]
|
|
1710
|
+
|
|
1711
|
+
def power(self, exponent: InputInteger = 2) -> Self:
|
|
1712
|
+
"""Raise this value to *exponent*."""
|
|
1713
|
+
self._assert_output("power")
|
|
1714
|
+
return self._imath.power(self.socket, exponent).o.value # ty: ignore[invalid-return-type]
|
|
1715
|
+
|
|
1605
1716
|
@staticmethod
|
|
1606
1717
|
def _is_integer_socket(value: Any) -> bool:
|
|
1607
1718
|
socket = getattr(
|
|
@@ -629,7 +629,7 @@ class TreeBuilder(Generic[_TreeT]):
|
|
|
629
629
|
ignore_visibility: bool = False,
|
|
630
630
|
):
|
|
631
631
|
if isinstance(tree, str):
|
|
632
|
-
self.tree = bpy.data.node_groups.new(tree, tree_type) #
|
|
632
|
+
self.tree = bpy.data.node_groups.new(tree, tree_type) # ty: ignore[invalid-assignment]
|
|
633
633
|
else:
|
|
634
634
|
self.tree = tree # type: ignore
|
|
635
635
|
|
|
@@ -725,6 +725,7 @@ class TreeBuilder(Generic[_TreeT]):
|
|
|
725
725
|
keep_reroutes: bool = False,
|
|
726
726
|
top_level: Literal["with", "class"] = "with",
|
|
727
727
|
format: bool = True,
|
|
728
|
+
nodebpy_pkg: str = "nodebpy",
|
|
728
729
|
) -> str:
|
|
729
730
|
"""Generate Python source that recreates this tree using nodebpy.
|
|
730
731
|
|
|
@@ -741,6 +742,7 @@ class TreeBuilder(Generic[_TreeT]):
|
|
|
741
742
|
keep_reroutes=keep_reroutes,
|
|
742
743
|
top_level=top_level,
|
|
743
744
|
format=format,
|
|
745
|
+
nodebpy_pkg=nodebpy_pkg,
|
|
744
746
|
)
|
|
745
747
|
|
|
746
748
|
def to_mermaid(self, fenced: bool = True) -> str:
|