BMesh ReferenceError on edit-mode operator

I’m running into a problem with an edit mode operator.
The script is for cleaning up a dense mesh after you use a knife (or similar) operator on it.

The full script is here:

Script:
import bpy
import bmesh
import random
import mathutils
from bpy.types import Operator
from bpy.props import (
    BoolProperty,
    EnumProperty,
    FloatProperty,
    FloatVectorProperty,
    IntProperty,
)

class FakeEdge:
  v1 = None
  v2 = None

class FakeVert:
  pos = None
  verts = None

class CleanUpEdges(bpy.types.Operator):
    """Clean up selected edges, for example after using the knife tool"""
    bl_idname = "mesh.clean_up_knife_cut"
    bl_label = "Clean up knife cut"
    bl_options = {'REGISTER', 'UNDO'}

    delimit_boundary: BoolProperty(
        default=True
    )
    delimit_existing_seams: BoolProperty(
        default=True
    )
    delimit_intersections: BoolProperty(
        default=True
    )

    min_length: FloatProperty(
        default=0.02,
        min=0,
        soft_max=0.5
    )

    relax_iterations: IntProperty(
        default=3,
        min=0,
        soft_max=20
    )

    neighbor_selection_radius: IntProperty(
        default=1,
        min=1,
        soft_max=20
    )

    neighbor_smooth_factor: FloatProperty(
        default=0.5,
        min=0,
        max=1
    )

    def execute(self, context):

        obj = bpy.context.active_object
        bm = bmesh.from_edit_mesh(obj.data)

        bm.verts.ensure_lookup_table()
        bm.edges.ensure_lookup_table()
        bm.faces.ensure_lookup_table()

        bpy.ops.mesh.select_mode(type="EDGE")

        max_it = len(list(filter(lambda e: e.select, bm.edges)))
        edges = list(filter(lambda e: e.select, bm.edges))

        if self.delimit_intersections:
            for e in edges:
                for v in e.verts:
                    star_count = 0
                    for le in v.link_edges:
                        if le.select:
                            star_count += 1
                    if star_count > 2:
                        e.select = False
                        break

        if self.delimit_existing_seams:
            for e in edges:
                for v in e.verts:
                    for le in v.link_edges:
                        if le.seam and not (le in edges):
                            e.select = False
                            break


        if self.delimit_boundary:
            for e in edges:
                for v in e.verts:
                    if v.is_boundary:
                        e.select = False
                        break

        edges = list(filter(lambda e: e.select, bm.edges))

        fake_verts = dict()
        for e in edges:
            for v in e.verts:
                fake_vert = FakeVert()
                fake_vert = FakeVert()
                fake_vert.verts = []
                fake_vert.verts.append(v)
                fake_vert.pos = v.co
                fake_verts[v] = fake_vert

        fake_vert_list = []
        fake_vert_list.extend(fake_verts.values())
        for fv in fake_vert_list:
            print(fv)

        fake_edges = []
        for e in edges:
            fake_edge = FakeEdge()
            fake_edge.v1 = fake_verts[e.link_loops[0].vert]
            fake_edge.v2 = fake_verts[e.link_loops[0]. link_loop_next.vert]
            fake_edges.append(fake_edge)

        for _ in range(max_it):
            if (len(fake_edges) <= 1):
                break;
            shortest = min(fake_edges, key=lambda e: (e.v1.pos - e.v2.pos).length)
            if ((shortest.v1.pos - shortest.v2.pos).length < self.min_length):
                fake_vert_list.remove(shortest.v1)
                fake_vert_list.remove(shortest.v2)
                new_fake_vert = FakeVert()
                new_fake_vert.pos = (shortest.v1.pos + shortest.v2.pos)/2
                new_fake_vert.verts = []
                new_fake_vert.verts.extend(shortest.v1.verts)
                new_fake_vert.verts.extend(shortest.v2.verts)
                fake_vert_list.append(new_fake_vert)
                fake_edges.remove(shortest)
                for fe in fake_edges:
                    if (fe.v1 is shortest.v1):
                        fe.v1 = new_fake_vert
                    if (fe.v2 is shortest.v2):
                        fe.v2 = new_fake_vert
                    if (fe.v1 is shortest.v2):
                        fe.v1 = new_fake_vert
                    if (fe.v2 is shortest.v1):
                        fe.v2 = new_fake_vert

        for fv in fake_vert_list:
            for v in fv.verts:
                v.co = fv.pos

        bpy.ops.mesh.remove_doubles(threshold=0.0001)

        print(len(edges))

        selection = list(filter(lambda e: e.select, bm.edges))

        for _ in range(self.relax_iterations):
            verts = list(filter(lambda v: v.select, bm.verts))
            locations = dict()
            for v in verts:
                locations[v] = v.co.copy()

            for v in verts:
                neighbors = []
                for l in v.link_loops:
                    if (l.edge.select):
                        neighbors.append(l.link_loop_next.vert)
                if (len(neighbors) == 2):
                    avg_pos = mathutils.Vector()
                    for n in neighbors:
                        avg_pos += locations[n]
                    avg_pos /= len(neighbors)
                    v.co = v.co.lerp(avg_pos, 0.2)

        for _ in range(self.neighbor_selection_radius):
            bpy.ops.mesh.select_more(use_face_step=True)

        if self.delimit_existing_seams:
            sel = list(filter(lambda e: e.select, bm.edges))
            for e in sel:
                if e.seam:
                    for v in e.verts:
                        v.select = False


        selected_verts = list(filter(lambda v: v.select, bm.verts))
        verts_to_smooth = []

        for v in selected_verts:
            if not(v.is_boundary and self.delimit_boundary):
                verts_to_smooth.append(v)

        print(len(verts_to_smooth))

        for e in selection:
            for v in e.verts:
                while v in verts_to_smooth:
                    verts_to_smooth.remove(v)

        smoothing_factor = self.neighbor_smooth_factor
        smoothing_factor = pow(smoothing_factor, 4)
        smoothing_factor /= 2

        for x in range(10):
            bmesh.ops.smooth_vert(bm, verts=verts_to_smooth, factor= smoothing_factor, use_axis_x=True, use_axis_y=True, use_axis_z=True)
            print("smooth")

        if obj.mode == 'EDIT':
            bmesh.update_edit_mesh(obj.data, loop_triangles=True, destructive=True)

        bm.free()

        return {'FINISHED'}



Or on gitlab:

It works fine for one time, maybe a couple of times.
Then it throws the following error:

blender-seams-to-sewing-pattern/op_clean_up_edges.py", line 67, in execute
    bm.verts.ensure_lookup_table()
ReferenceError: BMesh data of type BMesh has been removed

If I then exit edit mode and enter it again, and execute the operator once more, Blender crashes.

I have no idea what is causing this. Anyone have any ideas?

Just toggling between modes seems to fix the problem, so I guess it’s some kind of update issue.

   def execute(self, context):
        bpy.ops.object.mode_set(mode='OBJECT')
        bpy.ops.object.mode_set(mode='EDIT')

Hmm, as far as I know it shouldn’t be possible to be in edit mode without an active object but maybe add a print(obj) right after obj = bpy.context.active_object just to validate that it’s getting the correct object?

I’m grasping at straws here but maybe move bpy.ops.mesh.select_mode(type="EDGE") to before you get obj?

Also do you have more than one object in edit mode when this happens? Any other add-ons running that could be clearing the bmesh?

@Walker This seems to work, but I don’t think it’s a very nice way of doing it. I can be quite a heavy operation on higher poly meshes.

@ckohl_art The object itself seems to be correct, it’s really something in the bmesh itself.

Just had another quick look at it, and basically if you run this code on a mesh in edit mode:

import bpy, bmesh
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
print(bm)

…you will see that bmesh.from_edit_mesh returns the same bmesh object every time the code is run. If you add bm.free() to the code you will get the same issues happening that you are experiencing with your code.

Currently my best guess is that you shouldn’t be trying to free a bmesh object created in edit mode using the Bmesh from_edit_mesh function, but should instead let Blender handle cleaning it up. This does seem to be the correct answer because if you run this code once:

import bpy, bmesh
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
print(bm)
bm.free()

…then run an operator from some other script like looptools it will crash Blender. Although I don’t understand why Blender wouldn’t be checking that the Bmesh object it is destroying is the one that is currently being returned using the from_edit_mesh function and simply ignore any attempt to free it until the user has left edit mode.

Also you might need to use:
bmesh.update_edit_mesh(obj.data)

or if you are adding or removing any geometry then:
bmesh.update_edit_mesh(obj.data, destructive=True)

Huh, that seems strange because the bmesh documentation says that the bmesh gets freed automatically whenever the script finishes so I would think that manually running the free() right at the end of the script would be no different than letting Blender handle the freeing automatically. https://docs.blender.org/api/current/bmesh.types.html#bmesh.types.BMesh.free

use this basic structure

import bpy,bmesh
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
bm.normal_update()
---CODE
bmesh.update_edit_mesh(obj.data)

and use a poll() in in your operator to check edit mode
normal update is maybe not needed and its position can change…

If you were creating the bmesh object using bmesh.new() then I believe it’s fine to call the bmesh free method on your object once you have finished with it. However it seems the bmesh object that is returned using the static method bmesh.from_edit_mesh is only created once, either when the users enters edit mode or whenever the first script calls the method. It appears to be only meant to be freed by Blender once the user leaves edit mode, at least that is my current understanding of it.

A method taken from the looptools addon:

def initialise():
    object = bpy.context.active_object
    if 'MIRROR' in [mod.type for mod in object.modifiers if     mod.show_viewport]:
        # ensure that selection is synced for the derived mesh
        bpy.ops.object.mode_set(mode='OBJECT')
        bpy.ops.object.mode_set(mode='EDIT')
    bm = bmesh.from_edit_mesh(object.data)

    bm.verts.ensure_lookup_table()
    bm.edges.ensure_lookup_table()
    bm.faces.ensure_lookup_table()

    return(object, bm)

The only bmesh objects the looptool addon seems to be calling the free method explicitly on are the ones created by doing:
bm_mod = bm.copy()

bm_mod.free()