Blender’s Python API for selections

Selecting edit mesh vertices, edges and faces

In interactive Blender in edit mode, one can select vertices, edges and faces of a mesh and run operators on the selection. The same can be done via Blender’s Python API, but it’s much more finicky.

First, we create a cube and fetch its edit mesh for the examples to follow.

>>> import bmesh
>>> import bpy
>>>
>>> bpy.ops.mesh.primitive_cube_add()
{'FINISHED'}
>>>
>>> bpy.ops.object.mode_set_with_submode(mode="EDIT")
{'FINISHED'}
>>> cube = bpy.context.object
>>> bm = bmesh.from_edit_mesh(cube.data)
>>> bm.verts.ensure_lookup_table()
>>> bm.edges.ensure_lookup_table()
>>> bm.faces.ensure_lookup_table()
>>>
>>> def current_selection():
...     print(f"Selected vertices:   {[v.index for v in bm.verts if v.select]}")
...     print(f"Deselected vertices: {[v.index for v in bm.verts if not v.select]}")
...     print(f"Selected edges:      {[e.index for e in bm.edges if e.select]}")
...     print(f"Deselected edges:    {[e.index for e in bm.edges if not e.select]}")
...     print(f"Selected faces:      {[f.index for f in bm.faces if f.select]}")
...     print(f"Deselected faces:    {[f.index for f in bm.faces if not f.select]}")
...

As seen in the function body of current_selection, BMesh vertices, edges and faces have a select attribute. These attribute are Booleans, but are much more complicated than it seems.

Valid state

Blender makes several assumptions on the state of a mesh, in particular, the documentation states

  • When an edge is selected, its vertices are selected too.

  • When a face is selected, its edges and vertices are selected.

We will be slightly more precise than Blender’s documentation and will call a selection state that satisfies these conditions valid and otherwise invalid. By contraposition, a selection state is valid if and only if

  • if any vertex of an edge is deselected, the edge is deselected too and

  • if any vertex or edge of a face is deselected, the face is deselected too.

The Blender Python API makes it easy to create invalid state, which can even cause crashes.

Note that

  • selecting the vertices of an edge doesn’t necessarily select the edge,

  • selecting the edges of a face doesn’t necessarily select the face and

  • selecting the vertices of a face doesn’t necessarily select the face’s edges and the face.

For example, in interactive Blender, after

  • switching the default cube to edit mode,

  • setting the mesh select mode exclusively to faces,

  • selecting all faces and

  • deselecting exactly one face,

we see that all vertices and edges remain selected.

Repeating these steps with the mesh select mode set to vertices, edges and faces results in deselecting everything except for the opposite face and its vertices and edges.

Repeating these steps with the mesh select mode set to edges and faces has yet another result.

This shows that selecting is highly context dependent.

Flushing to restore valid selection state

Blender’s Python API has very few restrictions on selections and makes it easy to break valid selection state. Blender’s documentation suggests “flushing” the selection to restore valid selection state. This amounts to

which are all underdocumented as we’ll see below. Blender never explicitly defines what “flushing” is, but one can infer from context that flushing is changing other objects’ selection state in hopes to restore valid selection state. Note that there are usually multiple ways to restore valid state.

Blender’s documentation also mentions “flushing down” and “flushing up”, for example here and here. What they mean is that vertices, edges and faces are ordered as such:

UP

faces
edges
vertices

DOWN

Flushing down from a given “level” means to adjust the selection state of the levels further down to restore valid selection state. Flushing up from a given “level” means to adjust the selection state of the levels further up to restore valid selection state.

For example, suppose that a face is selected, but some of its vertices or edges are deselected. In this context, selecting all vertices and edges that belong to the face would be flushing down.

For another example, suppose that all boundary edges of a face are selected but the face is deselected. In this context, selecting the face would be flushing up.

The Python API is implemented in source/blender/python/bmesh/bmesh_py_types.c (source) and calls functions in source/blender/bmesh/intern/bmesh_marking.c (source).

select_flush

select_flush is implemented by static PyObject *bpy_bmesh_select_flush(BPy_BMesh *self, PyObject *value).

In particular, select_flush(False) corresponds to void BM_mesh_deselect_flush(BMesh *bm) (source) which is more or less the same as

def BM_mesh_deselect_flush(bm):
    for e in bm.edges:
        if not e.hide:
            if e.select:
                if not e.verts[0].select or not e.verts[1].select:
                    e.select = False

    if (there exists a face adjacent to e) and not e.select:
        for f in (faces adjacent to e):
            f.select = False

Note that select_flush(False) cannot select anything.

Its counterpart select_flush(True) corresponds to void BM_mesh_select_flush(BMesh *bm) (source) which is more or less the same as

def BM_mesh_select_flush(bm):
    for e in bm.edges:
        if e.verts[0].select and e.verts[1].select and not e.hide:
            e.select = True

    for f in bm.faces:
        ok = True
        if not f.hide:
            for v in (vertices incident to f):
                if not v.select:
                    ok = False
        else:
            ok = False
        if ok:
            f.select = True

Note that select_flush(True) cannot deselect anything.

Note that both select_flush variants

  • ignore the mesh selection mode (which is a subset of {"VERT", "EDGE", "FACE"}),

  • cannot change the selection state of vertices and

  • always flush upwards.

select_flush_mode

select_flush_mode is implemented by static PyObject *bpy_bmesh_select_flush_mode(BPy_BMesh *self, PyObject *value) and void BM_mesh_select_mode_flush(BMesh *bm).

It is more or less the same as

def BM_mesh_select_mode_flush(bm):
    # The conditions are actually bitwise operations involving constants in
    # source/blender/makesdna/DNA_scene_types.h
    #
    # bm.select mode should always match the mesh select mode, see
    # https://projects.blender.org/blender/blender/src/branch/blender-v3.3-release/source/blender/bmesh/bmesh_class.h#L346
    if "VERT" in bm.select_mode:
        bm_mesh_select_mode_flush_vert_to_edge(bm)

    if ("VERT" in bm.select_mode) or ("EDGE" in bm.select_mode):
        bm_mesh_select_mode_flush_edge_to_face(bm)


def bm_mesh_select_mode_flush_vert_to_edge(bm):
    for e in bm.edges:
        if not e.hide and e.verts[0].select and e.verts[1].select:
            e.select = True
        else:
            e.select = False


def bm_mesh_select_mode_flush_edge_to_face(bm):
    for f in bm.faces:
        ok = True
        if not f.hide:
            if not all(e.select for e in boundary edges of f):
                ok = False
        else:
            ok = False

        f.select = ok

Note that

  • if the mesh select mode equals {"FACE"}, then select_flush_mode will neither select or deselect anything,

  • select_flush_mode can select and deselect edges and faces and

  • select_flush_mode can neither select nor deselect vertices.

select_set

BMesh vertices, edges and faces each have a method sharing the same name called

According to the Blender documentation, all three methods set the selection state of a particular vertex, edge or face and flush the selection down in a reasonable fashion (but this isn’t quite true, as we’ll see in a bit). These methods will never flush up however (this is true).

For example, the select_set for vertices does little more than setting the select attribute, because vertices are already all the way down, so there is nothing to flush down. Therefore if f is a selected face with a corner vertex v, then since v.select_set(False) doesn’t flush up, it won’t deselect f, causing invalid selection state. The edges incident to v also retain their selection state, which is another potential source for invalid selection state.

select_set depends on the mesh select mode

Warning

Blender’s documentation doesn’t mention this, but the mesh select mode affects select_set for edges and faces. This can be verified by reading the source code.

For example, if e is an edge, then e.select_set(False) may deselect its vertices unless they belong to other selected edges, maintaining valid selection state if it was valid before. Setting mesh select mode exclusively to edges

>>> bpy.ops.object.mode_set_with_submode(mode="EDIT", mesh_select_mode={"EDGE"})
{'FINISHED'}
>>> bpy.ops.mesh.select_all(action="SELECT")
{'FINISHED'}
>>> bm.edges[0].select_set(False)
>>> current_selection()
Selected vertices:   [0, 1, 2, 3, 4, 5, 6, 7]
Deselected vertices: []
Selected edges:      [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
Deselected edges:    [0]
Selected faces:      [0, 1, 2, 3, 4, 5]
Deselected faces:    []

results in all vertices remaining selected.

Note

The new selection state won’t be visible in the viewport until we run

>>> bmesh.update_edit_mesh(cube.data)

but we will omit this in the following examples and rely on current_selection. Feel free to update the edit mesh when running the examples yourself.

On the other hand, setting the mesh select mode exclusively to vertices and edges

>>> bpy.ops.object.mode_set_with_submode(mode="EDIT", mesh_select_mode={"VERT", "EDGE"})
{'FINISHED'}
>>> bpy.ops.mesh.select_all(action="SELECT")
{'FINISHED'}
>>> bm.edges[0].select_set(False)
>>> current_selection()
Selected vertices:   [1, 3, 4, 5, 6, 7]
Deselected vertices: [0, 2]
Selected edges:      [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
Deselected edges:    [0]
Selected faces:      [0, 1, 2, 3, 4, 5]
Deselected faces:    []

ends up deselecting the two vertices incident to the deselected edge. The selection is invalid in both cases because any face containing bm.edges[0] should’ve been deselected too. We can restore valid state with

>>> bm.select_flush(False)  # We pass False because we want to deselect faces.
>>> current_selection()
Selected vertices:   [1, 3, 4, 5, 6, 7]
Deselected vertices: [0, 2]
Selected edges:      [2, 5, 6, 7, 8, 9, 11]
Deselected edges:    [0, 1, 3, 4, 10]
Selected faces:      [2, 5]
Deselected faces:    [0, 1, 3, 4]

The select_set method for faces also depends on the mesh select mode.

>>> bpy.ops.object.mode_set_with_submode(mode="EDIT", mesh_select_mode={"EDGE"})
{'FINISHED'}
>>> bpy.ops.mesh.select_all(action="SELECT")
{'FINISHED'}
>>> bm.faces[0].select_set(False)
>>> current_selection()
Selected vertices:   [0, 1, 2, 3, 4, 5, 6, 7]
Deselected vertices: []
Selected edges:      [4, 5, 6, 7, 8, 9, 10, 11]
Deselected edges:    [0, 1, 2, 3]
Selected faces:      [1, 2, 3, 4, 5]
Deselected faces:    [0]

All vertices remain selected. Note that it would have been impossible in interactive Blender to deselect a face with the mesh select mode exclusively set to edges.

A different mesh select mode results in different flushing behaviour.

>>> bpy.ops.object.mode_set_with_submode(
...     mode="EDIT", mesh_select_mode={"VERT", "EDGE", "FACE"}
... )
{'FINISHED'}
>>> bpy.ops.mesh.select_all(action="SELECT")
{'FINISHED'}
>>> bm.faces[0].select_set(False)
>>> current_selection()
Selected vertices:   [4, 5, 6, 7]
Deselected vertices: [0, 1, 2, 3]
Selected edges:      [4, 5, 6, 7, 8, 9, 10, 11]
Deselected edges:    [0, 1, 2, 3]
Selected faces:      [1, 2, 3, 4, 5]
Deselected faces:    [0]

The selection state is invalid in both of the previous two examples, because any face containing one of the deselected edges should also be deselected. Because the mesh is a cube, any edge is contained in two faces and since there are 4 deselected edges, there should be more than one deselected face. Since only a single face is deselected, the selection states are invalid.

Warning

One would think that select_set for faces should always result in valid selection states, because there is no need to flush upwards. This is true for select_set(True), but false for select_set(False), which isn’t mentioned in Blender’s documentation unless this comment in the source code counts as documentation.

Said comment suggests select_flush_mode to restore valid state

>>> bm.select_flush_mode()
>>> current_selection()
Selected vertices:   [4, 5, 6, 7]
Deselected vertices: [0, 1, 2, 3]
Selected edges:      [6, 7, 8, 9]
Deselected edges:    [0, 1, 2, 3, 4, 5, 10, 11]
Selected faces:      [2]
Deselected faces:    [0, 1, 3, 4, 5]

select_flush doesn’t guarantee valid selection state

Suppose all vertices, edges and faces selected and then we deselect one vertex.

>>> bpy.ops.object.mode_set_with_submode(mode="EDIT", mesh_select_mode={"EDGE"})
{'FINISHED'}
>>> bpy.ops.mesh.select_all(action="SELECT")
{'FINISHED'}
>>> bm.verts[0].select_set(False)
>>> current_selection()
Selected vertices:   [1, 2, 3, 4, 5, 6, 7]
Deselected vertices: [0]
Selected edges:      [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
Deselected edges:    []
Selected faces:      [0, 1, 2, 3, 4, 5]
Deselected faces:    []

Since select_flush never flushes up, the selection state is now invalid, because any edge or face containing bm.verts[0] should be deselected too. Since select_flush(True) cannot deselect anything, it doesn’t change the current selection state

>>> bm.select_flush(True)
>>> current_selection()
Selected vertices:   [1, 2, 3, 4, 5, 6, 7]
Deselected vertices: [0]
Selected edges:      [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
Deselected edges:    []
Selected faces:      [0, 1, 2, 3, 4, 5]
Deselected faces:    []

which shows that select_flush doesn’t guarantee valid selection state. Here select_flush(False) accomplishes the intended result

>>> bm.select_flush(False)
>>> current_selection()
Selected vertices:   [1, 2, 3, 4, 5, 6, 7]
Deselected vertices: [0]
Selected edges:      [2, 3, 4, 5, 6, 7, 8, 9, 11]
Deselected edges:    [0, 1, 10]
Selected faces:      [1, 2, 5]
Deselected faces:    [0, 3, 4]

Summary

  1. Selections are complicated. Avoid selections if possible.

  2. Use high level operators such as bpy.ops.mesh.select_all(action="SELECT") and bpy.ops.mesh.select_all(action="INVERT"), because they’re probably more reliable when it comes to ensuring valid selection state.

  3. Avoid setting select attributes manually if possible.

  4. Be really careful when using select_flush, select_flush_mode and the select_set variants. You’ll have to read this document or the source code to really understand what they do.