A "BasicModalOperator" proof of concept - please review

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)

This is handy for a specific type of operator, but I’m not sure it can be generalized well.

Saving and restoring the entire mesh (or .blend file state) is inefficient if you are only editing a small part of it. So leaving that up to the operator is usually better for performance. It’s also inefficient to reset the mesh for any event, without knowing if that event will actually affect anything.

There are also modal operators that continuously make changes on top of each other, like sculpting or painting. There’s also operators like the knife tool that need more complex confirmation/cancel event handling.

Hello, thanks for responding!

Definitely, the idea is to target that specific type of operator, and maybe try to generalize a little more as a secondary goal. Although it definitely can’t apply to operators like sculpting or knife - thanks for bringing those up.

My motivation is that blender already has an interface that does this type of resetting: After running an operator, blender gives you a way to adjust the parameters, which I assume also uses some type of reset/undo:

What I am envisioning is essentially a modal version of this, with premade shortcuts for confirmation/cancel and operator-specific keybinds, and with more polish than the proof of concept that I gave above.

For example, rather than have a propertyevent() (which I opted for to make my own life easier), a proper blender version could instead opt to attach a keybind directly to the property:

    smoothness = bpy.props.FloatProperty(keybind='MOUSE_X', ...)
    cuts = bpy.props.FloatProperty(keybind='MOUSEWHEEL', ...)

And of course this wouldn’t replace anything, just an additional feature to make it easier to write responsive modal operators.

I would not advise using the undo system for modal operators. It’s not optimized enough for more complex scenes and would make the tool unresponsive.

If it gets more optimized in the future it would make more sense to add a general mechanism like this.

Don’t know what kind of tech operators rely on to undo/redo last when changing parameters, but looks like something is broken in 2.8 eg: complex parenting operations are not property undo (was working in 2.79)

Best to report a bug in the tracker about that, it’s not really relevant to this topic.