from nodebpy import geometry as g
with g.tree("Assets") as tree:
(
g.Cube()
>> g.SmoothByAngle(angle=0.6)
>> g.Array(count=4)
>> tree.outputs.geometry()
)
treeAsset Node Groups
Blender ships node-group assets (the bundled “essentials” libraries), and many add-ons distribute their own. nodebpy can generate typed Python classes for these assets so they read, link and type-check exactly like any built-in node — the only difference is that instantiating one appends the asset’s node group from its .blend at runtime (and points a Group node at it) instead of building the tree from scratch.
This is the asset counterpart to Custom Node Groups: a custom group builds its tree in _build_group; an asset group appends a pre-authored one.
Using the bundled essentials
The classes for Blender’s bundled essentials libraries are generated into nodebpy.nodes.{geometry,shader,compositor} and exported alongside the built-in nodes, so you reach them straight off the editor module:
Each asset is a normal node: typed inputs/outputs (g.SmoothByAngle().o.mesh is a GeometrySocket), >> chaining, IDE autocomplete, and the usual operators. The underlying node group is appended once and reused on subsequent uses.
Generating an API for your own assets
generate_asset_api introspects a .blend asset library and writes a module of typed classes. Point it at your library and a destination .py file:
from nodebpy.assets import generate_asset_api, PackageLibrary
generate_asset_api(
PackageLibrary(__file__, "data/my_assets.blend"),
"my_addon/nodes/assets.py",
)PackageLibrary(anchor, relative) locates a .blend shipped inside your own package (resolved relative to anchor, usually __file__); use BundledLibrary("…") for a library that ships with Blender. You can pass a list of libraries to merge several into one module, and names={...} to restrict generation to specific node groups.
If your libraries mix tree types, use generate_asset_modules instead — it takes a directory and writes one module per tree type (geometry.py, shader.py, compositor.py), skipping tree types with no assets:
from nodebpy.assets import generate_asset_modules, PackageLibrary
generate_asset_modules(
PackageLibrary(__file__, "data/my_assets.blend"),
"my_addon/nodes/",
)Because asset names repeat across editors, splitting also keeps the generated class names collision-free where a single mixed module would silently shadow one tree type’s class with another’s.
How resolution works
Generated classes carry the library reference, not a hard-coded path, and the group is located on demand:
| Library | Resolves to |
|---|---|
BundledLibrary("geometry_nodes_essentials.blend") |
Blender’s system datafiles (…/datafiles/assets/nodes/…) |
PackageLibrary(__file__, "data/my_assets.blend") |
a path relative to the generated module’s file |
Because asset names can collide across editors (a geometry and a compositor “Combine Spherical” both exist), an appended group is reused only when its tree type matches; otherwise the correct one is appended fresh.
Dumping a library to Python source (and back)
dump_library and build_library round-trip a whole .blend asset library through per-asset Python modules, so the .py files — not the binary .blend — can be the version-controlled source of truth:
# | eval: false
from nodebpy.assets import build_library, dump_library
dump_library("my_assets.blend", "my_assets_src/") # one .py per asset
build_library("my_assets_src/", "my_assets.blend") # rebuild the .blendor from the command line (each runs in a fresh Blender session):
python -m nodebpy.assets dump my_assets.blend my_assets_src/
python -m nodebpy.assets build my_assets_src/ my_assets.blendDumping a whole library takes a while, so while iterating on a few assets pass --names (names= in Python) with exact names or wildcards, as for plot below:
python -m nodebpy.assets dump my_assets.blend my_assets_src/ --names "Style *" "Sample Radius"Only the matching assets’ modules are rewritten — plus the _shared/ helper and materials/ modules they depend on — byte-identical to what a full dump would write, since the whole library is still read to decide what is shared. Other assets’ files are left untouched, so edits to them are not picked up: a full dump, or the check subcommand in CI, catches those.
Node groups can also be rendered to PNG images headlessly, styled like Blender’s node editor — for reviewing new or edited nodes in pull requests without opening Blender — selected by exact name or wildcard (plot_library in Python):
python -m nodebpy.assets plot my_assets.blend plots/ "Style *"Each group gets two images: <name>.png shows its internals (header colours per node class, socket markers by type, value widgets, property dropdowns, frames, zones and socket-coloured links) and <name>_node.png shows the group node as a user sees it when adding the group to a tree, with its interface sockets and default values, with interface panels in their default open or closed state (--open-panels expands them all). --tree-only / --node-only keep just one. By default the internals are drawn at their stored layout; pass --arrange (plus any of the arrangement flags shared with build, e.g. --add-reroutes) to re-arrange before plotting. From Python, nodebpy.export.to_plot(tree, path) draws the internals and to_plot(tree, path, node=True) the group node (axes=True frames either with axes ticked in node-location units), and any TreeBuilder offers the same as tree.to_plot(path, ...).
Each asset becomes one module holding its group as a class; helper groups used by a single asset are embedded, groups shared between assets get their own module under _shared/, and referenced materials are code-generated under materials/. Asset metadata (catalog, description, tags), tree-level flags and non-serialisable datablock references (DATABLOCK_DEPENDENCIES) travel in module footers, and a blender_assets.cats.txt next to the .blend is copied along so catalog assignments survive. Pass snapshot_positions=True to keep authored node layouts (including split Group Input instances), and --typed-api / typed_api=True to merge the typed asset API from above into the dumped classes. On the build side the automatic layout is tunable: --add-reroutes / add_reroutes=True arranges the rebuilt trees with reroute nodes routing long links around nodes (the node-arrange addon’s behaviour), and --spacing, --iterations (crossing reduction), --direction, --socket-alignment, --no-sequential-frames, --no-balance-heights, --balance-aspect and the other SugiyamaOptions fields (arrange=SugiyamaOptions(...) in Python) override the defaults; --split-inputs / split_inputs=True gives each consumer node its own Group Input instance — named and labelled after the interface sockets it uses, unused sockets hidden — instead of a single Group Input trailing long noodles. Sources dumped with snapshot_positions keep their authored layout regardless.
Round-trip fidelity is checked with nodebpy.export.compare_libraries / serialize_library (or python -m nodebpy.export.parity a.blend b.blend), which deep-compare two libraries with cosmetic surfaces (positions, reroutes, …) excludable — Blender’s bundled essentials libraries and MolecularNodes round-trip to zero functional findings.
Notes
- The generator is a normal runtime tool — call
generate_asset_apiin a build step, a test, or once by hand; commit the generated module like the rest of your source. - Inputs with non-scalar defaults (vectors, colours) take
Nonein the generated signature; the appended group keeps its own socket defaults. nodebpy’s own bundled-essentials modules are regenerated bypython -m nodebpy.assets(wired intomake generate).