Cursor Rotation in a modal in edit mode

I did an addon to move the cursor with an empty, in object mode.
in edit mode, this is using the cursor tool (builtin_cursor), but I want to add in the modal a cursor rotation when pressing R.
it would be interesting to draw some gpu lines too (and deactivate the object gismow while)
the best scenario will be to reproduce the normal R behavior, but not sure I’m strong enough to do it.
It could be a modal alone when double clicking R

hrm… i’ve read through your post twice and I haven’t found the question yet. what did you need help with?

to do a modal to rotate cursor as R with an object.
if not I will just open a panel with the cursor settings loc and rot to change them manually
but I was asking because I saw no subject around this in python and could be usefull to have something more advanced to modify the cursor rotation

there’s nothing super complicated about it, you just create a modal operator as usual and then set context.scene.cursor.matrix with the updated rotation. the only ‘gotcha’ is that updating the matrix by itself doesn’t trigger the cursor to refresh visually, so you have to ‘touch’ one of the properties that have a callback, ie) context.scene.cursor.location = context.scene.cursor.matrix.translation.

or are you asking how to calculate a 3d rotation using 2d mouse coordinates?

I don’t see how to catch values in a modal. for example I type R, than X, than the value (how?).
and about using the mouse this is another possible option. like I said quite the same that R for an object

Well, you wait for event.type to match the key in question. If you’re already in a state where you’re waiting for decimal input, you can check if the input type is part of a set (the number keys) and update your rotation variable accordingly. That said, this is pretty basic modal operator stuff, you might try checking out the examples that ship with Blender or finding a tutorial to get you through the basics before diving into something more complicated. If you’re stuck at reading input you’re going to have a pretty painful time with the matrix math needed to update the cursor.

well I’m stuck on this precise point, I explained… for matrices, I already did this in my addon. I was asking because it doesn’t seem obvious to me, even having searching. but thanks for your answer. I wonder if I should keep on doing my addon lol

blender doesn’t have a built in way to enter different input states inside a modal operator, it’s basically just an infinite loop that gives you an event and expects you to exit out of it whenever you’re done with it.

With that in mind- you basically just have to listen for the events you care about and create your own state machine (or facsimile of one anyway).

for example:

  • you’d have a class variable that is tracking what sort of state you’re in.
  • the user presses “R”, and while you’re in the default state and get an “R” event, change the state to “rotate”.
  • the user presses “X”, and since you’re in the “rotate” state, set another class variable to constrain to that axis.
  • the user types “23.5” and then presses enter. This is actually five separate events, but since you’re in the rotate state and have a constraint, you can listen for event.type in {'ONE', 'TWO', 'THREE', etc}
    • each time one of these number events comes in, you can update your class variable for the rotation, you just append the new number to a string, then convert it back into a float: `self.rotation_value = float(f"{self.rotation_value}5")

Obviously this is a simplification, but that’s the general approach to take- just take it one step at a time and you’ll figure it out

1 Like

ok this answer was very inspiring.
I did a version independant of my addon to share it.
so far I have this. next step I need to have a way to see what I’m doing.

import bpy
from math import radians


class ROTATE_cursor(bpy.types.Operator):
    """rotate cursor R + axe + value / backspace to delete"""
    bl_idname = "rotate.cursor"
    bl_label = " Cursor rotation Modal"

    def modal(self, context, event):

        if event.type == 'R': #rotation ON
            self.R = True
            self.report({'WARNING'}, "Rotation Started")
        if event.type in {'X','Y','Z'}:
            self.axe = event.type
        if event.type in {'NUMPAD_0', 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4', 'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9'}:#, 'NUMPAD_PERIOD'}:
            if self.R:
                if event.value == 'RELEASE':
                    if self.axe:
                        self.rot_val += (event.type)[-1]
                        curs=context.scene.cursor
                        setattr(curs.rotation_euler, self.axe.lower(), radians(float(self.rot_val)))
                    else:
                        pass # no axe
            else: # to allow numpad when not using 'R'
               return {'PASS_THROUGH'} 

        if event.type == 'NUMPAD_PERIOD':
            if self.R:
                if event.value == 'RELEASE':
                    if self.axe:
                        self.rot_val += "."
                        curs=context.scene.cursor
                        setattr(curs.rotation_euler, self.axe.lower(), radians(float(self.rot_val)))
                    else:
                        pass

        if event.type == 'BACK_SPACE':#, 'NUMPAD_PERIOD'}:
            if self.R and self.axe:
                if event.value == 'RELEASE':
                    if self.rot_val:
                        self.rot_val = self.rot_val[:-1]
                        if self.rot_val:
                            curs=context.scene.cursor
                            setattr(curs.rotation_euler, self.axe.lower(), radians(float(self.rot_val)))
                        else:
                            curs=context.scene.cursor
                            setattr(curs.rotation_euler, self.axe.lower(), 0)
                                                    
    # Finish
        if event.type == 'RET' and event.value == 'PRESS':
            if self.R:
                self.rot_val = ""
                self.R = False
                self.axe = None
                self.report({'WARNING'}, "Rotation Finished")
            else:
                self.report({'WARNING'}, "Modal Finished")
                return {'FINISHED'}

        elif event.type == 'ESC' and event.value == 'PRESS':
            if self.R:
                self.rot_val = ""
                self.R = False
                self.axe = None
                curs = context.scene.cursor
                curs.rotation_euler = self.curs_rot
                self.report({'WARNING'}, "Cancelled")
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        curs = context.scene.cursor
        self.curs_rot = curs.rotation_euler
        self.R = False
        self.axe = None
        self.rot_val = ""
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def execute(self, context):
        pass

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


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


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.rotate.cursor('INVOKE_DEFAULT')

I added a way to show values. So this is possible to enter axe, then value, and alternate axe entries, to set 3 directions, without re running the modal.
Now I need a way to draw lines around the cursor to show better the orientation. and keep a constant size

import bpy
from math import radians


class ROTATE_cursor(bpy.types.Operator):
    """rotate cursor R + axe + value / backspace to delete"""
    bl_idname = "rotate.cursor"
    bl_label = " Cursor rotation Modal"

    def modal(self, context, event):

        if event.type == 'R': #rotation ON
            self.R = True
            self.report({'WARNING'}, "Rotation Started")
        if event.type in {'X','Y','Z'}:
            self.axe = event.type
            self.rot_val = ''
        if event.type in {'NUMPAD_0', 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4', 'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9'}:#, 'NUMPAD_PERIOD'}:
            if self.R:
                if event.value == 'RELEASE':
                    if self.axe:
                        self.rot_val += (event.type)[-1]
                        curs=context.scene.cursor
                        setattr(curs.rotation_euler, self.axe.lower(), radians(float(self.rot_val)))
                    else:
                        pass # no axe
            else: # to allow numpad when not using 'R'
               return {'PASS_THROUGH'} 

        if event.type == 'NUMPAD_PERIOD':
            if self.R:
                if event.value == 'RELEASE':
                    if self.axe:
                        self.rot_val += "."
                        curs=context.scene.cursor
                        setattr(curs.rotation_euler, self.axe.lower(), radians(float(self.rot_val)))
                    else:
                        pass
        if self.R:
            string = f"Cursor Rot: {self.rot_val if self.rot_val else '0.00'} along {self.axe}" if self.axe \
             else "Cursor Rot: axe(x,y,z) then value"
                        
            context.area.header_text_set(string)

        if event.type == 'BACK_SPACE':#, 'NUMPAD_PERIOD'}:
            if self.R and self.axe:
                if event.value == 'RELEASE':
                    if self.rot_val:
                        self.rot_val = self.rot_val[:-1]
                        if self.rot_val:
                            curs=context.scene.cursor
                            setattr(curs.rotation_euler, self.axe.lower(), radians(float(self.rot_val)))
                        else:
                            curs=context.scene.cursor
                            setattr(curs.rotation_euler, self.axe.lower(), 0)
                                                    
    # Finish
        if event.type == 'RET' and event.value == 'PRESS':
            if self.R:
                self.rot_val = ""
                self.rot_val_header =""
                self.R = False
                self.axe = None
                context.area.header_text_set(None)
                self.report({'WARNING'}, "Rotation Finished")
            else:
                self.report({'WARNING'}, "Modal Finished")
                return {'FINISHED'}

        elif event.type == 'ESC' and event.value == 'PRESS':
            if self.R:
                self.rot_val = ""
                self.R = False
                self.axe = None
                curs = context.scene.cursor
                curs.rotation_euler = self.curs_rot
                context.area.header_text_set(None)
                self.report({'WARNING'}, "Cancelled")
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        curs = context.scene.cursor
        self.curs_rot = curs.rotation_euler
        self.R = False
        self.axe = None
        self.rot_val = ""
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def execute(self, context):
        pass

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


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


if __name__ == "__main__":
    register()

ok from now I have the gizmo. not sure this is perfect but it works!

my god I get to adapt this to the modal it will considerably reduce the work
https://www.reddit.com/r/blender/comments/avwdv3/a_quick_trick_for_rotating_the_3d_cursor/

oh my god finally it was two lines

        if event.type == 'T' and event.value == 'PRESS': #then R
            bpy.ops.transform.translate('INVOKE_DEFAULT', cursor_transform=True, release_confirm=False)

so to move cursor press T (transform move) then for rotation release and press R
with another script I did to add a gizmo it’s giving this

import bpy
import bgl
import gpu
from mathutils import Vector
from gpu_extras.batch import batch_for_shader

vertex_shader = '''
uniform mat4 ModelViewProjectionMatrix;
in vec3 pos;
in vec4 color;
out vec4 finalColor;
void main()
{
    gl_Position = ModelViewProjectionMatrix * vec4(pos, 1.0);
    gl_Position.z -= 0.001;
    finalColor = color;
}
'''

fragment_shader = """
in vec4 finalColor;
in vec4 fragCoord;
out vec4 fragColor;
out float fragDepth;
void main()
{
    vec2 coord = gl_PointCoord - vec2(0.5, 0.5);
    fragColor = finalColor;
    fragDepth = 0;
}   
"""

def draw_callback_3d(self, context):
    curs = context.scene.cursor
    mat = curs.matrix
    offset = self.offset
    # constant size
    view_info = context.space_data.region_3d.view_matrix.inverted()
    view_loc = view_info.translation
    depth = (view_loc - curs.location).length
    scale = depth/12

    coords = [mat@Vector((0, 0, 0)), mat@Vector((offset*scale, 0, 0)),
              mat@Vector((0, 0, 0)), mat@Vector((0, offset*scale, 0)),
              mat@Vector((0, 0, 0)), mat@Vector((0, 0, offset*scale))]

    colors = ((0.8, 0, 0, 0.9), (0.8, 0, 0, 0.9),
              (0, 0.8, 0, 0.9), (0, 0.8, 0, 0.9),
              (0, 0, 0.8, 0.9), (0, 0, 0.8, 0.9), )
    shader = gpu.types.GPUShader(
        vertex_shader, fragment_shader)  # simpler way?
    batch = batch_for_shader(shader, 'LINES', {"pos": coords, "color": colors})
    bgl.glEnable(bgl.GL_BLEND)
    bgl.glLineWidth(3)
    bgl.glEnable(bgl.GL_LINE_SMOOTH)
    bgl.glEnable(bgl.GL_MULTISAMPLE)
    shader.bind()
    batch.draw(shader)
    # restore opengl defaults
    bgl.glLineWidth(1)
    bgl.glDisable(bgl.GL_BLEND)


class ROTATE_cursor(bpy.types.Operator):
    """rotate cursor R + axe + value / backspace to delete"""
    bl_idname = "rotate.cursor"
    bl_label = " Cursor rotation Modal"

    def modal(self, context, event):
        
        if event.type == 'T' and event.value == 'PRESS': #then R
            bpy.ops.transform.translate('INVOKE_DEFAULT', cursor_transform=True, release_confirm=False)

        if event.type == 'RIGHTMOUSE':
            self.right_press = event.value == 'PRESS'
            return {'RUNNING_MODAL'}

        if self.right_press and event.type == 'WHEELUPMOUSE':  # rightmouse+wheel to change gizmo size
            self.offset += 0.15
            context.area.tag_redraw()
            return {'RUNNING_MODAL'}

        if (
            self.right_press
            and event.type == 'WHEELDOWNMOUSE'
            and self.offset > 0.16
        ):
            self.offset -= 0.15
            context.area.tag_redraw()
            return {'RUNNING_MODAL'}

        if event.type == 'RET' and event.value == 'PRESS':
            bpy.types.SpaceView3D.draw_handler_remove(
                self.draw_handle_3d, 'WINDOW')
            context.area.tag_redraw()
            self.report({'WARNING'}, "Modal Finished")
            return {'FINISHED'}

        if event.type in {'LEFTMOUSE', 'MOUSEMOVE', 'EVT_TWEAK_L', 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE',
                          'NUMPAD_0', 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4',
                          'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9', 'NUMPAD_PERIOD'}:
            print(event.type)
            return {'PASS_THROUGH'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        self.curs_rot = context.scene.cursor.rotation_euler.copy()
        # gpu variables
        self.right_press = None
        self.offset = 1

        args = (self, context)
        self.draw_handle_3d = bpy.types.SpaceView3D.draw_handler_add(
            draw_callback_3d, args, 'WINDOW', 'POST_VIEW')
        context.area.tag_redraw()
        context.window_manager.modal_handler_add(self)

        return {'RUNNING_MODAL'}


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


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

if __name__ == "__main__":
    register()