BMeshing with context managers

14.09.2018 @ Tutorials(Blender, Python)

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()
        self.bm.from_object(self.obj)

        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=DEL_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.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 bpy.types import Object

from bmesh.types import BMFace, BMVert

from mathutils import Vector
from contextlib import contextmanager

from math import radians
from mathutils import Matrix

# ------------------------------------------------------------------------------
# BMesh Context manager
# ------------------------------------------------------------------------------

@contextmanager
def bmesh_from_obj(obj, mode):
    """Context manager to auto-manage bmesh regardless of mode."""

    if mode == 'EDIT_MESH':
        bm = bmesh.from_edit_mesh(obj.data)
    else:
        bm = bmesh.new()
        bm.from_mesh(obj.data)

    yield bm

    bm.normal_update()

    if mode == 'EDIT_MESH':
        bmesh.update_edit_mesh(obj.data)
    else:
        bm.to_mesh(obj.data)

    bm.free()



# ------------------------------------------------------------------------------
# Bmesh / Utils functions
# ------------------------------------------------------------------------------
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.objects.link(obj)

    return obj


def extrude(bm, faces, direction, remove=True):
    """Extrude a set of faces in a direction"""

    # Extrude
    extruded = bmesh.ops.extrude_face_region(bm, geom=faces)
    translate_verts = [v for v in extruded['geom'] if isinstance(v, BMVert)]

    bmesh.ops.translate(bm, vec=Vector(direction), verts=translate_verts)

    if remove:
        bmesh.ops.delete(bm, geom=faces, context=5)

    return [f for f in extruded['geom'] if isinstance(f, BMFace)]


# ------------------------------------------------------------------------------
# Testing
# ------------------------------------------------------------------------------

# Add a new (empty) object
obj = new_obj('Bmesh test')

# We could also pass an existing object
# obj = bpy.context.object

with bmesh_from_obj(obj, bpy.context.mode) as bm:

    # A grid with segments of 1 is a plane
    bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1)

    # We need to call this since we are accesing faces by index
    bm.faces.ensure_lookup_table()

    faces = [bm.faces[0]]
    new_faces = extrude(bm, faces, (0, 0, 1), False)

    # Keep a copy of these verts for later
    middle_verts = new_faces[0].verts[:]

    # Another extrusion because why not
    top_faces = extrude(bm, new_faces, (0, 0, 3))

    # Give it a thin waist
    bmesh.ops.scale(bm, vec=Vector((0.25, 0.25, 1)), verts=middle_verts)

    # Add a small rotation at the top
    bmesh.ops.rotate(bm, verts=top_faces[0].verts, cent=(0, 0, 0),
                     matrix=Matrix.Rotation(radians(15), 3, 'Z'))

    # Unnecesary but for demo purposes...
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)

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.

All the posts you can read

No comments yet

Leave a Reply

Your email address will not be published.