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
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()