nodebpy 520.1.0__tar.gz → 520.2.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.1.0 → nodebpy-520.2.0}/PKG-INFO +1 -1
- {nodebpy-520.1.0 → nodebpy-520.2.0}/pyproject.toml +1 -1
- nodebpy-520.2.0/src/nodebpy/assets/__init__.py +17 -0
- nodebpy-520.2.0/src/nodebpy/assets/__main__.py +57 -0
- nodebpy-520.2.0/src/nodebpy/assets/_codegen.py +318 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/builder/__init__.py +17 -1
- nodebpy-520.2.0/src/nodebpy/builder/asset.py +139 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/export/codegen.py +102 -10
- nodebpy-520.2.0/src/nodebpy/nodes/_mixins.py +247 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/compositor/__init__.py +37 -1
- nodebpy-520.2.0/src/nodebpy/nodes/compositor/assets.py +783 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/compositor/converter.py +11 -43
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/compositor/interface.py +5 -15
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/__init__.py +140 -12
- nodebpy-520.2.0/src/nodebpy/nodes/geometry/assets.py +3342 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/attribute.py +5 -4
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/converter.py +1553 -349
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/geometry.py +156 -47
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/grid.py +72 -69
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/input.py +69 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/interface.py +5 -21
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/manual.py +19 -1269
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/shader/__init__.py +10 -0
- nodebpy-520.2.0/src/nodebpy/nodes/shader/assets.py +157 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/shader/converter.py +5 -12
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/shader/shader.py +2 -2
- {nodebpy-520.1.0 → nodebpy-520.2.0}/README.md +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/__init__.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/builder/_registry.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/builder/_utils.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/builder/accessor.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/builder/arrange.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/builder/items.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/builder/mixins.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/builder/node.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/builder/socket.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/builder/tree.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/export/__init__.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/export/diagram.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/__init__.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/compositor/color.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/compositor/group.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/compositor/input.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/compositor/output.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/color.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/group.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/output.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/shader/color.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/shader/grid.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/shader/group.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/shader/input.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/shader/manual.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/shader/output.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/shader/script.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/shader/texture.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/nodes/shader/vector.py +0 -0
- {nodebpy-520.1.0 → nodebpy-520.2.0}/src/nodebpy/types.py +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Typed APIs for node-group assets.
|
|
2
|
+
|
|
3
|
+
``generate_asset_api`` builds typed :class:`~nodebpy.builder.AssetNodeGroup`
|
|
4
|
+
classes for the node groups in a ``.blend`` asset library. The bundled-essentials
|
|
5
|
+
APIs generated for nodebpy itself live alongside this module as
|
|
6
|
+
``nodebpy.assets.geometry`` / ``.shader`` / ``.compositor``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ..builder import AssetLibrary, BundledLibrary, PackageLibrary
|
|
10
|
+
from ._codegen import generate_asset_api
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"generate_asset_api",
|
|
14
|
+
"AssetLibrary",
|
|
15
|
+
"BundledLibrary",
|
|
16
|
+
"PackageLibrary",
|
|
17
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
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 os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from ..builder import BundledLibrary
|
|
16
|
+
from ._codegen import generate_asset_api
|
|
17
|
+
|
|
18
|
+
# Bundled libraries shipped with Blender, grouped by output module. Each library
|
|
19
|
+
# holds node groups of a single tree type.
|
|
20
|
+
_ESSENTIALS: dict[str, tuple[str, ...]] = {
|
|
21
|
+
"geometry": (
|
|
22
|
+
"geometry_nodes_essentials.blend",
|
|
23
|
+
"geometry_nodes_dynamics_assets.blend",
|
|
24
|
+
"procedural_hair_node_assets.blend",
|
|
25
|
+
"principal_components.blend",
|
|
26
|
+
),
|
|
27
|
+
"shader": ("shading_nodes_essentials.blend",),
|
|
28
|
+
"compositor": ("compositing_nodes_essentials.blend",),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def generate_essentials(nodes_dir: Path) -> dict[str, list[str]]:
|
|
33
|
+
"""Generate the bundled-essentials asset modules into
|
|
34
|
+
``<nodes_dir>/<tree>/assets.py``; returns the class names written per tree
|
|
35
|
+
(libraries not present in this Blender install are skipped)."""
|
|
36
|
+
written: dict[str, list[str]] = {}
|
|
37
|
+
for tree, filenames in _ESSENTIALS.items():
|
|
38
|
+
libraries = [
|
|
39
|
+
BundledLibrary(f)
|
|
40
|
+
for f in filenames
|
|
41
|
+
if os.path.exists(BundledLibrary(f).path())
|
|
42
|
+
]
|
|
43
|
+
if not libraries: # pragma: no cover - depends on the Blender install
|
|
44
|
+
print(f" {tree}: no bundled libraries present, skipping")
|
|
45
|
+
continue
|
|
46
|
+
names = generate_asset_api(libraries, Path(nodes_dir) / tree / "assets.py")
|
|
47
|
+
written[tree] = names
|
|
48
|
+
print(f" nodes/{tree}/assets.py: {len(names)} asset classes")
|
|
49
|
+
return written
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main() -> None: # pragma: no cover - CLI wrapper over generate_essentials
|
|
53
|
+
generate_essentials(Path(__file__).parent.parent / "nodes")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__": # pragma: no cover
|
|
57
|
+
main()
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Generate typed ``nodebpy`` API classes for node-group assets.
|
|
2
|
+
|
|
3
|
+
Given one or more asset ``.blend`` libraries, :func:`generate_asset_api` appends
|
|
4
|
+
each node group, introspects its interface, and writes a Python module of typed
|
|
5
|
+
:class:`~nodebpy.builder.AssetNodeGroup` subclasses — so an asset reads and
|
|
6
|
+
type-checks like any other node. The emitted classes append the asset at runtime
|
|
7
|
+
rather than rebuilding it.
|
|
8
|
+
|
|
9
|
+
This is a *shipped*, reusable tool: other projects call it on their own asset
|
|
10
|
+
libraries (via :class:`~nodebpy.builder.PackageLibrary`) to generate APIs for
|
|
11
|
+
their assets.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections import Counter
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import bpy
|
|
22
|
+
|
|
23
|
+
from ..builder import AssetLibrary, BundledLibrary, asset_group_base
|
|
24
|
+
from ..builder._utils import normalize_name
|
|
25
|
+
|
|
26
|
+
# bl_socket_type substring → (Socket accessor class, Input* parameter type).
|
|
27
|
+
# Order matters: more specific keys (IntVector before Int) come first.
|
|
28
|
+
_SOCKET_TYPES: dict[str, tuple[str, str]] = {
|
|
29
|
+
"NodeSocketFloat": ("FloatSocket", "InputFloat"),
|
|
30
|
+
"NodeSocketIntVector": ("IntegerVectorSocket", "InputIntegerVector"),
|
|
31
|
+
"NodeSocketInt": ("IntegerSocket", "InputInteger"),
|
|
32
|
+
"NodeSocketBool": ("BooleanSocket", "InputBoolean"),
|
|
33
|
+
"NodeSocketVector": ("VectorSocket", "InputVector"),
|
|
34
|
+
"NodeSocketColor": ("ColorSocket", "InputColor"),
|
|
35
|
+
"NodeSocketRotation": ("RotationSocket", "InputRotation"),
|
|
36
|
+
"NodeSocketMatrix": ("MatrixSocket", "InputMatrix"),
|
|
37
|
+
"NodeSocketString": ("StringSocket", "InputString"),
|
|
38
|
+
"NodeSocketMenu": ("MenuSocket", "InputMenu"),
|
|
39
|
+
"NodeSocketGeometry": ("GeometrySocket", "InputGeometry"),
|
|
40
|
+
"NodeSocketObject": ("ObjectSocket", "InputObject"),
|
|
41
|
+
"NodeSocketMaterial": ("MaterialSocket", "InputMaterial"),
|
|
42
|
+
"NodeSocketImage": ("ImageSocket", "InputImage"),
|
|
43
|
+
"NodeSocketCollection": ("CollectionSocket", "InputCollection"),
|
|
44
|
+
"NodeSocketBundle": ("BundleSocket", "InputBundle"),
|
|
45
|
+
"NodeSocketClosure": ("ClosureSocket", "InputClosure"),
|
|
46
|
+
"NodeSocketShader": ("ShaderSocket", "InputShader"),
|
|
47
|
+
"NodeSocketFont": ("FontSocket", "InputFont"),
|
|
48
|
+
"NodeSocketSound": ("SoundSocket", "InputSound"),
|
|
49
|
+
"NodeSocketVirtual": ("Socket", "InputLinkable"),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _socket_types(bl_socket_type: str) -> tuple[str, str]:
|
|
54
|
+
for key, value in _SOCKET_TYPES.items():
|
|
55
|
+
if key in bl_socket_type:
|
|
56
|
+
return value
|
|
57
|
+
return ("Socket", "InputLinkable")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _class_name(name: str) -> str:
|
|
61
|
+
"""A valid, readable Python class name for an asset group display name."""
|
|
62
|
+
cleaned = "".join(c if c.isalnum() or c.isspace() else " " for c in name)
|
|
63
|
+
parts = cleaned.split()
|
|
64
|
+
cleaned = "".join(p[:1].upper() + p[1:] for p in parts)
|
|
65
|
+
if cleaned and cleaned[0].isdigit():
|
|
66
|
+
cleaned = "_" + cleaned
|
|
67
|
+
return cleaned or "AssetGroup"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _format_default(socket: bpy.types.NodeSocket) -> str:
|
|
71
|
+
"""Source for a socket's scalar default value, or ``None``.
|
|
72
|
+
|
|
73
|
+
Only plain scalars are emitted as parameter defaults. Vector/colour/matrix
|
|
74
|
+
defaults vary in arity (2D UVs, 3D vectors, 4-component colours) and don't
|
|
75
|
+
always fit the parameter's ``Input*`` type, so they're left as ``None`` —
|
|
76
|
+
the appended group keeps its own socket default regardless.
|
|
77
|
+
"""
|
|
78
|
+
value = getattr(socket, "default_value", None)
|
|
79
|
+
if isinstance(value, bool):
|
|
80
|
+
return str(value)
|
|
81
|
+
if isinstance(value, int):
|
|
82
|
+
return str(value)
|
|
83
|
+
if isinstance(value, float):
|
|
84
|
+
return str(round(value, 6))
|
|
85
|
+
if isinstance(value, str):
|
|
86
|
+
return repr(value)
|
|
87
|
+
return "None"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class _Socket:
|
|
92
|
+
name: str
|
|
93
|
+
identifier: str
|
|
94
|
+
socket_class: str # e.g. "GeometrySocket"
|
|
95
|
+
input_type: str # e.g. "InputGeometry"
|
|
96
|
+
default: str # source for the default value
|
|
97
|
+
attr: str # normalized accessor/param name
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class _AssetClass:
|
|
102
|
+
class_name: str
|
|
103
|
+
asset_name: str
|
|
104
|
+
description: str
|
|
105
|
+
library_source: str
|
|
106
|
+
tree_idname: str
|
|
107
|
+
inputs: list[_Socket]
|
|
108
|
+
outputs: list[_Socket]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _collect(sockets) -> list[_Socket]:
|
|
112
|
+
raw = [
|
|
113
|
+
s
|
|
114
|
+
for s in sockets
|
|
115
|
+
if s.identifier != "__extend__" and not getattr(s, "is_inactive", False)
|
|
116
|
+
]
|
|
117
|
+
# The accessor resolves attribute names by identifier first, then name, so a
|
|
118
|
+
# group socket's readable name works as the attr/param when it's unambiguous;
|
|
119
|
+
# fall back to the opaque-but-unique identifier only on a name collision.
|
|
120
|
+
name_counts = Counter(normalize_name(s.name) for s in raw)
|
|
121
|
+
out: list[_Socket] = []
|
|
122
|
+
for s in raw:
|
|
123
|
+
socket_class, input_type = _socket_types(type(s).__name__)
|
|
124
|
+
norm_name = normalize_name(s.name)
|
|
125
|
+
attr = (
|
|
126
|
+
norm_name if name_counts[norm_name] == 1 else normalize_name(s.identifier)
|
|
127
|
+
)
|
|
128
|
+
out.append(
|
|
129
|
+
_Socket(
|
|
130
|
+
name=s.name,
|
|
131
|
+
identifier=s.identifier,
|
|
132
|
+
socket_class=socket_class,
|
|
133
|
+
input_type=input_type,
|
|
134
|
+
default=_format_default(s),
|
|
135
|
+
attr=attr,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
return out
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetClass]:
|
|
142
|
+
"""Append each requested group from the library and introspect its
|
|
143
|
+
interface into :class:`_AssetClass` records."""
|
|
144
|
+
path = library.path()
|
|
145
|
+
library_source = _library_source(library)
|
|
146
|
+
|
|
147
|
+
with bpy.data.libraries.load(path, link=False, assets_only=True) as (src, _):
|
|
148
|
+
available = list(src.node_groups)
|
|
149
|
+
wanted = [n for n in available if names is None or n in names]
|
|
150
|
+
|
|
151
|
+
classes: list[_AssetClass] = []
|
|
152
|
+
for name in wanted:
|
|
153
|
+
# Always append from *this* library — never reuse a same-named group by
|
|
154
|
+
# global lookup, since names collide across tree types (a geometry and a
|
|
155
|
+
# compositor "Combine Spherical" both exist) and we'd introspect the
|
|
156
|
+
# wrong one. Blender renames the appended copy on a clash; that's fine,
|
|
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 (
|
|
159
|
+
src,
|
|
160
|
+
dst,
|
|
161
|
+
):
|
|
162
|
+
dst.node_groups = [name]
|
|
163
|
+
group = dst.node_groups[0]
|
|
164
|
+
|
|
165
|
+
host = bpy.data.node_groups.new("_introspect_host", group.bl_idname)
|
|
166
|
+
try:
|
|
167
|
+
node_type = {
|
|
168
|
+
"GeometryNodeTree": "GeometryNodeGroup",
|
|
169
|
+
"ShaderNodeTree": "ShaderNodeGroup",
|
|
170
|
+
"CompositorNodeTree": "CompositorNodeGroup",
|
|
171
|
+
}[group.bl_idname]
|
|
172
|
+
node = host.nodes.new(node_type)
|
|
173
|
+
node.node_tree = group
|
|
174
|
+
classes.append(
|
|
175
|
+
_AssetClass(
|
|
176
|
+
class_name=_class_name(name),
|
|
177
|
+
asset_name=name,
|
|
178
|
+
description=(group.description or name).strip(),
|
|
179
|
+
library_source=library_source,
|
|
180
|
+
tree_idname=group.bl_idname,
|
|
181
|
+
inputs=_collect(node.inputs),
|
|
182
|
+
outputs=_collect(node.outputs),
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
finally:
|
|
186
|
+
bpy.data.node_groups.remove(host)
|
|
187
|
+
return classes
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _library_source(library: AssetLibrary) -> str:
|
|
191
|
+
"""Source expression that reconstructs ``library`` in the generated module."""
|
|
192
|
+
if isinstance(library, BundledLibrary):
|
|
193
|
+
return f"BundledLibrary({library.filename!r})"
|
|
194
|
+
from ..builder import PackageLibrary
|
|
195
|
+
|
|
196
|
+
if isinstance(library, PackageLibrary):
|
|
197
|
+
return f"PackageLibrary(__file__, {library.relative!r})"
|
|
198
|
+
raise TypeError(f"Cannot serialise asset library: {library!r}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _accessor(sockets: list[_Socket], kind: str) -> str:
|
|
202
|
+
if not sockets:
|
|
203
|
+
return f" class {kind}(SocketAccessor):\n pass"
|
|
204
|
+
lines = [f" class {kind}(SocketAccessor):"]
|
|
205
|
+
for s in sockets:
|
|
206
|
+
lines.append(f" {s.attr}: {s.socket_class}")
|
|
207
|
+
doc = s.name
|
|
208
|
+
if doc and doc != s.attr:
|
|
209
|
+
lines.append(f' """{doc}"""')
|
|
210
|
+
return "\n".join(lines)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _render_class(cls: _AssetClass) -> str:
|
|
214
|
+
base = asset_group_base(cls.tree_idname).__name__
|
|
215
|
+
inputs_cls = _accessor(cls.inputs, "_Inputs")
|
|
216
|
+
outputs_cls = _accessor(cls.outputs, "_Outputs")
|
|
217
|
+
|
|
218
|
+
params = [f"{s.attr}: {s.input_type} = {s.default}" for s in cls.inputs]
|
|
219
|
+
signature = (
|
|
220
|
+
"(\n self,\n " + ",\n ".join(params) + ",\n )"
|
|
221
|
+
if params
|
|
222
|
+
else "(self)"
|
|
223
|
+
)
|
|
224
|
+
key_args = ", ".join(f'"{s.identifier}": {s.attr}' for s in cls.inputs)
|
|
225
|
+
|
|
226
|
+
return f'''class {cls.class_name}({base}):
|
|
227
|
+
"""{cls.description}"""
|
|
228
|
+
|
|
229
|
+
_name = {cls.asset_name!r}
|
|
230
|
+
_asset_name = {cls.asset_name!r}
|
|
231
|
+
_library = {cls.library_source}
|
|
232
|
+
|
|
233
|
+
{inputs_cls}
|
|
234
|
+
|
|
235
|
+
{outputs_cls}
|
|
236
|
+
|
|
237
|
+
if TYPE_CHECKING:
|
|
238
|
+
@property
|
|
239
|
+
def i(self) -> _Inputs: ...
|
|
240
|
+
@property
|
|
241
|
+
def o(self) -> _Outputs: ...
|
|
242
|
+
|
|
243
|
+
def __init__{signature}:
|
|
244
|
+
super().__init__(**{{{key_args}}})
|
|
245
|
+
'''
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _render_module(classes: list[_AssetClass]) -> str:
|
|
249
|
+
socket_classes = sorted(
|
|
250
|
+
{s.socket_class for c in classes for s in c.inputs + c.outputs}
|
|
251
|
+
)
|
|
252
|
+
input_types = sorted({s.input_type for c in classes for s in c.inputs})
|
|
253
|
+
bases = sorted({asset_group_base(c.tree_idname).__name__ for c in classes})
|
|
254
|
+
libraries = sorted(
|
|
255
|
+
{
|
|
256
|
+
"BundledLibrary"
|
|
257
|
+
if c.library_source.startswith("BundledLibrary")
|
|
258
|
+
else "PackageLibrary"
|
|
259
|
+
for c in classes
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
builder_imports = sorted(
|
|
264
|
+
set(bases) | set(libraries) | {"SocketAccessor"} | set(socket_classes)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
lines = [
|
|
268
|
+
"# Auto-generated by nodebpy.assets.generate_asset_api — do not edit manually.",
|
|
269
|
+
"from typing import TYPE_CHECKING",
|
|
270
|
+
"",
|
|
271
|
+
f"from nodebpy.builder import (\n {',\n '.join(builder_imports)},\n)",
|
|
272
|
+
f"from nodebpy.types import (\n {',\n '.join(input_types)},\n)"
|
|
273
|
+
if input_types
|
|
274
|
+
else "",
|
|
275
|
+
]
|
|
276
|
+
header = "\n".join(line for line in lines if line) + "\n\n\n"
|
|
277
|
+
ordered = sorted(classes, key=lambda c: c.class_name)
|
|
278
|
+
body = "\n\n".join(_render_class(c) for c in ordered)
|
|
279
|
+
all_names = ",\n ".join(f'"{c.class_name}"' for c in ordered)
|
|
280
|
+
footer = (
|
|
281
|
+
f"\n\n__all__ = (\n {all_names},\n)\n" if ordered else "\n__all__ = ()\n"
|
|
282
|
+
)
|
|
283
|
+
return header + body + footer
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def generate_asset_api(
|
|
287
|
+
libraries: AssetLibrary | Sequence[AssetLibrary],
|
|
288
|
+
output_path: str | Path,
|
|
289
|
+
*,
|
|
290
|
+
names: set[str] | None = None,
|
|
291
|
+
) -> list[str]:
|
|
292
|
+
"""Generate typed asset classes for ``libraries`` into ``output_path``.
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
libraries:
|
|
297
|
+
One or more :class:`~nodebpy.builder.AssetLibrary` instances
|
|
298
|
+
(:class:`~nodebpy.builder.BundledLibrary` for Blender's bundled assets,
|
|
299
|
+
:class:`~nodebpy.builder.PackageLibrary` for a ``.blend`` shipped inside
|
|
300
|
+
your own package).
|
|
301
|
+
output_path:
|
|
302
|
+
The ``.py`` file to write.
|
|
303
|
+
names:
|
|
304
|
+
Restrict generation to these asset (node-group) names; defaults to all.
|
|
305
|
+
|
|
306
|
+
Returns the list of generated class names.
|
|
307
|
+
"""
|
|
308
|
+
if isinstance(libraries, AssetLibrary):
|
|
309
|
+
libraries = [libraries]
|
|
310
|
+
|
|
311
|
+
classes: list[_AssetClass] = []
|
|
312
|
+
for library in libraries:
|
|
313
|
+
classes.extend(_introspect(library, names))
|
|
314
|
+
|
|
315
|
+
output_path = Path(output_path)
|
|
316
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
317
|
+
output_path.write_text(_render_module(classes), encoding="utf-8")
|
|
318
|
+
return [c.class_name for c in classes]
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
from ._utils import SocketError, denormalize_name, normalize_name
|
|
2
2
|
from .accessor import SocketAccessor
|
|
3
|
+
from .asset import (
|
|
4
|
+
AssetCompositorGroup,
|
|
5
|
+
AssetGeometryGroup,
|
|
6
|
+
AssetLibrary,
|
|
7
|
+
AssetShaderGroup,
|
|
8
|
+
BundledLibrary,
|
|
9
|
+
PackageLibrary,
|
|
10
|
+
asset_group_base,
|
|
11
|
+
)
|
|
3
12
|
from .items import Item, ItemsMixin
|
|
4
13
|
from .mixins import LinkingMixin, OperatorMixin
|
|
5
14
|
from .node import (
|
|
@@ -83,7 +92,14 @@ __all__ = [
|
|
|
83
92
|
"CustomCompositorGroup",
|
|
84
93
|
"CustomGeometryGroup",
|
|
85
94
|
"CustomShaderGroup",
|
|
86
|
-
|
|
95
|
+
# Asset-backed node groups
|
|
96
|
+
"AssetLibrary",
|
|
97
|
+
"BundledLibrary",
|
|
98
|
+
"PackageLibrary",
|
|
99
|
+
"AssetGeometryGroup",
|
|
100
|
+
"AssetShaderGroup",
|
|
101
|
+
"AssetCompositorGroup",
|
|
102
|
+
"asset_group_base",
|
|
87
103
|
# Runtime socket types
|
|
88
104
|
"FloatSocket",
|
|
89
105
|
"FloatSocketGrid",
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Asset-backed node groups.
|
|
2
|
+
|
|
3
|
+
An :class:`AssetNodeGroup` behaves like a :class:`NodeGroupBuilder`, but instead
|
|
4
|
+
of *building* its inner tree it **appends** a named node group from a ``.blend``
|
|
5
|
+
asset library and points a Group node at it. Subclasses (typically generated by
|
|
6
|
+
:func:`nodebpy.assets.generate_asset_api`) declare the library + asset name and a
|
|
7
|
+
typed interface, so an asset reads and type-checks like any other node.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import bpy
|
|
17
|
+
|
|
18
|
+
from .node import (
|
|
19
|
+
CustomCompositorGroup,
|
|
20
|
+
CustomGeometryGroup,
|
|
21
|
+
CustomShaderGroup,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AssetLibrary(ABC):
|
|
26
|
+
"""Locates a ``.blend`` asset library on disk at runtime."""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def path(self) -> str:
|
|
30
|
+
"""Absolute path to the ``.blend`` file."""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BundledLibrary(AssetLibrary):
|
|
35
|
+
"""A node-group asset library shipped with Blender itself, under the system
|
|
36
|
+
datafiles (``…/datafiles/assets/nodes/<filename>``) — e.g. the
|
|
37
|
+
``geometry_nodes_essentials.blend`` essentials library."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, filename: str):
|
|
40
|
+
self.filename = filename
|
|
41
|
+
|
|
42
|
+
def path(self) -> str:
|
|
43
|
+
nodes_dir = os.path.join(
|
|
44
|
+
bpy.utils.system_resource("DATAFILES"), # ty: ignore[unresolved-attribute]
|
|
45
|
+
"assets",
|
|
46
|
+
"nodes",
|
|
47
|
+
)
|
|
48
|
+
return os.path.join(nodes_dir, self.filename)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PackageLibrary(AssetLibrary):
|
|
52
|
+
"""A ``.blend`` shipped inside a Python package, located relative to a module
|
|
53
|
+
file. Pass ``anchor=__file__`` from the generated module and the path to the
|
|
54
|
+
``.blend`` relative to it::
|
|
55
|
+
|
|
56
|
+
PackageLibrary(__file__, "../data/my_assets.blend")
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, anchor: str, relative: str):
|
|
60
|
+
self.anchor = anchor
|
|
61
|
+
self.relative = relative
|
|
62
|
+
|
|
63
|
+
def path(self) -> str:
|
|
64
|
+
return str((Path(self.anchor).parent / self.relative).resolve())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _AssetGroupMixin:
|
|
68
|
+
"""Mixin that turns a ``Custom*Group`` into an asset-backed group: it appends
|
|
69
|
+
``_asset_name`` from ``_library`` (reusing an already-appended group of that
|
|
70
|
+
name) rather than building the tree."""
|
|
71
|
+
|
|
72
|
+
_library: AssetLibrary
|
|
73
|
+
_asset_name: str
|
|
74
|
+
_tree_idname: str # provided by the Custom*Group this is mixed into
|
|
75
|
+
|
|
76
|
+
def _build_group(self, tree) -> None: # pragma: no cover - never called
|
|
77
|
+
raise NotImplementedError(
|
|
78
|
+
"Asset groups are appended from a library, not built."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def create_group(cls):
|
|
83
|
+
"""Append the named asset group from the library and return its tree,
|
|
84
|
+
reusing a previously-appended group of the same name and tree type.
|
|
85
|
+
|
|
86
|
+
The tree-type check matters because asset names collide across editors
|
|
87
|
+
(a geometry and a compositor "Combine Spherical" both exist); reusing by
|
|
88
|
+
name alone could hand back a group of the wrong type.
|
|
89
|
+
"""
|
|
90
|
+
existing = bpy.data.node_groups.get(cls._asset_name)
|
|
91
|
+
if existing is not None and existing.bl_idname == cls._tree_idname:
|
|
92
|
+
return existing
|
|
93
|
+
path = cls._library.path()
|
|
94
|
+
if not os.path.exists(path):
|
|
95
|
+
raise FileNotFoundError(f"Asset library not found: {path}")
|
|
96
|
+
with bpy.data.libraries.load(path, link=False, assets_only=True) as (src, dst):
|
|
97
|
+
if cls._asset_name not in src.node_groups:
|
|
98
|
+
raise KeyError(f"Node group {cls._asset_name!r} not found in {path}")
|
|
99
|
+
dst.node_groups = [cls._asset_name]
|
|
100
|
+
return dst.node_groups[0]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AssetGeometryGroup(_AssetGroupMixin, CustomGeometryGroup):
|
|
104
|
+
"""A Geometry Nodes group appended from an asset library."""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AssetShaderGroup(_AssetGroupMixin, CustomShaderGroup):
|
|
108
|
+
"""A Shader node group appended from an asset library."""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AssetCompositorGroup(_AssetGroupMixin, CustomCompositorGroup):
|
|
112
|
+
"""A Compositor node group appended from an asset library."""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
_ASSET_GROUP_BASE = {
|
|
116
|
+
"GeometryNodeTree": AssetGeometryGroup,
|
|
117
|
+
"ShaderNodeTree": AssetShaderGroup,
|
|
118
|
+
"CompositorNodeTree": AssetCompositorGroup,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def asset_group_base(tree_idname: str) -> type:
|
|
123
|
+
"""The ``Asset*Group`` base class for a tree-type bl_idname."""
|
|
124
|
+
return _ASSET_GROUP_BASE[tree_idname]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
__all__ = [
|
|
128
|
+
"AssetLibrary",
|
|
129
|
+
"BundledLibrary",
|
|
130
|
+
"PackageLibrary",
|
|
131
|
+
"AssetNodeGroup",
|
|
132
|
+
"AssetGeometryGroup",
|
|
133
|
+
"AssetShaderGroup",
|
|
134
|
+
"AssetCompositorGroup",
|
|
135
|
+
"asset_group_base",
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
# Public alias for the mixin, for type hints / isinstance in generated code.
|
|
139
|
+
AssetNodeGroup = _AssetGroupMixin
|