Painless BMeshing with context managers
The cost of using Python is always looking for simpler ways to do things. That applies to Blender too. Recently I found myself copying and pasting the same old BMesh boilerplate from the code and wondering if there was a way to make this less repetitive. Of course there is!
Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.
John F. Woods
My first idea was making a function that takes another function (a High Order Function in Functional lingo). This function would create the bmesh object, run the function it was given and then send the BMesh data to a mesh and free it. But this was kind of limiting. You could only run one thing, or you would have to group them in another function. Another idea was to make a wrapper class but I would still have to manually free it. Besides I’m not a big fan of classes.
Opening a file gave me a better idea: context managers.
Context managers are special objects in Python that allow you to define special runtime contexts. Usually for resources management. You create/open a resources when go inside the context, use it and when you leave the resource gets automatically closed/released.
Consider the open
function/context manager for instance.
# This snippet...
with open('some/file', 'r') as the_file:
do_something(the_file)
# ...is doing this behind the scenes
the_file = open('some/file', 'r')
do_something(the_file)
close(the_file)
A little bit of sugar for the mundane task of opening files. But it automatically takes care of closing files (so we do’t leak descriptors) and groups all the code neatly in a new indentation level. What about BMesh though? A BMesh object isn’t too different from a Python resource. We create it, pass it around and finally send it to a mesh and free it (or let if fall out of scope).
Context managers are defined as classes with three specific methods:__init__
, __enter__
and __exit__
. You can add your own of course, but these are the minimum required. Let’s look at a first implementation.
class Bmesh_from_obj():
def __init__(self, obj):
# Register parameters as class properties
self.obj = obj
def __enter__(self):
# Create and return bmesh object
self.bm = bmesh.new()
depsgraph = bpy.context.evaluated_depsgraph_get()
self.bm.from_object(self.obj, depsgraph)
return self.bm
def __exit__(self, *args):
# Clean up: send bmesh to mesh and free()
self.bm.to_mesh(obj.data)
self.bm.free()
With this we can already use bmesh_obj
as a context manager (in object mode). Note that returning a value in __enter__
is optional, you can create a with
block that doesn’t bind a variable. Also if we hit an exception on __init__
or __enter__
we never go inside the with block. On the other hand, if we go inside the __exit__
method is always called and we can use it to handle exceptions like this:
def __exit__(self, exc_type, exc_value, exc_traceback):
self.bm.to_mesh(obj.data)
self.bm.free()
if exc_type:
print('Oh no')
print(f'{exc_value}. Trace: {exc_traceback}')
I will be leaving exception handling to you though. Every project is different and you might want to handle exceptions earlier or later (letting them go up). Exceptions aside, we can do two more things to improve this: simplify the code and support edit mode. Let’s simplify first.
We can use a decorator on a function instead of writing a whole class. First we need to import @contextmanager
from contextlib. The contextlib module is entirely dedicated to context managers, and it’s documentation is the place to go if you want to get deep into them. How would this function look then?
from contextlib import contextmanager
@contextmanager
def bmesh_from_obj(obj):
# Create bmesh object and yield it
yield bm
# Code inside the with block gets executed here
# Clean up (we're leaving the with block)
Notice we yield bm instead of returning it like we did in the in the __enter__
method before. The decorator needs the function to return a single value generator-iterator which then becomes the target of the with statement (the “as” part). That’s it for initialization. The code inside the with
block gets executed immediately after the yield, and once we reach the end it runs the rest of the function.
Let’s add a mode parameter and flesh this out:
@contextmanager
def bmesh_from_obj(obj, mode):
"""Context manager to auto-manage BMesh."""
if mode == 'EDIT_MESH':
bm = bmesh.from_edit_mesh(obj.data)
else:
bm = bmesh.new()
bm.from_mesh(obj.data)
yield bm
# Send to mesh and clean up
bm.normal_update()
if mode == 'EDIT_MESH':
bmesh.update_edit_mesh(obj.data)
else:
bm.to_mesh(obj.data)
bm.free()
Now we can pass the mode variable from the context and use BMesh in either mode. Here’s an example from the extrusion tutorial.
with bmesh_from_obj(obj, bpy.context.mode) as bm:
# Get geometry to extrude
bm.faces.ensure_lookup_table()
faces = [bm.faces[0]] # For a plane
faces = [bm.faces[5]] # For the top face of the cube# Extrude
extruded = bmesh.ops.extrude_face_region(bm, geom=faces)
# Move extruded geometry
translate_verts = [v for v in extruded['geom'] if isinstance(v, BMVert)]
up = Vector((0, 0, 1))
bmesh.ops.translate(bm, vec=up, verts=translate_verts)
bmesh.ops.delete(bm, geom=faces, context='FACES')
# Remove doubles
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
What if we want to send the BMesh data to a new object? We can add a small utility function to create a new empty object and pass it to the context manager.
def new_obj(obj_name):
"""Add a new object to the scene."""
# Make new object when leaving context manager
mesh = bpy.data.meshes.new(obj_name)
obj = bpy.data.objects.new(obj_name, mesh)
bpy.context.scene.collection.objects.link(obj)
return obj
# (Later)
with bmesh_obj(new_obj('myobj'), bpy.context.mode):
pass
Final code
I’ve made the example a bit more interesting for the final sample. Try it out! This code creates a simple twisted shape. It starts with a plane, extrudes it a couple of times, and rotates and scales some edges. As you can see in the extrude
function we can also abstract some common bmesh operations into functions to reuse code or make it more expressive. Anything can go in the with
block.
import bpy
import bmesh
from mathutils import Vector
from bmesh.types import BMVert
# Create BMesh object
scene = bpy.context.scene
obj = bpy.context.object
depsgraph = bpy.context.evaluated_depsgraph_get()
bm = bmesh.new()
bm.from_object(obj, depsgraph)
# Get geometry to extrude
bm.faces.ensure_lookup_table()
faces = [bm.faces[0]] # For a plane
faces = [bm.faces[5]] # For the top face of the cube
# Extrude
extruded = bmesh.ops.extrude_face_region(bm, geom=faces)
# Move extruded geometry
translate_verts = [v for v in extruded['geom'] if isinstance(v, BMVert)]
up = Vector((0, 0, 1))
bmesh.ops.translate(bm, vec=up, verts=translate_verts)
# Delete original faces
bmesh.ops.delete(bm, geom=faces, context='FACES')
# Remove doubles
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
# Update mesh and free Bmesh
bm.normal_update()
bm.to_mesh(obj.data)
bm.free()
Check out my mesh generation tutorial series if you’re looking for more ideas.
If you are interested in learning more about context managers I recommend hitting the contextlib module documentation. Also cheeck out the original PEP proposal for this feature: PEP343.