Hi all,
I like to use custom modal operators in my workflow, but I found the current API pretty tedious to use, especially considering that a lot of modal operators (translate, rotate, extrude, bevel, etc) could benefit from a higher level shared abstraction. Right now I have my own barebones wrapper class to handle this, but I would like a proper version upstreamed for the sake of avoiding duplicate code and making it easier to conform to blender’s interface, hence this post.
Observations of a basic modal operator (or, list of features that can be included in a BasicModalOperator):
- Once the operation starts, all input is locked until the operation finishes (left click/enter) or is cancelled (right click/esc)
- Apart from events that terminate the operation, all other events are used to toggle the operator’s properties
- While the operation is active, blender displays a list of keys that can be used to toggle said properties
- Whenever a property is changed, the operator state is reset and re-executed with the new properties
Questions:
- How expensive is resetting the state on every mousemove event?
- Are there any modal operators that fall outside of this BasicModalOperator abstraction?
- Would it make more sense to change operator invocation instead so that every operator gets realtime modal feedback?
This is the wrapper class I use right now, along with a simple subclass showing how it is used. It’s specific to edit meshes since I can’t figure out how blender’s undo stack works.
import bpy, bmesh
class BasicEditModalOperator(bpy.types.Operator):
bl_options = {'REGISTER', 'UNDO', 'BLOCKING', 'GRAB_CURSOR'}
backupmesh = None
def resetmesh(self, context):
bm = bmesh.from_edit_mesh(context.edit_object.data)
bm.clear()
bm.from_mesh(self.backupmesh)
bmesh.update_edit_mesh(context.edit_object.data)
def execute(self, context):
bm = bmesh.from_edit_mesh(context.edit_object.data)
self.edit(bm)
bmesh.update_edit_mesh(context.edit_object.data)
return {'FINISHED'}
def modal(self, context, event):
if event.type in {'RIGHTMOUSE', 'ESC'}:
self.resetmesh(context)
bpy.data.meshes.remove(self.backupmesh)
return {'CANCELLED'}
elif event.type in {'LEFTMOUSE', 'ENTER'}:
bpy.data.meshes.remove(self.backupmesh)
return {'FINISHED'}
self.resetmesh(context)
self.propertyevent(context, event)
self.execute(context)
return {'RUNNING_MODAL'}
def invoke(self, context, event):
self.backupmesh = bpy.data.meshes.new('_bl_backup_mesh')
bm = bmesh.from_edit_mesh(context.edit_object.data)
bm.to_mesh(self.backupmesh)
self.execute(context)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
class SymmetryModalOperator(BasicEditModalOperator):
bl_idname = "custom.symmetrymodal"
bl_label = "Symmetry"
x = bpy.props.FloatProperty()
def propertyevent(self, context, event):
if event.type == 'MOUSEMOVE':
self.x += (event.mouse_x - event.mouse_prev_x) / 100.0
def edit(self, bm):
bmesh.ops.translate(bm, verts=bm.verts, vec=(self.x, self.x, 0.0))
bmesh.ops.symmetrize(bm, input=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.001)
bpy.utils.register_class(SymmetryModalOperator)