Painless 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. Recently 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 Function in Functional 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. Another 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.

Opening a file gave me a bet­ter idea: con­text man­agers.
Context man­agers are spe­cial objects in Python that allow you to define spe­cial run­time con­texts. Usually 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.

Consider 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).

Context 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()
        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 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 does­n’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). Exceptions 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='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.collection.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. 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()
The resulting mesh generated from Bmesh

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.