Changelog

v520.1.0 - UNRELEASED

Added

  • Added a general handler that triggers after nodes are added to the tree in the GUI. Sets the default material of style nodes and generates data objects for biological assemblies. #1191

v520.0.1 - 2026-09-06

Fixed

  • Text printing in a jupyter notebook crashed rendering #1185

v520.0.0 - 2026-09-05

Added

  • On using the import operators, the camera and viewport end clipping distance is increased to 10000 from 100 so users aren’t confused when importing larger structures and systems #1150

  • Molecule.add_style() accepts a callable for style, selection and color. The callable is evaluated inside the node tree context, so it can build any nodes from molecularnodes.nodes.geometry without having to write out a whole node tree:

    mol.add_style("cartoon", color=lambda: g.ColorSecondaryStructure())
    mol.add_style("sticks", selection=lambda: g.IsPeptide() & g.IsSideChain())
    mol.add_style(lambda: g.StyleSpheres(sphere="Instance", quality=4))

    A callable style defines the style node itself, so selection, material and style keyword arguments cannot be passed alongside it - passing them raises a TypeError rather than silently dropping them.

  • SelectionManager.node() returns a Named Attribute node for a selection, creating the selection if it does not already exist. This is the bridge between selections and the node tree API, and is how an MDAnalysis selection phrase is used inside a callable style:

    mol.add_style(lambda: g.StyleSpheres(selection=mol.selections.node("not protein")))

    Existing selections are reused - matched on the selection string together with the updating and periodic flags, or on name - so repeated calls do not pile up duplicate selections and mesh attributes.

  • SelectionManager.find() looks up an existing selection by the selection string that created it, where get() looks a selection up by its name.

  • Canvas.look_at() takes a margin, the fraction of the frame to leave empty around the subject. It defaults to 0.05, so renders get a little breathing room rather than the subject sitting against the edge of the frame. Set it to 0 to fit the subject exactly, or go negative to crop in past its edges. #1170

  • molecularnodes.framing solves camera framing on a set of positions. fit_camera_to_points() is the exact solve used by look_at(), with fit_orthographic_to_points() for orthographic cameras and enclosing_sphere() for framing that does not change with viewing angle. It is plain numpy, with no dependency on Blender. #1170

Changed

  • add_style(color=...) now validates its argument. A string must be a known keyword ("common", "default", "plddt") or the name of an existing colour attribute on the molecule. Anything else raises a UserWarning and adds no colour, rather than silently rendering the geometry black. This includes names of attributes that exist but are not colours, such as "b_factor" or "lipophobicity".
  • add_style(style=...) is validated against the styles that can actually be dispatched, and the error lists them.
  • add_style("spheres") picks its sphere to suit the render engine. Point clouds are only ray-traced by Cycles, so under EEVEE the spheres came out as octahedra - invisible at whole-protein zoom and glaring on a close-up. EEVEE now gets "Instance", Cycles keeps "Point", and passing sphere yourself always wins. The engine is read when the style is added, so switching engines afterwards does not revisit it. #1172
  • Breaking: Canvas.scene_reset() is renamed Canvas.load_preset(), which is what it does - it loads a whole preset scene, its lighting, camera, world shader and render settings included. The keep_settings argument is gone, and engine now defaults to the engine the preset defines rather than forcing EEVEE. #1169
  • Creating a Canvas no longer wipes the scene. A template given explicitly is always loaded, but left out the “Molecular Nodes” preset is only loaded into a scene with no molecules in it. A first mn.Canvas() still gets the studio lighting, while re-running the same cell - which marimo does on its own - binds to the scene and leaves the work in it alone. Previously it destroyed every molecule in the scene with no warning. Pass template=None to bind without loading anything. #1169
  • Breaking: Canvas.look_at() accepts any number of positions, where it previously expected the 8 vertices of a bounding box. get_view() returns the positions making up a view rather than 8 bounding-box corners - still a list[tuple], so views combine with + as before. blender.utils.get_bounding_box(), look_at_object() and look_at_bbox() are gone. #1170
  • Canvas.look_at() frames the geometry an entity renders rather than the entity’s bounds, so styling one chain of four frames that chain. Every component of the evaluated geometry is taken into account - a style can output a mesh, a point cloud and instances at once, and an object only ever exposes one of them through its data. Point clouds are grown by their radius and instances by the bounds of what they instance, so spheres are framed by their surface rather than their centres. #1170
  • Canvas.clear() empties the scene of content rather than removing only Molecular Nodes entities. The camera, lights, render settings, world shader and compositor are kept, so the canvas is left ready to render whatever is added next, and only the canvas’s own scene is touched. #1169

Fixed

  • add_style() raised a bare KeyError for style names that were accepted by validation but had no corresponding style node - "vdw", "atoms", "sphere" and "ball+stick" among them. These now raise a ValueError naming the supported styles.
  • Saving a .blend silently lost the entire session if any entity’s object had been deleted, from the outliner or otherwise. The save handler raised before writing the .MNSession file while the .blend itself saved normally, so every other molecule and trajectory in the file lost its state on reload. #1169
  • Opening a second .blend left the previous file’s entities in the session, where they raised a LinkedObjectError on any access. #1169
  • MNSession.remove() raised a LinkedObjectError when the object behind an entity had already been deleted. The entity is now dropped either way, so removing is always safe to call - and a single stale entity can no longer abort a whole Canvas.clear(). #1169
  • Canvas.look_at() pulled the camera back further than the subject needed, by a different amount from each direction. It built a mesh from the 8 corners of an axis-aligned bounding box and handed that to Blender’s camera_to_view_selected operator; an axis-aligned box projects larger than the points inside it, and by an amount that changes with the viewing angle. Framing is now solved directly on the positions, so it is exact and consistent from any direction. #1170
  • Canvas.look_at() silently emptied the frame when given more than 8 positions. It was annotated list[tuple] with no check on how many, and anything but a bounding box left the subject a few pixels wide in the middle of an empty frame. #1170
  • get_view() read bound_box before the depsgraph had caught up, so what it returned depended on whether anything had happened to trigger an evaluation - a molecule read straight after add_style() reported the bounds of its atoms rather than of the style built from them. It also mixed spaces, returning object-local bounds without a selection and world-space ones with. Both are now world-space positions read from evaluated geometry. #1170
  • Canvas.clear() leaked data-blocks. A molecule left behind over a hundred of them - its mesh, material and the node groups backing its styles - which accumulated on every load-and-clear cycle, as only a recursive purge collects node groups hanging off an object’s modifier tree. #1169