Is there a recommended option for avoiding fence-post issues (as in waiting for input) when exiting or changing a modal operator in Blender? It's been something I've been trying to find a nice solution for with my add-on. Currently I have been using "cursor_warp" as the rough equivalent to a "tic" event (found in many game engine APIs) to tell Blender to run the modal event loop again, but this feels like a sloppy workaround. When a modal is changed or exited without being followed "cursor_warp" the current key settings and any on-screen messages do not change until a key press or a mouse move is detected to "bump" the event loop.
I know Blender has a lot of modals operators itself and I was curious what the favored solution was.
Rough demonstration of what the problem looks like in GIF form below:
Edit: sample code:
Note, the key areas to look at in this sample are the "main_entry" and "draw_callback_px" functions.
'''
BEGIN GPL LICENSE BLOCK
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
END GPL LICENSE BLOCK
============================================================
Code sample demonstrating a modal that draws a line using Blender 3D's
Python API and OpenGL interface. This script should exit after clicking
the mouse twice, but will stay running until user input causes the event
loop to update. If the "warp_cursor" function call at the bottom of the
main_entry function (line 211) is uncommented, the script will exit
immediately after the second mouse click.
'''
from copy import deepcopy
import bpy
import bmesh
import bgl
from mathutils import Vector
from bpy_extras import view3d_utils
from bpy_extras.view3d_utils import location_3d_to_region_2d as loc3d_to_reg2d
from bpy_extras.view3d_utils import region_2d_to_vector_3d as reg2d_to_vec3d
from bpy_extras.view3d_utils import region_2d_to_location_3d as reg2d_to_loc3d
from bpy_extras.view3d_utils import region_2d_to_origin_3d as reg2d_to_org3d
print("Loaded add-on.\n") # debug
# "Constant" values
(
X,
Y,
Z,
PLACE_1ST,
PLACE_2ND,
EXIT
) = range(6)
class Colr:
red = 1.0, 0.0, 0.0, 0.5
green = 0.0, 1.0, 0.0, 0.5
blue = 0.0, 0.0, 1.0, 0.5
white = 1.0, 1.0, 1.0, 1.0
grey = 1.0, 1.0, 1.0, 0.4
black = 0.0, 0.0, 0.0, 1.0
brown = 0.15, 0.15, 0.15, 0.20
def backup_blender_settings():
backup = [
deepcopy(bpy.context.tool_settings.use_snap),
deepcopy(bpy.context.tool_settings.snap_element),
deepcopy(bpy.context.tool_settings.snap_target),
deepcopy(bpy.context.space_data.pivot_point),
deepcopy(bpy.context.space_data.transform_orientation),
deepcopy(bpy.context.space_data.show_manipulator),
deepcopy(bpy.context.scene.cursor_location)]
return backup
def init_blender_settings():
bpy.context.tool_settings.use_snap = False
bpy.context.tool_settings.snap_element = 'VERTEX'
bpy.context.tool_settings.snap_target = 'CLOSEST'
bpy.context.space_data.pivot_point = 'ACTIVE_ELEMENT'
bpy.context.space_data.transform_orientation = 'GLOBAL'
bpy.context.space_data.show_manipulator = False
return
def restore_blender_settings(backup):
bpy.context.tool_settings.use_snap = deepcopy(backup[0])
bpy.context.tool_settings.snap_element = deepcopy(backup[1])
bpy.context.tool_settings.snap_target = deepcopy(backup[2])
bpy.context.space_data.pivot_point = deepcopy(backup[3])
bpy.context.space_data.transform_orientation = deepcopy(backup[4])
bpy.context.space_data.show_manipulator = deepcopy(backup[5])
bpy.context.scene.cursor_location = deepcopy(backup[6])
return
def draw_pt_2D(pt_co, pt_color):
if pt_co is not None:
bgl.glEnable(bgl.GL_BLEND)
bgl.glPointSize(40)
bgl.glColor4f(*pt_color)
bgl.glBegin(bgl.GL_POINTS)
bgl.glVertex2f(*pt_co)
bgl.glEnd()
return
def draw_line_2D(pt_co_1, pt_co_2, pt_color):
if None not in (pt_co_1, pt_co_2):
bgl.glEnable(bgl.GL_BLEND)
bgl.glPointSize(15)
bgl.glColor4f(*pt_color)
bgl.glBegin(bgl.GL_LINE_STRIP)
bgl.glVertex2f(*pt_co_1)
bgl.glVertex2f(*pt_co_2)
bgl.glEnd()
return
# Creates a mesh object origin to use as a "snapping" point and for plotting
# locations in the 3D Viewport
class SnapPoint():
def __init__(self):
self.mode = "OBJECT"
self.point = None
self.ob = bpy.context.scene.objects
# todo : move outside SnapPoint ?
def get_mouse_3d(self, mous_loc):
region = bpy.context.region
rv3d = bpy.context.region_data
mous_vec3d = reg2d_to_vec3d(region, rv3d, mous_loc)
enterloc = reg2d_to_loc3d(region, rv3d, mous_loc, mous_vec3d)
test2d = loc3d_to_reg2d(region, rv3d, enterloc)
# make sure converted mouse location is visible from the 3D view,
# if not, use less accurate alternative for getting mouses 3D coordinates
if test2d is None:
persp_md_fix = mous_vec3d / 5
enterloc = reg2d_to_org3d(region, rv3d, mous_loc) + persp_md_fix
return enterloc
def create(self, ms_loc_2d, ed_type):
ms_loc_3d = self.get_mouse_3d(ms_loc_2d)
if ed_type == 'OBJECT':
bpy.ops.object.add(type = 'MESH', location = ms_loc_3d)
self.point = bpy.context.object
bpy.ops.transform.translate('INVOKE_DEFAULT')
# Makes sure only the "guide point" object or vert
# added with create is grabbed.
def grab(self, ed_type):
if ed_type == 'OBJECT':
bpy.ops.object.select_all(action='DESELECT')
self.ob[0].select = True
bpy.ops.transform.translate('INVOKE_DEFAULT')
# Makes sure only the "guide point" object or vert
# added with create is deleted.
def remove(self, ed_type):
if ed_type == 'OBJECT':
bpy.ops.object.select_all(action='DESELECT')
self.ob[0].select = True
bpy.ops.object.delete()
self.point = None
def get_co(self, ed_type):
if self.mode == 'OBJECT':
return self.point.location.copy()
def exit_addon(self):
if self.curr_ed_type == 'EDIT_MESH':
bpy.ops.object.editmode_toggle()
self.curr_ed_type = bpy.context.mode
self.snap.remove(self.curr_ed_type)
restore_blender_settings(self.settings_backup)
print("\n\nAdd-On Exited!\n") # debug
def warp_cursor(self, context, dest_co):
if dest_co is None:
return
area = context.area
win = None
for r in area.regions:
if r.type == "WINDOW":
win = r
break
if win is not None:
warpco = dest_co[0] + win.x, dest_co[1] + win.y
context.window.cursor_warp(*warpco)
def main_entry(self, context):
snap = self.snap.get_co(self.curr_ed_type)
if self.stage == PLACE_1ST:
if self.left_click:
self.left_click = False
self.first_pt.append(snap)
self.stage += 1
self.snap.grab(self.curr_ed_type)
elif self.stage == PLACE_2ND:
if self.left_click:
self.left_click = False
self.stage = EXIT
#warp_cursor(self, context, self.mouse_loc)
def draw_callback_px(self, context):
main_entry(self, context)
reg = bpy.context.region
rv3d = bpy.context.region_data
snap = self.snap.get_co(self.curr_ed_type)
snap2d = loc3d_to_reg2d(reg, rv3d, snap)
pts2d = []
line_pts = []
if self.stage == PLACE_2ND:
line_pts = self.first_pt[0], snap
pts2d = [loc3d_to_reg2d(reg, rv3d, i) for i in line_pts]
draw_line_2D(pts2d[0], pts2d[1], Colr.white)
draw_pt_2D(pts2d[0], Colr.white)
draw_pt_2D(snap2d, Colr.green)
class DrawLineTest(bpy.types.Operator):
'''Draw a line with the mouse'''
bl_idname = "view3d.add_on_test"
bl_label = "Draw a Line"
# Only launch Add-On from OBJECT or EDIT modes
@classmethod
def poll(self, context):
return context.mode == 'OBJECT' or context.mode == 'EDIT_MESH'
def modal(self, context, event):
context.area.tag_redraw()
self.curr_ed_type = context.mode
if event.type == 'MOUSEMOVE':
self.mouse_loc = Vector((event.mouse_region_x, event.mouse_region_y))
if event.type in {'RET', 'LEFTMOUSE', 'SPACE'} and event.value == 'RELEASE':
self.left_click = True
if event.type == 'D' and event.value == 'RELEASE':
# start debug console
__import__('code').interact(local=dict(globals(), **locals()))
if event.type in {'ESC', 'RIGHTMOUSE'} and event.value == 'RELEASE':
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
exit_addon(self)
return {'CANCELLED'}
if self.stage == EXIT:
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
exit_addon(self)
return {'FINISHED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
if context.area.type == 'VIEW_3D':
args = (self, context)
# Add the region OpenGL drawing callback
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px,
args, 'WINDOW', 'POST_PIXEL')
if context.mode == 'EDIT_MESH':
bpy.ops.object.editmode_toggle()
bpy.ops.object.select_all(action='DESELECT')
self.curr_ed_type = context.mode # current Blender Editor Type
self.stage = PLACE_1ST
self.left_click = False
self.mouse_loc = Vector((event.mouse_region_x, event.mouse_region_y))
self.first_pt = []
self.snap = SnapPoint()
self.settings_backup = backup_blender_settings()
self.debug_flag = False
context.window_manager.modal_handler_add(self)
init_blender_settings()
self.snap.create(self.mouse_loc, self.curr_ed_type)
print("Add-on started!") # debug
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "View3D not found, cannot run operator")
return {'CANCELLED'}
def register():
bpy.utils.register_class(DrawLineTest)
def unregister():
bpy.utils.unregister_class(DrawLineTest)
if __name__ == "__main__":
register()