Changelog
v520.19.0 - 2026-09-12
Enhancements
- Unified
arrange()API —nodebpy.arrange(tree, method)lays out any node tree, in or outside aTreeBuildercontext.methodis"sugiyama"(layered layout, the default),"simple",None, or aSugiyamaOptions/SimpleOptionsinstance for tuned settings; everyarrange=parameter accepts the same values.default_sugiyama_options(options)scopes what the plain"sugiyama"default resolves to, so batch builds can tune trees whose recipes don’t set an arrangement themselves. - Vendored node-arrange synced and made truly headless — the layout engine is synced with upstream
8ca5e29(cycle-edge fix, Blender 5.0–5.2 socket bindings,optimize_sizes), its module-global state replaced with per-run state, and — critically — it now always arranges the whole tree: the addon-derived code arranged the selection, so a tree loaded from a.blend(no selection) was silently left untouched, and links to unselected nodes were dropped from the layout graph. - Calibrated layout geometry — headless size estimation now skips sockets Blender doesn’t draw (hidden-unlinked), and the row metrics were calibrated against real addon-arranged output (socket-aligned reroutes measure the true socket offsets), fixing nodes estimated ~45% too tall.
SugiyamaOptionsdefaults now match the addon settings validated against MolecularNodes’ hand-arranged trees: 30/30 spacing, top-right alignment, no socket alignment.add_reroutes=Trueroutes long links with reroute nodes (off by default — added reroutes change authored structure). - Structural layout snapshots —
to_python(snapshot_positions=True)emits atree.layout_snapshotblock recording each node’s type, location, frame parent and links; on rebuild, nodes are matched to entries by structure (names only break ties), renamed to their authored names, re-parented into frames and placed. Duplicate-type nodes a rebuild names in a different order previously landed on each other’s authored spots; real libraries (including the 617-node “Curve to Tube”) now round-trip with identical names, positions and link topology. Sockets are recorded by rebuild-stable keys (name + repeat index) instead of history-dependent identifiers, andgroup_input_splitsentries carry each instance’s location and frame parent. - Headless layout plots —
nodebpy.export.to_plot(tree, path)draws a tree’s layout to an image with matplotlib (pip install nodebpy[plot]): real node rectangles, estimated socket-anchored links, frames and reroutes.nodebpy.assets.plot_library(blend, dir, names)/python -m nodebpy.assets plotrender node groups selected by exact name or wildcard ("Style *") to PNGs — for reviewing layouts or posting graphs in pull requests — optionally re-arranged first. - Arrangement on the build CLI —
build_library(andpython -m nodebpy.assets build) acceptsarrange=SugiyamaOptions(...)plusadd_reroutes=True; the CLI exposes every option (--spacing,--iterations,--direction,--socket-alignment,--add-reroutes, …), shared with theplotsubcommand. Snapshot-positions sources keep their authored layout regardless.
Fixes
TreeBuilder.linkread socket endpoints afterlinks.new(handle_dynamic_sockets=True)had freed and recreated them (a reroute retypes its sockets to match the link), an intermittent use-after-free segfault; endpoints are now re-read from the created link. Thegroup_input_splitssetter had the same stale-socket hazard afterlinks.remove.- Arranged node locations are quantized to the 2-decimal dump precision, so arranged trees round-trip losslessly through dump → build.
v250.18.0 - 2026-09-10
Enhancements
- Typed
_build_groupsignatures — generated classes annotate the builder parameter per tree type (def _build_group(self, tree: TreeBuilder[GeometryNodeTree]) -> None:), importing thebpy.typestree class, so the whole method body type-checks and autocompletes in editors;NodeGroupBuilder._build_groupitself is now typedTreeBuilder[T], so hand-writtenCustom*Groupsubclasses inherit the narrowed hint too. - Lint-clean generated output — formatted output now runs
ruff check --fixandruff formatas it is written (sorted imports, double-quoted strings,**{...}collapsed to plain kwargs where socket names allow), so dumped sources need no lint pass afterwards; the dump’s metadata footers and library references emit double-quoted strings directly.
Fixes
- Codegen’s value formatter rendered a one-element tuple without its trailing comma (
("X")— a plain string), so any single-entry sequence value degenerated on rebuild; it now keeps the comma.
v250.17.0 - 2026-09-10
Enhancements
- Dump and build asset libraries —
nodebpy.assets.dump_library(blend, dir)writes every node-group asset in a.blendto its own.pymodule: assets, shared helper groups (under_shared/), code-generated materials (undermaterials/), withASSET_METADATA/MATERIAL_PROPERTIES/DATABLOCK_DEPENDENCIESfooters and the asset catalog file travelling alongside.nodebpy.assets.build_library(dir, blend)rebuilds the.blendfrom those sources, so the Python files can be the version-controlled source of truth.python -m nodebpy.assets dump <blend> <dir>/build <dir> <blend>run both in a fresh session;--typed-apimerges the typed asset API (docstrings,_Inputs/_Outputsaccessors,PackageLibraryanchor) into the dumped classes. A full re-dump clears modules of assets since renamed or deleted, appending refuses sessions whose same-named datablocks would corrupt the dumped names, andbuild_libraryresolves non-serialisable datablocks from the session, aresources=.blend, oron_missing="drop"placeholders — and fails upfront (with a per-tree-directory hint) when two sources build same-named assets. - Library parity auditing —
nodebpy.export.serialize_library/compare_libraries(and thepython -m nodebpy.export.parity a.blend b.blendCLI) deep-compare two asset libraries viatree_clipperserialization, with selectable cosmetic surfaces (positions,reroutes, …) to exclude. Used throughout the test suite to verify dump → build round-trips. - Round-trip fidelity against real libraries — Blender’s bundled geometry / shading / compositing essentials and the MolecularNodes asset library (456 assets together) now round-trip through dump → build to zero functional-parity findings, enforced by tests.
- Curve mappings round-trip — Float Curve, RGB / Vector Curves and Hue Correct nodes with edited curves now emit their full mapping state (point locations, handle types, selection, clip ranges, tone, black/white levels); previously an edited curve silently rebuilt as the default ramp.
- Multiple separate Group Input nodes — trees using the editor convention of one input node per consumer cluster round-trip via
tree.group_input_splits(recorded withsnapshot_positions=True), andtree.split_group_inputs()/TreeBuilder(split_inputs=True)regenerate the style on authored trees. Each split entry moves exactly one link, so parallel links from several instances into one multi-input socket survive. - Same-named sibling panels — Blender allows several same-named panels under one parent;
tree.panel(...)gainedreuse=False(create a fresh panel instead of reusing) and accepts an existing panel (or a previoustree.panel(...)context) to reopen exactly that panel. Codegen uses both spellings so such interfaces rebuild without folding panels together, and empty organizational panels are now emitted too. A mixedtree.panelinsidetree.outputs.panel(...)now nests under it correctly and restores each direction’s own active panel on exit. - Interface menu defaults assigned after the
with TreeBuilder(...)block exits now apply immediately instead of queueing forever, and readingmenu.default_valuereports the pending or applied interface value instead of the stale node socket. - Interface
Font/Soundsocket defaults are now dumped and rebuilt like the other datablock defaults, andis_strip_modifierjoined the round-tripped tree-level properties. - Exposed the
TrimStringnode viaStringSocket.trim(), with codegen round-tripping trees back to the method call - Exposed the
StringToValuenode viaStringSocket.to_float()/.to_integer(base), andIntegerSocket.to_string()now exposes the node’sbaseandpaddingoptions to_pythonnow round-tripssplit(),to_float()/to_integer()and float/integerto_string()as socket method calls instead ofg.SplitString(...)/g.StringToValue.*(...)/g.ValueToString.*(...)factory calls
Fixes
to_pythonrendered datablock values asbpy.data.<collection>["Name"], so a generated script raisedKeyErrorin any session lacking the datablock — they now render as guarded.get("Name")lookups, letting standalone scripts run with the default left empty (the dump/build pipeline still resolves them upfront viaDATABLOCK_DEPENDENCIES).- Zone items (simulation / repeat / for-each) were re-created in socket-identifier order, silently un-reordering items that had been reordered in the editor; they now rebuild in collection order. The zone output’s
inspection_indexround-trips too. - Referencing one of several same-named outputs on a group node used an identifier-derived accessor name (
group.o.socket_0) that broke on rebuild — such outputs are now referenced by position (group.o[2]), which the interface order preserves. value.map_range(...)with a non-default steps value emitted five positional arguments wherestepsis keyword-only, making the generated script fail to run.- An IntegerMath operation with a float operand was lifted to a Python operator that rebuilt as a float
ShaderNodeMathnode; the lift now checks operand types like the other math lifts.
v520.15.0 - 2026-08-28
Enhancements
- Font and Sound sockets everywhere Blender allows them —
tree.inputs.font()/.sound()(and theoutputsequivalents) create interface sockets; typedfont/soundfactories were added toIndexSwitch,MenuSwitch, the repeat zone’sitems, closure zoneinputs/outputs,EvaluateClosureinputs/outputsand Combine / Separate Bundleitems.to_pythonemits the new spellings, andFONTjoined the socket-compatibility table so font links are accepted by the builder. Simulation, For-Each, Bake and Capture Attribute items are unchanged: Blender 5.2 rejects these datablock types there.
v520.15.0 - 2026-08-27
Enhancements
- Menu Switch item descriptions — enum items can now carry the tooltip Blender shows in the menu. A dict item value may be a
(value, description)pair (g.MenuSwitch.geometry(menu, {"Object": (obj, "Use the source object")})), and the newswitch.item(name, value, description=...)helper declares a single item and returns aMenuItemhandle exposing the item’sinputsocket, itsis_selectedboolean output and its (settable)description. Declaring the first item viaitem()defaults the menu selection to it, matching the constructor, and a selection named before any items exist (g.MenuSwitch.geometry("Mesh")) is deferred until the tree is built.to_pythonexport emits the pair form for described items, so descriptions now round-trip instead of being silently dropped.
v520.14.0 - 2026-08-23
Enhancements
- Datablock comparisons — the
Comparenode now covers the datablock types Blender 5.2 can compare in geometry trees:g.Compare.object,.image,.collection,.material,.fontand.soundfactories each offerequal/not_equal(the only operations Blender permits for datablocks) and return a typedCompare[...]whosei.a/i.bcarry the matching socket class.to_pythonexport emits the factory spellings automatically. - ty 0.0.74 / ruff 0.16 migration — the whole repository now passes
ty checkunder the current ty release. The socket class hierarchy was made Liskov-compliant without suppressions,types-networkxtypes the arrange library’s graphs, and code touching the newer bpy stubs (which mark most collections and pointers as optional) narrows explicitly at each call site. Test files relax only the bpy-stub noise rules via[[tool.ty.overrides]]inpyproject.toml. - The full
ruff checkrule set now passes: remaining findings were fixed individually (collapsed conditionals,contextlib.suppress, iterator idioms, a mutableEuler/list argument default, sorted__all__), with the deliberate catch-all exception handlers in probing/repr-fallback code markednoqaexplicitly. - PEP 695 generics — every generic class and function now uses native type-parameter syntax (
class SampleGrid[T](BaseNode)instead ofGeneric[_T]), including the generator’s emitted node classes; the shared module-levelTypeVars are gone, with the socket result-type constraints carried onto each class’s own parameters. The generator also now computes stub-narrowing ignores from the actual enum subsets instead of a hard-coded property-name list, and drops the blanketnode:annotation ignore the current stubs no longer need.
Fixes
- MenuSwitch export in shader and compositor trees —
to_pythonemitted the privateg._MenuSwitchBaseconstructor for a MenuSwitch outside a geometry tree (and for the shader-onlySHADERdata type), producing code that failed to import. The emitter now uses the tree-appropriateMenuSwitchclass (s.MenuSwitch.shader(...)), and the codegen registry prefers a public class over a private base sharing itsbl_idname. - Comparison operators on vector and integer sockets were annotated as returning a
Compare[...]node builder; at runtime they have returned the result socket since v520.x — the annotations now sayBooleanSocket, so(a < b).x-style code type-checks against what actually happens. vector.__rmatmul__gained its missingMatrixSocketoverload, and matrix__rmatmul__accepts raw sockets and numpy arrays in its signature (the runtime always did).
v520.13.0 - 2026-08-20
Enhancements
- Typed item factories for item-driven nodes — the typed per-datatype factories introduced for zones now cover the other items-driven nodes.
CaptureAttribute(...).items.vector("Pos", field)andBake().items.geometry("Geo", source)declare items and return statically typed two-roleItemhandles;FieldToGrid.float(topology).items.float("Density", field)returns a dual-typedGridItemwhosefieldinput andgridoutput each carry their own socket class. CombineBundle().items.float("a", 0.5)/SeparateBundle(bundle).items.float("a")andEvaluateClosure(closure).inputs.geometry("Geo", source)/.outputs.vector("Force")declare bundle and closure-call items with static types, returning the relevant typed socket directly. All bundle/closure item factories acceptstructure_type=; menu items gained factories throughout (including the closure zone’szone.inputs.menu()).- The factory surface is shared infrastructure in
nodebpy.builder.items(_FieldItemFactory,_SocketItemFactory,_SocketValueItemFactory), so new items-driven nodes can adopt it declaratively; the Combine/Separate Bundle constructors moved from generator-inlined source into real mixins innodes/_mixins.py. to_pythonexport emits the typed factories for these nodes (statement form with handle variables for consumed items) instead of theitems={...}dicts. This makes generated code typed and self-documenting, and fixes real losses in the dict form: unlinked bundle/closure item defaults were silently dropped, non-"AUTO"structure_typewas never emitted, and an unlinked capture/bake item whose default couldn’t round-trip type inference (e.g. a color item, or a string default spelling a socket-type name) was re-declared with the wrong type. The dict constructors remain supported as the string-typed fallback, and emission falls back to them for item types without a typed factory.
Fixes
EvaluateClosure’sdefine_signature,active_input_indexandactive_output_indexconstructor parameters now actually reach the Blender node — previously they only set attributes on the Python wrapper, soEvaluateClosure(define_signature=True)silently did nothing. All three are also exposed as node-backed properties.
v520.12.0 - 2026-08-20
Enhancements
- Typed zone item factories — simulation and repeat zones gain a
zone.itemsnamespace with one factory method per data type (zone.items.geometry(),zone.items.float(), …). Each declares a state item and returns aZoneItemhandle whoseinitial/current/next/resultrole sockets are statically typed to the matching socket class, so editors autocomplete and type-check the zone body. An optional second argument links a linkable as the item’s starting value or sets a plain default. The repeat factory additionally offers the datablock and closure types only the repeat zone supports (object,image,collection,material,closure), so invalid simulation item types are caught statically. - The for-each zone gains the same typed factories for its three item collections:
zone.inputs(per-element fields),zone.main(per-element results) andzone.generated(values stored on the generated geometry, withdomain=), plus a typedzone.elementshortcut for the current element geometry. - The closure zone gains
zone.inputs/zone.outputstyped factories that declare signature items (with optionalstructure_type=) and return the body-side socket directly. Item sockets are now resolved by identifier prefix and collection position instead of fragile positional indexing, andItem.input/Item.outputandzone.iteration/zone.indexare properly typed. to_pythonexport now emits the typed factories (repeat_zone.items.float("value", 1.0)instead ofrepeat_zone.item("value", 1.0, type=...)), making generated zone code self-documenting and removing the type-inference drift checks. This also fixes a latent round-trip hazard where a string item whose default spelled a socket-type name (e.g."GEOMETRY") would be re-declared as an item of that type instead of a string default.
Breaking
- The raw bpy item collection is no longer exposed as
.itemson zone input/output builder nodes (it collided with the new typed factory namespace); the string-typedzone.item(...),zone.main_item(...),zone.generated_item(...)andzone.input_item(...)/zone.output_item(...)fallbacks are unchanged.
v520.11.0 - 2026-08-03
Fixes
- Breaking change that fixes the geneartion of class names like
BrighnessContrastwhich previously were being generated asBrightnesscontrastwithout capitalization.
v520.10.0 - 2026-08-03
Fixes
- Asset generation exposes every interface input — introspecting an asset node group skipped inputs that Blender’s socket-usage inference marks inactive under the group’s current node options (e.g. a Menu Switch selection deactivating the inputs of the branches not taken), so those parameters were silently missing from the generated
__init__. All interface inputs are now generated; the bundled essentials APIs were regenerated and pick up the previously hidden menu-gated inputs (e.g. the compositorChromaticAberrationgainsaxis,center,samplesandfit).
v520.9.0 - 2026-07-22
Enhancements
- Generated asset classes now carry numpy-style docstrings (description,
Parameters,Inputs,Outputs) built from the asset’s own socket tooltips, so editors show documentation alongside the type hints. Passdocstrings=Falsetogenerate_asset_api(or--no-docstringstopython -m nodebpy.assets) for the terser output. - Menu sockets on generated asset classes are typed with the items they actually offer —
shape: InputMenu | Literal["Line", "Circle", "Curve", "Transform"]— matching how menu sockets are already typed on the built-in nodes.
v520.8.0 - 2026-07-17
Enhancements
- Group nodes are named after their tree — adding a custom node group (
CustomGeometryGroup/CustomShaderGroup/CustomCompositorGroup, including asset-backed groups) now names the group node after its node tree (e.g.Smooth by Angle,Smooth by Angle.001) instead of Blender’s defaultGroup/Group.001, matching how group assets are named when added from the Add menu.
v520.7.0 - 2026-07-16
Enhancements
- Per-tree-type asset modules —
nodebpy.assets.generate_asset_modules(libraries, output_dir)splits the generated asset classes into one module per tree type (geometry.py/shader.py/compositor.py), writing only the tree types that have assets. Asset names repeat across editors (a geometry and a compositor “Combine Spherical” both exist), so splitting keeps the generated class names collision-free where a singlegenerate_asset_apimodule would silently shadow one with the other. The CLI splits the same way when the output is a directory:
python -m nodebpy.assets -b my_assets.blend -o my_addon/nodes/v520.6.0 - 2026-07-15
Enhancements
- Blender 5.2 stable —
bpynow tracks the final 5.2 release (CI no longer installs daily builds); the node classes and bundled-essentials asset APIs were regenerated against it. TheSetAttachmentSurfaceasset was removed upstream and is no longer generated.
Fixes
to_pythonexport now preserves aValuenode’s number. The editable value lives on the node’s output socket, which the generic constructor path never examined, so non-default values were silently dropped from the generated code.
v520.5.2 - 2026-07-07
Internal
- Documentation cleanup.
v520.5.1 - 2026-07-07
Enhancements
- Interactive graphs in docs / notebooks — a
TreeBuildernow displays as an interactive, Blender-styled, pan-and-zoomable node graph in Jupyter and Quarto (via_repr_html_, backed by the newnodebpy.web_rendermodule usingtree_clipperand thegeonodes-web-renderweb component). Falls back to the existing Mermaid diagram when rendering isn’t available.
Fixes
to_pythonexport: a single link into a multi-input socket is now emitted as a one-element tuple, since the manual classes (JoinGeometry,MeshBoolean, …) expect an iterable — previously such trees didn’t round-trip. Unary float/integer math socket methods also emit Blender’s zero-padded socket identifiers (Value_001).- Regenerated the bundled asset APIs with upstream label fixes — e.g.
cip_start→clip_start,animated_→animated, and the “Super 8 mm” film-grain preset spelling.
v520.5.0 - 2026-06-19
Enhancements
- Changed the linking of asset node groups for the
_AssetGroupMixinto be ‘linked & packed’ by default
v520.4.0 - 2026-06-19
Enhancements
- Chaining nodes with
>>node supportsNonein a chain. Allows for optional insertion of a node given a condition.
from nodebpy import geometry as g
transform = False
with g.tree():
(
g.Cube()
>> g.Array(count=4)
>> (g.TransformGeometry(translation=(1, 1, 1)) if transform else None)
>> g.SetPosition()
)v520.3.0 - 2026-06-18
Enhancements
- Additional socket methods on
FloatSocketandIntegerSocket.
Fixes
- Changed
nodebpyabsolute imports to relative inside the package, and potentially relative / dynamic import for asset generation. Absolute imports would fail when the package is vendored. Does change the behaviour of the node class generation for the imports. FloatGridSocket.to_mesh()method didn’t link any of the input arguments, this now properly links and is tested against
v520.2.0 - 2026-06-16
Enhancements
- Asset node-group APIs — generate typed
nodebpyclasses for node-group assets, so an asset reads, links and type-checks like any other node. Unlike aCustom*Group(which builds its tree), an asset class appends the asset’s node group from a.blendat runtime and points a Group node at it.- Blender’s bundled essentials are generated into
nodebpy.nodes.{geometry,shader,compositor}and exported alongside the built-in nodes, so they’re used exactly like any other node:
from nodebpy import geometry as g mesh = g.SmoothByAngle(mesh=g.Cube(), angle=0.6).o.mesh # an asset, fully typed g.Array(geometry=mesh, count=4)nodebpy.assets.generate_asset_api(library, output_path)generates the same typed classes for your own assets — point it at a.blendshipped in your package viaPackageLibrary(__file__, "…/assets.blend")(orBundledLibrary("…")for a Blender-bundled library) and import the result like any other node module.- New runtime bases
AssetGeometryGroup/AssetShaderGroup/AssetCompositorGroup(parallel to theCustom*Groupbuilders) back these classes; library resolution is handled byBundledLibrary/PackageLibrary.
- Blender’s bundled essentials are generated into
Fixes
- Fixed a bug where
AxesToRotationsilently did not set theprimaryandsecondarywhen instantiating a new node
v520.1.1 - 2026-06-15
Internal
- Code generator refactor — the generator gained a
register_customizationregistry (mirroringcodegen.register_emitter), so node classes that previously had to be hand-written in full insidemanual.pyare now auto-generated, with small reusable mixins or bespoke__init__/factory bodies layered on at generation time. The Bézier handle nodes,Switch, the bundle pack/unpack nodes, the items nodes (Bake,FieldToList,FormatString), and the field-evaluation nodes (AccumulateField,EvaluateAtIndex,FieldAverage,FieldMinAndMax,EvaluateOnDomain,FieldVariance) were moved off the hand-written path. No public API changes. - Generator reorganised into a
gen/package — the monolithicgenerate.pywas split into focused modules (config,customizations,model,introspect,emit,writers), kept outsidesrc/so it never ships in the wheel. Run withpython -m gen(orpython -m gen --only geometryto regenerate a single tree). The skip / hand-written / generate decision is now a singleDispositionderived from the same class-name logic used for generation, introspection is cached so each node is only inspected once, and the generator loads the dependency-freetypesleaf standalone so it can run even when the generated tree is mid-refactor. - The code generator now infers generic typing more completely — generic input sockets that track a node’s data type, and nodes whose output type is fixed while the inputs vary — tightening the type hints on
HashValue,ListLength,ValueToString,FilterList,StoreBundleItem,SetSelection, andStoreNamedGrid.
Fixes
CombineBundle/SeparateBundleitem construction is now part of the generated output, fixing a latent issue where their custom constructors were silently overwritten whenever the node classes were regenerated.
v520.1.0 - 2026-06-15
Enhancements
- Nodes to code (
to_python) —TreeBuilder.to_python()(and the standalonenodebpy.export.to_python()) converts any node tree back into idiomaticnodebpyPython — interface sockets, properties, links, zones, frames and nested groups included. It recognises lifted operators (Math→*,SeparateXYZ→.x), socket methods, factory methods and zone item APIs, so generated code reads like hand-writtennodebpy. Validated end-to-end against Blender’s full bundled geometry, shader and compositor essentials asset libraries, so it round-trips real-world trees, not only ones built withnodebpy. See Nodes to Code. Options:snapshot_positions=True— capture and restore each node’s authoredlocation(top-level and inside nested groups) instead of auto-laying-out the rebuilt tree.keep_reroutes=True— preserve reroute nodes asg.Reroute(...)pass-throughs instead of collapsing each reroute chain into a direct link; pairs withsnapshot_positionsto reproduce the original wire routing.top_level="class"— emit every node group, including the working tree, as aCustom*Groupsubclass, for archiving a set of groups as plain reusable Python. Defaults to thewith TreeBuilder(...) as tree:form.- Nested frames are reconstructed as nested
with g.Frame():blocks (including container frames that hold only sub-frames). format=True(default) runs the output throughruff formatwhen the optionalruffpackage is installed (pip install nodebpy[format]), for tidier source; a no-op whenruffis unavailable.strict=Falseemits a# TODOplaceholder for unsupported nodes;register_emitter(bl_idname)plugs in a custom generator for any node type.
NodeGroupBuilder.create_group()— classmethod that builds and returns a custom group’s node tree without an activeTreeBuildercontext (it opens its own), reusing an existing tree of the same name. Lets a group be pre-built and assigned directly to a node’snode_tree.TreeBuilder.node_positions— a read/write{node name: (x, y)}mapping for snapshotting and restoring node locations, plusTreeBuilder.disable_arrange()to skip the auto-layout that otherwise runs on context exit.- Bundle and closure item APIs —
CombineBundle(items={name: source})/SeparateBundle(bundle, items={name: "TYPE"}),EvaluateClosure(closure, input_items=..., output_items=...), and aClosureZonewrapper (cz.input_item(...),cz.output_item(...),cz.closure) for defining a closure’s body inline. - Colour field evaluation —
ColorSocketgained the domain field-evaluation methods (.point.at(i),.point.evaluate(), …) viaEvaluateAtIndex/EvaluateOnDomain, matching the other socket types. - Socket methods for AlignRotationToVector for
VectorSocketandRotationSocket(align_rotation()andalign_to_vector() - Grid socket operator methods — chainable methods on the
*SocketGridtypes that build and wire up the matching grid node, so grid pipelines can be expressed fluently:- All grids —
sample(position, interpolation),sample_index(x, y, z),field_to_grid(),clip(...),dilate_erode(steps, connectivity, tiles),prune(threshold, mode),voxelize(),to_points() - Float, vector and integer grids —
mean(width, iterations),median(width, iterations) - Float grids —
gradient(),laplacian(),sdf_fillet(),sdf_laplacian(),sdf_mean(),sdf_mean_curvature(),sdf_median(),sdf_offset(),to_mesh() - Vector grids —
curl(),divergence()
- All grids —
grid = g.CubeGridTopology() >> g.FieldToGrid.boolean()
density = grid.capture_float(g.NoiseTexture().o.fac)
flow = density.dilate_erode(1).laplacian().gradient().divergence()Fixes
- Grid
mean()andmedian()now pass the correctdata_type(Blender’sVALUEfloat type is mapped toFLOAT), fixing a crash when calling them on float grids INT_VECTORsockets (e.g. compositor Image Info “Dimensions”) are now link-compatible with regularVECTORsockets, matching Blender’s implicit conversion.- Multi-input sockets (
JoinBundle, …) accept an iterable of sources via their constructor, linking each in turn (asJoinGeometryalready did). - Linking a node into a
Reroute(or any other adaptive__extend__socket, such asViewer) now works from any source type — the reroute adapts instead of rejecting the connection.
Breaking Changes
g.SetHandleType()now defaults toleft=True, right=True(mode = {'LEFT', 'RIGHT'}), matching Blender’s native default for a freshly added node. Previously it defaulted to an emptymode, which set no handle types. The sharedleft/right/modelogic forSetHandleTypeandHandleTypeSelectionwas factored into a mixin;SetHandleTypealso gained amodeproperty for parity.
v520.0.1 - 2026-06-05
Fixes
- Import and usage of the
arrange()function properly handles the optionalnetowrkxdependency
v520.0.0 - 2026-06-04
Enhancements
- Added
leading(),trailling()andtotal()methods fromAccumulateFieldnode onto relevant sockets. Added toFloat,Vector,IntegerandMatrixsockets. - Blender 5.2 support — generated nodes updated to include the nodes for Blender 5.2.
- List socket subtypes — new
*SocketListsocket types matching Blender 5.2’s list sockets, added for every base socket type (FloatSocketList,IntegerSocketList,VectorSocketList,ColorSocketList,BooleanSocketList,RotationSocketList,MatrixSocketList,StringSocketList,MenuSocketList,GeometrySocketList,ObjectSocketList,MaterialSocketList,CollectionSocketList,ImageSocketList, and more). List sockets carry methods for working with the list:list_length()— number of elements, also available vialen()get(index)— retrieve an element (or a sub-list when indexed with anIntegerSocketList) viaGetListItemfilter(selection)— keep elements where selection is truesort(sort_weight, group_id=None, selection=None)— sort viaSortListreverse()— reverse the listlist_slice(start, stop, step)— Python-style slicing, also driven through[]indexing and slicing (e.g.list[::2],list[-3:-1])
indices = g.Index().o.index.to_list(10) # an IntegerSocketList of equal to `range(10)`
evens = indices[::2] # IntegerSocketList via GetListItem
count = len(indices) # IntegerSocket via ListLength
first = indices.get(0) # IntegerSocket- Grid socket subtypes — new
*SocketGridsocket types for volume grids (FloatSocketGrid,IntegerSocketGrid,VectorSocketGrid,BooleanSocketGrid), withtransform(),background_value(), and component indexing. to_list(count)— convert a field socket into a list socket viaFieldToList. Available onFloatSocket,IntegerSocket,VectorSocket,ColorSocket,BooleanSocket,RotationSocket,MatrixSocket,StringSocket, andMenuSocket.- New
StringSocketmethods —uppercase()andlowercase()(viaSetStringCase) andreverse()(viaReverseString).
string = g.String("Example").o.string
string.uppercase() # StringSocket via SetStringCase
string.lowercase() # StringSocket via SetStringCase
string.reverse() # StringSocket via ReverseStringv0.18.0 - 2026-05-20
Added
- Pre-commit hooks for
ruffandtychecks and auto-formatting. tytype checking for the fullsrc/directory for type safety- Convenience methods for
ObjectSocketandCollectionSocket:CollectionSocketinstances(transform_space="ORIGINAL", separate_children=False, reset_children=False)— import objects from the collection as instances, returnsGeometrySocket
ObjectSocket:transform(transform_space="ORIGINAL")— get the transform matrix, returnsMatrixSocketlocation(transform_space="ORIGINAL")— get the location, returnsVectorSocketrotation(transform_space="ORIGINAL")— get the rotation, returnsRotationSocketscale(transform_space="ORIGINAL")— get the scale, returnsVectorSocketgeometry(as_instance=False, transform_space="ORIGINAL")— get the geometry, returnsGeometrySocket
- Added
is_selected()method toMenuSwitch- returns theBooleanSocketfor the named menu item that is true when the item is selected
v0.17.0 - 2026-05-13
Enhancements
- New methods on
VectorSocketfor applying transforms:rotate(rotation)— apply aRotationSocketviaRotateVector, returnsVectorSockettransform(matrix)— apply aMatrixSocketviaTransformPoint, returnsVectorSocket
- New methods on
RotationSocket:rotate(rotation, rotation_space="GLOBAL")— compose rotations viaRotateRotation, returnsRotationSocketto_euler()— convert to XYZ euler angles, returnsVectorSocket(renamed fromeuler())to_quaternion()— decompose viaRotationToQuaternion, returns aQuaternionnamed tuple with.w,.x,.y,.zto_axis_angle()— decompose viaRotationToAxisAngle, returns anAxisAnglewith.axisand.angle
FloatSocket.mix— factory property for creating typedMixnodes driven by this socket as the factor. Supports.float(),.vector(),.color(),.rotation().FloatSocket.map_range()andVectorSocket.map_range()— remap a socket’s values usingMapRange. Supportsfrom_min,from_max,to_min,to_max,clamp,interpolation_type, andsteps.
normalized = value.map_range(0.0, 100.0, 0.0, 1.0)
remapped_vec = vec.map_range((0,0,0), (1,1,1), (-1,-1,-1), (1,1,1))- New methods on
FloatSocket:clamp(min=0.0, max=1.0)— clamp to range viaClampsqrt()— square rootpower(exponent)— raise to a powerfloor()/ceil()/round()— rounding variantsmodulo(divisor)— floored modulo (always non-negative, consistent with Python%)wrap(min, max)— repeat cyclically within a rangeto_radians()/to_degrees()— angle unit conversion
- New methods on
VectorSocket:cross(other)— cross product, returnsVectorSocketdistance(other)— Euclidean distance, returnsFloatSocketproject(other)— project onto another vectorreflect(normal)— reflect around a normal (normal does not need to be normalised)
- New methods on
IntegerSocket:clamp(min=0, max=1)— clamp to integer rangemodulo(divisor)— integer remainder (always non-negative)
- New method on
MatrixSocket:transform_direction(direction)— apply the matrix to a direction vector, ignoring translation. Use this instead oftransform()for normals and tangents.
- Domain factories on
FloatSocket,VectorSocket,IntegerSocket,BooleanSocket,RotationSocket, andMatrixSocket— select a domain property, then call the operation. Each call returns a single typed socket.- Domain properties:
.point,.edge,.face,.corner,.spline,.instance,.layer - All socket types:
.evaluate()— re-evaluate on the domain viaEvaluateOnDomain;.at(i)— retrieve at an index viaEvaluateAtIndex - Float/Vector additionally:
.min(),.max(),.mean(),.median(),.std_dev(),.variance()(with optionalgroup_index) - Integer additionally:
.min(),.max()(with optionalgroup_index)
- Domain properties:
# Field evaluation — works on all socket types
position.face.evaluate() # VectorSocket — position re-evaluated on face domain
flag.point.at(3) # BooleanSocket — flag value at point index 3
rot.edge.evaluate() # RotationSocket
# Statistics — Float / Vector
lo = position.point.min()
mean = curvature.face.mean()
std_dev = weight.point.std_dev(group_index)fac = g.Value(0.5).o.value
fac.mix.float(0.0, 1.0) # FloatSocket
fac.mix.vector((0,0,0), (1,1,1)) # VectorSocket
fac.mix.color(color_a, color_b) # ColorSocketBreaking Changes
RotationSocket.euler()renamed toRotationSocket.to_euler()for consistency with the newto_quaternion()andto_axis_angle()methods.RotationSocket.w,.x,.y,.zcomponent properties removed. Useto_quaternion()instead.- Multi-output socket methods (
to_quaternion(),to_axis_angle(),find(),svd()) now return typedNamedTupleresults. Both named access and positional unpacking are fully typed.
# Named access
rot.to_quaternion().w # FloatSocket
rot.to_axis_angle().angle # FloatSocket
string.find("/").first_found # IntegerSocket
mat.svd().u # MatrixSocket
# Positional unpacking — all variables are specifically typed
w, x, y, z = rot.to_quaternion()
axis, angle = rot.to_axis_angle()
first, count = string.find("/")
u, s, v = mat.svd()FloatSocket.to_integer(rounding_mode="ROUND")— convert to integer viaFloatToInteger. Accepts"ROUND","FLOOR","CEILING", or"TRUNCATE".
v0.16.0 - 2026-05-05
Enhancements
- Input
VectorSocketnow properly has thex,y,zattributes throughCombineXYZnode. - Socket methods added for strings. Methods added are:
length(),starts_with(),ends_with(),contains(),slice(),format(),replace(),find(),join()
string = g.String("Example String").o.string
string.length() # return g.StringLength().o.length, same as len(string)
string.starts_with() # return g.MatchString().o.result
string.ends_with() # return g.MatchString().o.result
string.contains() # return g.MatchString().o.result
string.slice() # return g.SliceString().o.string
string.format() # return g.FormatString().o.string
string.replace() # return g.ReplaceString().o.string
string.find() # return FindResult(first_found, count)
string.join(x) # return g.JoinStrings(x, delimeter=string)String sockets can also be joined with + operator like python strings.
These two are equivalent.
string + "example"
JoinStrings((string, g.String("example")), separator="").o.stringFloat and integer sockets have to_string() methods:
g.Float().o.value.to_string(3) # specify decimal places
g.Integer().o.integer.to_string()- Math, comparison, and unary operations on sockets now return the output socket of the created node rather than the node itself. This allows method chaining directly on the result.
# Before: result was a Math / VectorMath / Compare node
# After: result is a FloatSocket / VectorSocket / BooleanSocket
pos = g.Position().o.position
scaled = pos * 2.0 # VectorSocket
clamped = (scaled > 0.5) # BooleanSocket
mat = g.CombineTransform() @ g.CombineTransform() # MatrixSocket
vec = g.CombineTransform() @ g.Position() # VectorSocketTo access the underlying builder node from any socket returned by a math operation, use the .builder_node property:
result = g.Value(2.0) ** 3.0 # FloatSocket
result.builder_node # the Math node
result.builder_node.i.value_001 # input socket on that node- Accessing
.oor.ion anyBaseNodenow sets.builder_nodeon the returned socket, pointing back to that node.
pos = g.Position().o.position
pos.builder_node # the Position node- Added
svd()method ontoMatrixSocketwhich returns anSVDResultwith.u,.s,.vproperties. - Add
sign()andnegate()methods onto theFloatSocketfor method chaining. Both returnFloatSocket. - Remove
socket_nameproperty fromBaseSocket, already accessible via.socket.name. - Added
.dot(),.length()and.normalize()methods toVectorSocketwhich create the correspondingDotProduct,VectorLengthandNormalizenodes. - Properties on sockets that aren’t just accessing components of the socket are node methods. They still return sockets and not nodes.
RotationSocket.invert->RotationSocket.invert()RotationSocket.euler->RotationSocket.to_euler()MatrixSocket.invert->MatrixSocket.invert()MatrixSocket.transpose->MatrixSocket.transpose()MatrixSocket.determinant->MatrixSocket.determinant()
Breaking Changes
Compare.switch(false, true)with automatic type inference has been removed. Use the explicit typed factory methods onBooleanSocket.switchinstead.
# Before
(val == 5).switch(g.Cube(), g.IcoSphere())
# After
(val == 5).switch.geometry(g.Cube(), g.IcoSphere())
(val == 5).switch.float(0.0, 1.0)
(val == 5).switch.integer(0, 1)- Math operations no longer return nodes — code that accessed node properties directly on the result (e.g.
result.operation,result.data_type,result.i) must now go throughresult.nodeorresult.builder_node:
result = g.Value(2.0) * 3.0
# Before
result.operation # "MULTIPLY"
result.i.value.default_value # 2.0
# After
result.node.operation # "MULTIPLY"
result.builder_node.i.value.default_value # 2.0Bug Fixes
- The
ColorSocketproperly only indexes to length3inside of the shader asSeparateXYZandCombineXYZdon’t have alpha inputs or outputs. - Fixed bug in mixins that was resulting in node comparison creation when checking if a node / socket was
Noneinstead of usingis Nonecomparison. tree()helper functions ingeometry,shader, andcompositormodules now return a typedTreeBuilder[NodeTreeType]for improved type-checker support.- Fixed
BaseNode._from_node()to correctly wrap an existing node without creating and immediately discarding a temporary node.
v0.15.0 - 2026-04-30
Enhancements
- Boolean sockets have a
switchmethod which creates aSwitchnode with the socket as the input. Allows for quick chaining.
b = tree.inputs.boolean()
b.switch.float(0.1, 0.2)
b.switch.geometry(g.Cube(), g.IcoSphere())- Changes to some node methods to better align with naming inside of Geometry Nodes:
EvaluateAtIndex&EvaluateOnDomain:rotation->quaterniontransform->matrix
SampleIndex&SampelCurve:rotation->quaternion
- Handle adding items for the
ColorRampandFloatCurvenodes to create mappins of0..1floats to values and colors.
Bug Fixes
- The
*Socketclasses have been added to the types for checking. - Inputs and outputs properly listed on the
ForEachGeometryElementnodes. JoinStringlinks in the intended order (by first reversing the iterator before linking which is required for multi-input sockets).StoreNamedAttributehas thedomainanddata_typefactor methods properly exposed forStoreNamedAttribute.face.vector().
v0.14.0 - 2026-04-29
Enhancements
- Nodes which previously took
*argsand**kwargshave been updated to use keyword-only arguments instead. This is a hard breaking change but makes the code more readable and less error-prone. (#69)- Affected nodes:
FieldToGrid,JoinGeometry,MenuSwitch,IndexSwitch,CaptureAttribute,JoinStrings,FormatString,SDFGridBoolean,MeshBoolean,RepeatZone,SimulationZone
- Affected nodes:
- Tree interfaces are defined with
tree.inputs.geometry()methods rather than using the old context-baseds.SocketGeometry(). Both systems have been living side-by-side but this completely removed old system so is a hard breaking change. (#67)
# old system
with tree.inputs:
s.SocketGeometry()
# new system
tree.inputs.geometry()Bug Fixes
v0.13.0 - 2026-04-28
Enhancements
- Support adding of closure nodes (
EvaluateClosureand theClosureInput/ClosureOutputnodes). ConvenienceClosureZoneclass is added similar to the repeat, simulation and for-each-element zones. (#60) - Iteration output for the
RepeatZonehas change.i->.iterationto not confuse with input / output socket access (#60) - Add a
Float()class which just wraps theValue()class / node but is better for hinting towards it’s type and more discoverage ([#58](https://github.com/BradyAJohnston/nodebpy/pull/58))
Bug Fixes
v0.12.0 - 2026-04-25
Enhancements
- Support custom node groups for each node tree via
CustomGeometryGroup,CustomShaderGroup,CustomCompositorGroup(#53)
v0.11.1 - 2026-04-24
Bug Fixes
- Fix type inference for the
>>operator in chains, properly propagating the correct node’s return type.
v0.11.0 — 2026-04-24
Enhancements
- Refactor the mermaid diagram generation. Change
screenshot.py->diagram.pyand added test coverage. - Socket iteration and indexing —
VectorSocket,ColorSocket, andMatrixSocketnow support__getitem__,__iter__, and__len__on both output and input sockets. (#48) Output sockets decompose viaSeparateXYZ/SeparateColor/SeparateMatrix(node reuse on repeated access); input sockets auto-wire aCombineXYZ/CombineColor/CombineMatrixand return the component input socket.
for i, axis in enumerate(g.Position().o.position):
math = axis * float(i)
# Pipe a value into the Y component of a position input
g.Value(5.0) >> g.SetPosition().i.position[1]
mat = g.InstanceTransform().o.transform
vec = g.CombineXYZ(*mat[:3])RotationSocket/MatrixSockethelpers — Added.invertand.transposeproperties onMatrixSocket,.invertonRotationSocket, following the same node-reuse pattern as.x/.y/.z.SocketAccessoroverloads —__getitem__and_getare overloaded so slices returnlist[Socket]and str/int keys returnSocket, eliminating theSocket | list[Socket]union that was blockingenumerateand unpacking.- Blender 5.1 compatibility — Generator updated for Blender 5.1:
FontSockettype,Framenode moved tomanually_defined,SVDclass name normalisation, and classmethod param deduplication fix (min_x/min_y/min_zno longer collapsed tomin). (#50) - Precise operator return types — Arithmetic operators on
FloatSocket→Math,VectorSocket→VectorMath,IntegerSocket→IntegerMath. Comparison operators (<,>,<=,>=,==,!=) →Compare. The>>operator is typed viaTypeVarso the right-hand operand’s exact type is preserved through chains. - Generic factory nodes —
AccumulateField,EvaluateAtIndex,FieldAverage,FieldMinAndMax,EvaluateOnDomain,FieldVariance, andCompareare nowGeneric[_T]. Their_Inputs/_Outputsinner classes carry the type parameter so e.g..point.vector(...)returnsFieldAverage[VectorSocket]and.o.meanresolves toVectorSocket.
Bug Fixes
- Fix doc building and will only deploy on tagged releases. (#49)
- Domain factory pattern — All
_domain_factory/ local-class patterns replaced with proper_DomainFactoryinner classes (includingCaptureAttribute) so the type checker can resolve their return types. SocketAccessoridentifier lookup fix — Added a normalised-identifier pass (normalize_name(id)) so attribute access like.i.value_001correctly resolves Blender identifiers such asValue_001that cannot be round-tripped throughdenormalize_name.
v0.10.2 - 2026-04-21
Enhancements
- Added changelog to the documentation to better track and explain changes in the project.
- Support
len(tree.inputs)andlen(tree.outputs)to get the number of inputs and outputs in the tree. (#43) - Added the GPLv3 license to the project.
v0.10.1 — 2026-04-20
Bug fixes
- Fixed
CaptureAttribute.capture()not correctly linking the captured input socket. (#41)
v0.10.0 — 2026-04-19
The biggest release yet. The headline change is a new typed socket accessor API — node.i.x / node.o.x — that replaces the old node.o_position-style properties and brings full IDE auto-complete and type narrowing to socket access.
Enhancements
node.i / node.o socket accessors (#39)
Sockets are now accessed through .i (inputs) and .o (outputs) accessor objects. Attribute names are the normalised socket identifier, so spaces become underscores and the first letter is lowercased.
node.o.position >> node.i.offset # pipe position into offset
node.o.position.y * 0.2 # operate on the y componentIn node definitions, _Inputs / _Outputs inner classes declare the available sockets and their types so IDEs can provide auto-complete:
class SetPosition(BaseNode):
class _Inputs(SocketAccessor):
geometry: GeometrySocket
position: VectorSocket
offset: VectorSocket
class _Outputs(SocketAccessor):
geometry: GeometrySocketNodeGroupBuilder — custom node groups as Python classes (#31)
Define reusable node groups as plain Python classes. The group tree is built once and cached; subsequent uses insert a Group node pointing at that tree.
class Jitter(NodeGroupBuilder):
_name = "Jitter"
_color_tag = "geometry"
def __init__(self, geometry=None, amount=0.2, seed=0):
super().__init__(Geometry=geometry, Amount=amount, Seed=seed)
@classmethod
def _build_group(cls, tree):
geom = tree.inputs.geometry("Geometry")
amount = tree.inputs.float("Amount", 0.2)
seed = tree.inputs.integer("Seed", 0)
offset = g.RandomValue.vector(min=-1, seed=seed) * amount
_ = g.SetPosition(geom, offset=offset) >> tree.outputs.geometry()
# Composes identically with built-in nodes
g.IcoSphere(subdivisions=4) >> Jitter(amount=0.15) >> outg.tree() module-level helper (#36)
Eliminates the need to import TreeBuilder directly when working with a single editor type.
# Before
from nodebpy import TreeBuilder
with TreeBuilder.geometry("My Group") as tree: ...
# After
from nodebpy import geometry as g
with g.tree("My Group") as tree: ...Simplified interface socket definition (#37)
Interface sockets can now be defined directly on the tree object without a context manager:
with g.tree() as tree:
geo = tree.inputs.geometry("Points")
g.SetPosition(geo)The previous context-manager form still works.
Other changes
- Auto-detection of nodes requiring data-type class methods (e.g.
.float(),.vector()) is now more robust. (#38) builder.pywas split into abuilder/package for maintainability;VectorSocketLinkerwas renamed toVectorSocket. (#35)- Internal type aliases cleaned up —
InputFloatreplacesTYPE_INPUT_VALUEetc. (#34)
v0.9.1 — 2026-03-27
Enhancements
== / != comparison operators (#28)
BaseNode objects now support Python equality operators, returning a Compare node. Chain .switch() to immediately branch on the result:
# Creates a Compare node then routes into a Switch
(g.Value(5.0) > 2.0).switch(false=g.Cube(), true=g.IcoSphere())Data-type-specific socket linkers (#29)
VectorSocket, ColorSocket, FloatSocket, and IntegerSocket carry type-specific operations (e.g. .x, .y, .z on vectors) so arithmetic stays typed all the way through a chain.
Other changes
- Documentation styling improvements. (#27)
v0.8.0 — 2026-03-16
Enhancements
networkx is now an optional dependency (#24)
nodebpy now has no hard dependencies outside of bpy, making it easier to vendor into add-ons. A built-in simple arranger is used when networkx is absent; the Sugiyama layout remains the default when it is installed.
v0.7.2 — 2026-03-15
Enhancements
matrix @ vector creates a TransformPoint node (#22)
matrix @ g.Position() # → TransformPoint nodeBug fixes
- Fixed
...(ellipsis) handling in>>chains — type-aware output selection now works correctly when skipping intermediate nodes. (#23)
v0.7.1 — 2026-03-14
Enhancements
Color >> Shader linking (#21)
Piping a color socket into a shader input is now handled automatically, matching the way Blender promotes color connections in the node editor.
v0.7.0 — 2026-03-13
Enhancements
Panels for tree interfaces (#20)
Group interface sockets can be organised into named panels:
with tree.inputs.panel("Settings"):
s.SocketFloat("Amount", 0.2)
s.SocketInt("Seed", 0)Integer math is now handled correctly in Shader and Compositor editors (mapped to float math, as Blender does not expose integer math there).
v0.6.0 — 2026-03-13
Bug fixes
- Fixed
MenuSwitchnode creation and socket wiring. (#18)
Other changes
- Mermaid diagram generation improvements: math node operators are now shown, and socket connections use
->instead of>>. (#19)
v0.5.0 — 2026-03-13
Enhancements
Compositor and Material (Shader) node editors (#12)
nodebpy now supports building Compositor and Shader/Material node trees in addition to Geometry Nodes.
Remaining Python math operators (#15, #16)
** (power), % (modulo), // (floor divide), abs(), and unary - are all wired up. Operator order for vector math was also corrected.