BMeshing with context managers

14.09.2018 @ Tutorials(Blender, Python)

The cost of using Python is always look­ing for sim­pler ways to do things. That applies to Blender too. Recent­ly I found myself copy­ing and past­ing the same old BMesh boil­er­plate from the code and won­der­ing if there was a way to make this less repet­i­tive. Of course there is!

Always code as if the guy who ends up main­tain­ing your code will be a vio­lent psy­chopath who knows where you live.

John F. Woods

My first idea was mak­ing a func­tion that takes anoth­er func­tion (a High Order Func­tion in Func­tion­al lin­go). This func­tion would cre­ate the bmesh object, run the func­tion it was giv­en and then send the BMesh data to a mesh and free it. But this was kind of lim­it­ing. You could only run one thing, or you would have to group them in anoth­er func­tion. Anoth­er idea was to make a wrap­per class but I would still have to man­u­al­ly free it. Besides I’m not a big fan of class­es.

Open­ing a file gave me a bet­ter idea: con­text man­agers.
Con­text man­agers are spe­cial objects in Python that allow you to define spe­cial run­time con­texts. Usu­al­ly for resources man­age­ment. You create/open a resources when go inside the con­text, use it and when you leave the resource gets auto­mat­i­cal­ly closed/released.

Con­sid­er the open function/context man­ag­er 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 lit­tle bit of sug­ar for the mun­dane task of open­ing files. But it auto­mat­i­cal­ly takes care of clos­ing files (so we do’t leak descrip­tors) and groups all the code neat­ly in a new inden­ta­tion lev­el. What about BMesh though? A BMesh object isn’t too dif­fer­ent from a Python resource. We cre­ate it, pass it around and final­ly send it to a mesh and free it (or let if fall out of scope).

Con­text man­agers are defined as class­es with three spe­cif­ic meth­ods:
__init__, __enter__ and __exit__. You can add your own of course, but these are the min­i­mum required. Let’s look at a first imple­men­ta­tion.

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 con­text man­ag­er (in object mode). Note that return­ing a val­ue in __enter__ is option­al, you can cre­ate a with block that doesn’t bind a vari­able. Also if we hit an excep­tion on __init__ or __enter__ we nev­er go inside the with block. On the oth­er hand, if we go inside the __exit__ method is always called and we can use it to han­dle excep­tions 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 leav­ing excep­tion han­dling to you though. Every project is dif­fer­ent and you might want to han­dle excep­tions ear­li­er or lat­er (let­ting them go up). Excep­tions aside, we can do two more things to improve this: sim­pli­fy the code and sup­port edit mode. Let’s sim­pli­fy first.

We can use a dec­o­ra­tor on a func­tion instead of writ­ing a whole class. First we need to import @contextmanager from con­textlib. The con­textlib mod­ule is entire­ly ded­i­cat­ed to con­text man­agers, and it’s doc­u­men­ta­tion is the place to go if you want to get deep into them. How would this func­tion 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 return­ing it like we did in the in the __enter__ method before. The dec­o­ra­tor needs the func­tion to return a sin­gle val­ue gen­er­a­tor-iter­a­tor which then becomes the tar­get of the with state­ment (the “as” part). That’s it for ini­tial­iza­tion. The code inside the with block gets exe­cut­ed imme­di­ate­ly after the yield, and once we reach the end it runs the rest of the func­tion.

Let’s add a mode para­me­ter 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 vari­able from the con­text and use BMesh in either mode. Here’s an exam­ple from the extru­sion tuto­r­i­al.

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 util­i­ty func­tion to cre­ate a new emp­ty object and pass it to the con­text man­ag­er.

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 exam­ple a bit more inter­est­ing for the final sam­ple. Try it out! This code cre­ates a sim­ple twist­ed shape. It starts with a plane, extrudes it a cou­ple of times, and rotates and scales some edges. As you can see in the extrude func­tion we can also abstract some com­mon bmesh oper­a­tions into func­tions to reuse code or make it more expres­sive. Any­thing 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 gen­er­a­tion tuto­r­i­al series if you’re look­ing for more ideas.

If you are inter­est­ed in learn­ing more about con­text man­agers I rec­om­mend hit­ting the con­textlib mod­ule doc­u­men­ta­tion. Also cheeck out the orig­i­nal PEP pro­pos­al for this fea­ture: PEP343.

All the posts you can read

No comments yet

Leave a Reply

Your email address will not be published.