How do to indicate that the operator "rotate.view" completed its action?

I’m developing a Modal Operator that when pressing the ‘X’ button, enables a function in the 3D View.

The problem is that with the ‘PASS_THROUGH’ event.type, the ‘X’ button opens the “Delete” window, as you know.

So I must return the ‘RUNNING_MODAL’. Unfortunately with “context.window_manager.modal_handler_add(self)” it disables the utility: “Orbit the view”.

So in ‘MIDDLEMOUSE’ event I want to enable again. And I can with the “bpy.ops.view3d.rotate()”. But then I lose the detection event.value ‘RELEASE’. Why?

import bgl, bpy, bmesh, mathutils
from mathutils import Vector

"""Functions for Project and UnProject"""
def get_viewport():
    view = bgl.Buffer(bgl.GL_INT, 4)
    bgl.glGetIntegerv(bgl.GL_VIEWPORT, view)
    return view


def get_modelview_matrix():
    model_matrix = bgl.Buffer(bgl.GL_DOUBLE, [4, 4])
    bgl.glGetDoublev(bgl.GL_MODELVIEW_MATRIX, model_matrix)
    return model_matrix


def get_projection_matrix():
    proj_matrix = bgl.Buffer(bgl.GL_DOUBLE, [4, 4])
    bgl.glGetDoublev(bgl.GL_PROJECTION_MATRIX, proj_matrix)
    return proj_matrix


def get_depth(x, y):    
    depth = bgl.Buffer(bgl.GL_FLOAT, [0.0])
    bgl.glReadPixels(x, y, 1, 1, bgl.GL_DEPTH_COMPONENT, bgl.GL_FLOAT, depth)
    return depth

#mouse_coords_to_3D_view
def UnProject(x, y):    
    depth = get_depth(x, y)
    #if (depth[0] != 1.0):
    world_x = bgl.Buffer(bgl.GL_DOUBLE, 1, [0.0])
    world_y = bgl.Buffer(bgl.GL_DOUBLE, 1, [0.0])
    world_z = bgl.Buffer(bgl.GL_DOUBLE, 1, [0.0])
    view1 = get_viewport()
    model = get_modelview_matrix()
    proj = get_projection_matrix ()   
    bgl.gluUnProject(x, y, depth[0], 
                     model, proj,
                     view1,
                     world_x, world_y, world_z)
    return (world_x[0], world_y[0], world_z[0])


"""Other Functions"""
def Project(x, y, z):
    world_x = bgl.Buffer(bgl.GL_DOUBLE, 1, [0.0])
    world_y = bgl.Buffer(bgl.GL_DOUBLE, 1, [0.0])
    world_z = bgl.Buffer(bgl.GL_DOUBLE, 1, [0.0])
    view3 = get_viewport()
    model1 = get_modelview_matrix()
    proj1 = get_projection_matrix () 
    bgl.gluProject(x, y, z, model1, proj1, view3, world_x, world_y, world_z)
    return (int(world_x[0]), int(world_y[0]))


def close_point_range(mcursor, vertices, location):
    obj = bpy.context.active_object
    
    if isinstance(vertices, bmesh.types.BMVert):
        bgl.glColor4f(1.0, 0.0, 0.0, 1.0)
        vert = obj.matrix_world * vertices.co
        return vert    
    elif isinstance(vertices, bmesh.types.BMEdge):        
        #length = vertices.calc_length()
        vert0 = obj.matrix_world * vertices.verts[0].co
        vert1 = obj.matrix_world * vertices.verts[1].co
        Pvert0 = Project(*vert0)
        Pvert1 = Project(*vert1)
        Lv0 = (Vector(Pvert0) - Vector(mcursor))        
        Lv1 = (Vector(Pvert1) - Vector(mcursor))
        Lvv = (Vector(Pvert1) - Vector(Pvert0))
        Lx0 = Lv0[0]**2+Lv0[1]**2
        Lx1 = Lv1[0]**2+Lv1[1]**2
        Lxv = Lvv[0]**2+Lvv[1]**2
        factor = (0.5*(Lx0-Lx1)/Lxv)+0.5
        if abs(Lx0-Lx1) < 500:
            bgl.glColor4f(1.0, 0.0, 1.0, 1.0)
            return (vert0+vert1)/2        
        else:
            bgl.glColor4f(0.0, 0.5, 1.0, 1.0)            
            return vert0 + (vert1-vert0)*factor            
    else:
        return location


"""drawing point OpenGL in mouse_coords_to_3D_view"""
def draw_callback_px(self, context):
    # mouse coordinates relative to 3d view
    x, y = self.mouse_path
    vertices = self.geom
    # mouse coordinates relative to Blender interface
    view = get_viewport()
    gmx = view[0] + x
    gmy = view[1] + y    
    
    depth1 = get_depth(gmx, gmy)
    mouse3d = UnProject(gmx, gmy)
    
    # draw 3d point OpenGL in the 3D View
    bgl.glEnable(bgl.GL_BLEND)
    bgl.glColor4f(1.0, 0.8, 0.0, 1.0)
    self.snap3d = close_point_range((gmx, gmy),vertices, Vector(mouse3d))
    #print(snap3d)
    
    bgl.glDepthRange(0,0)    
    bgl.glPointSize(10)    
    bgl.glBegin(bgl.GL_POINTS)
    bgl.glVertex3f(*self.snap3d)
    bgl.glEnd()
    bgl.glDisable(bgl.GL_BLEND)
    
    # draw 3d line OpenGL in the 3D View
    bgl.glEnable(bgl.GL_BLEND)
    bgl.glDepthRange(0,0.9999)
    bgl.glColor4f(1.0, 0.8, 0.0, 1.0)    
    bgl.glLineWidth(2)


    bgl.glBegin(bgl.GL_LINE_STRIP)
    self.mouse_line = (self.lpoints, self.snap3d)
    #print(self.snap3d)
    
    for [a, b, c] in self.lpoints:
        bgl.glVertex3f(a, b, c)
        
    bgl.glVertex3f(*self.snap3d)
        
    bgl.glEnd()
    # restore opengl defaults
    bgl.glDepthRange(0,1)
    bgl.glPointSize(1)
    bgl.glLineWidth(1)
    bgl.glDisable(bgl.GL_BLEND)
    bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
    
    context.area.header_text_set("hit: %.3f %.3f %.3f" % tuple(self.snap3d))
    
class PanelSnapUtilities(bpy.types.Panel) :
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    bl_context = "mesh_edit"
    bl_category = "Mano-Addons"
    bl_label = "snap utilities"
    
    @classmethod
    def poll(cls, context):
        return (context.object is not None and
                context.object.type == 'MESH' and
                context.object.data.is_editmode)


    def draw(self, context) :
        layout = self.layout
        TheCol = layout.column(align = True)                  
        TheCol.operator("action.snaputilities", text = "snaputilities")


class ModalDrawOperator(bpy.types.Operator):
    """Draw a point with the mouse"""
    bl_idname = "action.snaputilities"
    bl_label = "Simple Modal View3D Operator"       
        
    def modal(self, context, event):
        context.area.tag_redraw()


        if event.type == 'MOUSEMOVE':            
            self.mouse_path = (event.mouse_region_x, event.mouse_region_y)    
            
            if self.lshift:
                bpy.ops.view3d.select(extend=False, location=(self.mouse_path))                           
             
            try:
                self.geom = self.bm.select_history[0]
            except IndexError:
                self.geom = None
                
        elif event.type == 'LEFTMOUSE':
            self.geom2 = self.bm.select_history[0]
            self.lpoints.append((self.snap3d))
            print(self.geom2)
            #bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
            #context.area.header_text_set()
            return {'PASS_THROUGH'}


        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
            self.lpoints[:] = []
            context.area.header_text_set()
            return {'CANCELLED'}
        
        if event.type == 'MIDDLEMOUSE':            
            bpy.ops.view3d.ndof_orbit()
            #bpy.ops.view3d.view_pan(type='PANLEFT')
            #bpy.ops.view3d.ndof_pan()
        
        elif event.type in {'RIGHT_SHIFT', 'LEFT_SHIFT'}:            
            self.lshift = event.value != 'PRESS'
            
        elif event.type == 'X':            
            self.kX = event.value == 'RELEASE'


        return {'RUNNING_MODAL'}


    def invoke(self, context, event):        
        self.lshift = True
        self.kX = False
        # the arguments we pass the the callback
        args = (self, context)
        # Add the region OpenGL drawing callback
        # 'POST_PIXEL', 'POST_VIEW' or 'PRE_VIEW'
        self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_VIEW')
        self.lpoints = []
        obj = bpy.context.active_object
        self.bm = bmesh.from_edit_mesh(obj.data)
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}
    
    #def execute(self, context):
        #bmesh.ops.extrude_vert_indiv(self.bm, verts=[self.geom])
        #return {"FINISHED"}


def register():
    bpy.utils.register_class(PanelSnapUtilities)
    bpy.utils.register_class(ModalDrawOperator)
    bpy.types.Action.snaputilities = bpy.props.StringProperty(name="")


def unregister():
    bpy.utils.register_class(PanelSnapUtilities)
    bpy.utils.unregister_class(ModalDrawOperator)
    del bpy.types.Action.snaputilities


if __name__ == "__main__":
    register()

The problem is in “context.window_manager.modal_handler_add(self)”.
Here is the summarized script:

import bpy
class ModalDrawOperator(bpy.types.Operator):

    """context.window_manager.modal_handler_add(self)"""
    """Try to orbit in 3D view ('MIDDLEMOUSE' button)"""
    """You can't use return {'PASS_THROUGH'} in modal"""

    bl_idname = "view3d.with_modal_handler_add"
    bl_label = "1 Simple View3D Operator With self Modal Handler"

    def modal(self, context, event):
        if event.type == 'MOUSEMOVE':
            return {'PASS_THROUGH'}
        elif event.type == 'LEFTMOUSE':
            return {'FINISHED'}
        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            return {'CANCELLED'}

        return {'RUNNING_MODAL'} # It has to be {'RUNNING_MODAL'}

    def invoke(self, context, event):        
        context.window_manager.modal_handler_add(self) # Here the problem
        return {'RUNNING_MODAL'}        

def register():
    bpy.utils.register_class(ModalDrawOperator)

if __name__ == "__main__":
    register()

    # test call
bpy.ops.view3d.with_modal_handler_add('INVOKE_DEFAULT')

Someone?

Unfortunately when an operator is “RUNNING_MODAL” that means that it’s exclusive and single and “PASS_THROUGH” that is on background while allowing everything else work as by default.

Two solutions exist, one is to remap all of the keys when the “PASS_THROUGH” operator runs so you can disable any other normal functionality Blender has by default, for example you might disable a few keystrokes and re enable them when the operator finishes.

Second and most easy approach would be to use other keys instead of the default ones (e.g. ALT-X), it might be difficult to get a free space though and it might break the perfect alignment but it’s painless.

However if anyone has a better idea about what to do, I am interested to know that too because I face a similar problem with one script. :slight_smile:

Thank @const, but it is not only the X button that bothers me. Also when I click on a face in Edit Mode and translate it. Or when I click in Tab and exits the Edit mode without converting the Bmesh.

RUNNING_MODAL is quite useful. I can’t use the PASS_THROUGH. :frowning:

I “solved” using the conditional below:

if event.type in {'WHEELDOWNMOUSE', 'WHEELUPMOUSE', 'MIDDLEMOUSE'}:            
            return {'PASS_THROUGH'}

I do not know if this is the best solution. I still need to find out a way to detect if the event.value of event.type ‘MIDDLEMOUSE’, is ‘RELEASE’. (With the ‘PASS_THROUGH’ only detects the event.value ‘PRESS’.)

How to do this?

Here is a demonstration of the addon I’m doing:

I just need to resolve this problem: detect ‘RELEASE’ with ‘PASS_THROUGH’.

I can do this:


if event.type == 'MIDDLEMOUSE':
    print(event.value)
    if event.shift:
        bpy.ops.view3d.move('INVOKE_DEFAULT')
            
    else:
        bpy.ops.view3d.rotate('INVOKE_DEFAULT')

But it still doesn’t detect the event.value ‘RELEASE’. Why?

I can detect the ‘RELEASE’ if I continually compare the View_Matrix.
Once the ‘MIDDLEMOUSE’ is pressed, a copy is made of the View_Matrix’s value. When the operator rotate or move is complete, A comparison is made between the copied View_Matrix’s value and the current View_Matrix’s value.
If they are different (they are), it is print the string ‘MIDDLEMOUSE WAS RELEASE’.


import bpy
class ModalDrawOperator(bpy.types.Operator):


    """context.window_manager.modal_handler_add(self)"""
    """Try to orbit in 3D view ('MIDDLEMOUSE' button)"""
    """You can't use return {'PASS_THROUGH'} in modal"""


    bl_idname = "view3d.with_modal_handler_add"
    bl_label = "1 Simple View3D Operator With self Modal Handler"


    def modal(self, context, event):


        if self.rv3d.view_matrix != self.rotMat:
            self.rotMat = self.rv3d.view_matrix
            print('MIDDLEMOUSE WAS RELEASE')


        if event.type == 'MIDDLEMOUSE':
            self.rotMat = self.rv3d.view_matrix.copy()
            if event.value == 'PRESS':
                if event.shift:
                    bpy.ops.view3d.move('INVOKE_DEFAULT')
                else:
                    bpy.ops.view3d.rotate('INVOKE_DEFAULT')
                    
        elif event.type == 'MOUSEMOVE':
            return {'RUNNING_MODAL'}
        elif event.type == 'LEFTMOUSE':
            return {'FINISHED'}
        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            return {'CANCELLED'}


        return {'RUNNING_MODAL'} # It has to be {'RUNNING_MODAL'}


    def invoke(self, context, event):
        if context.space_data.type == 'VIEW_3D':
            self.rv3d = bpy.context.region_data
            self.rotMat = self.rv3d.view_matrix
            context.window_manager.modal_handler_add(self) # Here the problem
            return {'RUNNING_MODAL'}
        
        else:
            self.report({'WARNING'}, "Active space must be a View3d")
            return {'CANCELLED'}


def register():
    bpy.utils.register_class(ModalDrawOperator)


if __name__ == "__main__":
    register()


However, this comparison is done continuously, all the time. This can be bad for memory. Although involving little data transfer, I did not like this solution. Must have a better one.

1 Like

Hi again, cool script you make there.

I have started investigating a way to handle the mouse states (normally with previous/current states) but I soon realized that I could simply tie certain events like middle mouse pass through.

This code below allows you to rotate the viewport, now the boring part would be to add zooming panning events also.


import bpy
class ModalDrawOperator(bpy.types.Operator):
	bl_idname = "view3d.with_modal_handler_add"
	bl_label = "1 Simple View3D Operator With self Modal Handler"


	def modal(self, context, event):
		if event.type == "LEFTMOUSE":
			print("LEFTMOUSE")


		if event.type == "MIDDLEMOUSE":
			print("Middle Mouse")
			print("ZZZ", context.selected_objects[0].location)
			return {"PASS_THROUGH"}
		
		if event.type in {"RIGHTMOUSE", "ESC"}:
			print("Cancelled")
			return {"CANCELLED"}


		# It has to be {"RUNNING_MODAL"}
		return {"RUNNING_MODAL"}


	def invoke(self, context, event):
		if context.space_data.type == "VIEW_3D":
			print("Running Modal: ESC or RightMouse to cancel.")
			context.window_manager.modal_handler_add(self)
			return {"RUNNING_MODAL"}


def register():
	bpy.utils.register_class(ModalDrawOperator)


def unregister():
	bpy.utils.unregister_class(ModalDrawOperator)


try: unregister()
except: pass


register()

Cool. Recently I saw this same solution in template “operator_modal_view3d_raycast.py” in Blender Python Templates. But I would rather not use it since it impedes the detection of the event.value ‘RELEASE’.

Oh I see, I went back to my initial idea and after a bit of code juggling this was the best solution I come up with. I don’t know why the PASS_THROUGH return, destroys the mouse events at the next loop, but anyway holding a cache of the mouse to a field variable might be a good way to trick Python into abstracting the input system a bit. :slight_smile:


import bpy


class ModalDrawOperator(bpy.types.Operator):
	bl_idname = "view3d.with_modal_handler_add"
	bl_label = "1 Simple View3D Operator With self Modal Handler"


	mouse_middle = False


	def modal(self, context, event):


		# Assignment
		if event.type == "MIDDLEMOUSE":
			self.mouse_middle = True
		else:
			self.mouse_middle = False
		# Assignment




		# Debug
		if event.value == "PRESS":
			print("Press")


		if event.value == "RELEASE":
			print("Release")
		# Debug




		# Return
		if event.type in {"RIGHTMOUSE", "ESC"}:
			print("Cancelled")
			return {"CANCELLED"}


		if self.mouse_middle:
			return {"PASS_THROUGH"}	
		else:
			return {"RUNNING_MODAL"}
		# Return


	def invoke(self, context, event):
		if context.space_data.type == "VIEW_3D":
			print("Running Modal: ESC or RightMouse to cancel.")
			context.window_manager.modal_handler_add(self)
			return {"RUNNING_MODAL"}


def register():
	bpy.utils.register_class(ModalDrawOperator)


def unregister():
	bpy.utils.unregister_class(ModalDrawOperator)


try: unregister()
except: pass


register()

The idea is good @const. But this way the script detects the event.value ‘RELEASE’ for all event.types (‘A’, ‘B’, ‘C’, etc.). Not only the ‘MIDDLEMOUSE’. :frowning:

Hi Mano Wii,

While developing the Contours and PolyStrips addon, we ran into similar difficulties! At the end of navigation, our BGL drawing would lag one frame behind because ‘RELEASE’ was not detected. Our solution was to also compare the view matrix too. I know it’s not the solution you want, but maybe it helps if “everyone is doing it”

Great progress on this addon by the way.

-Patrick

You may also run into problems where people have non default navigation settings in their Key Maps. We have not yet conquered this, but we have made improvements where we extract the events from user preferences, and simply ignore the ones which collie with our default action keys in our addon.

Step One. Create our own little convention for events. This is not necessary but it helped us get started. So we turn all events into ‘CTRL+SHIFT+A’ or a unique string. But this is not necessary at all. The point here is. We collect a list of navigation events, then sort through the users keymap and find them.

http://www.pasteall.org/57041/python

Thank you @patmo141. I’m starting to accept the solution of the View_Matrix. I will follow your recommendations.

This is difficult to understand (to me) :frowning:
How can I get the variable “keycon” in posted script?

Sorry to leave you hanging for months! Keycon is the users key configuration. Accessible from user preferences.

http://www.blender.org/api/blender_python_api_2_74_release/bpy.types.UserPreferencesInput.html?highlight=keycon#bpy.types.UserPreferencesInput.active_keyconfig

No problem, thanks for the tip @patmo141. I also developed a strategy for the key user settings:

def navigation(self, context, event):
    #TO DO:
    #'View Orbit', 'View Pan', 'NDOF Orbit View', 'NDOF Pan View'
    rv3d = context.region_data
    if not hasattr(self, 'navigation_cache'): # or self.navigation_cache == False:
        self.navigation_cache = True
        self.keys_rotate = set()
        self.keys_move = set()
        self.keys_zoom = set()
        for key in context.window_manager.keyconfigs.user.keymaps['3D View'].keymap_items:
            if key.idname == 'view3d.rotate':
                #self.keys_rotate[key.id]={'Alt': key.alt, 'Ctrl': key.ctrl, 'Shift':key.shift, 'Type':key.type, 'Value':key.value}
                self.keys_rotate.add((key.alt, key.ctrl, key.shift, key.type, key.value))
            if key.idname == 'view3d.move':
                self.keys_move.add((key.alt, key.ctrl, key.shift, key.type, key.value))
            if key.idname == 'view3d.zoom':
                self.keys_zoom.add((key.alt, key.ctrl, key.shift, key.type, key.value, key.properties.delta))
                if key.type == 'WHEELINMOUSE':
                    self.keys_zoom.add((key.alt, key.ctrl, key.shift, 'WHEELDOWNMOUSE', key.value, key.properties.delta))
                if key.type == 'WHEELOUTMOUSE':
                    self.keys_zoom.add((key.alt, key.ctrl, key.shift, 'WHEELUPMOUSE', key.value, key.properties.delta))


    evkey = (event.alt, event.ctrl, event.shift, event.type, event.value)
    if evkey in self.keys_rotate:
        bpy.ops.view3d.rotate('INVOKE_DEFAULT')
    elif evkey in self.keys_move:
        bpy.ops.view3d.move('INVOKE_DEFAULT')
    else:
        for key in self.keys_zoom:
            if evkey == key[0:5]:
                delta = -key[5]
                if delta == 0:
                    bpy.ops.view3d.zoom('INVOKE_DEFAULT')
                else:
                    bpy.ops.view3d.zoom('INVOKE_DEFAULT', delta = delta )

                break

This solution does not work with some events, such as ‘DOUBLE_CLICK’, ‘ANY’, etc.
I do not know if it’s a good solution, but it works in most of the operators.

That is very interesting! If I interpret your code correctly, instead of {‘PASS_THROUGH’} when a navigation event is detected by your modal code, you invoke the appropriate navigation operator. What is the advantage of this? Does that solve the ‘RELEASE’ problem?

I see also you have accounted for the WHEELINMOUSE/WHEELUPMOUSE inconsistency.

Cheers,
Patrick