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
select_set in three variations for vertices, edges and faces respectively,
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"}, thenselect_flush_modewill neither select or deselect anything,select_flush_modecan select and deselect edges and faces andselect_flush_modecan 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
Selections are complicated. Avoid selections if possible.
Use high level operators such as
bpy.ops.mesh.select_all(action="SELECT")andbpy.ops.mesh.select_all(action="INVERT"), because they’re probably more reliable when it comes to ensuring valid selection state.Avoid setting
selectattributes manually if possible.Be really careful when using
select_flush,select_flush_modeand theselect_setvariants. You’ll have to read this document or the source code to really understand what they do.