from .utils import get_addon_prefs ## known issue: auto-perspective mess up when triggered out after rotation import bpy import math import mathutils from bpy_extras.view3d_utils import location_3d_to_region_2d ## draw utils import gpu import bgl import blf from gpu_extras.batch import batch_for_shader from gpu_extras.presets import draw_circle_2d """ Notes: Samuel.B: OpenGL drawing can be disabled by passing self.hud to False in invoke (mainly used for debugging) Base script by Jum, simplified and modified to work in both view and camera with rotate axis method suggested by Christophe Seux Jum: Script base. Thanks to bigLarry and Jum https://blender.stackexchange.com/questions/136183/rotating-camera-view-in-grease-pencil-draw-mode-in-blender-2-8 """ def draw_callback_px(self, context): # 50% alpha, 2 pixel width line shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') bgl.glEnable(bgl.GL_BLEND) bgl.glLineWidth(2) # init batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": [self.center, self.initial_pos]})#self.vector_initial shader.bind() shader.uniform_float("color", (0.5, 0.5, 0.8, 0.6)) batch.draw(shader) batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": [self.center, self.pos_current]}) shader.bind() shader.uniform_float("color", (0.3, 0.7, 0.2, 0.5)) batch.draw(shader) ## vector init vector current (substracted by center) # batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": [self.vector_initial, self.vector_current]}) # shader.bind() # shader.uniform_float("color", (0.5, 0.5, 0.5, 0.5)) # batch.draw(shader) # restore opengl defaults bgl.glLineWidth(1) bgl.glDisable(bgl.GL_BLEND) ## text font_id = 0 ## draw text debug infos blf.position(font_id, 15, 30, 0) blf.size(font_id, 20, 72) blf.draw(font_id, f'angle: {math.degrees(self.angle):.1f}') class RC_OT_RotateCanvas(bpy.types.Operator): bl_idname = 'view3d.rotate_canvas' bl_label = 'Rotate Canvas' bl_options = {"REGISTER", "UNDO"} # @classmethod # def poll(cls, context): # return context.region_data.view_perspective == 'CAMERA' """ def get_center_view(self, area, cam): ''' https://blender.stackexchange.com/questions/6377/coordinates-of-corners-of-camera-view-border Thanks to ideasman42 ''' region_3d = area.spaces[0].region_3d for region in area.regions: if region.type == 'WINDOW': frame = cam.data.view_frame() # if cam.parent: # mat = cam.matrix_parent_inverse @ cam.matrix_world # # mat = cam.parent.matrix_world @ cam.matrix_world# not inverse from parent # else: # mat = cam.matrix_world mat = cam.matrix_world frame = [mat @ v for v in frame] ## bpy.context.scene.cursor.location = frame[1]#DEBUG frame_px = [location_3d_to_region_2d(region, region_3d, v) for v in frame] center_x = frame_px[2].x + (frame_px[0].x - frame_px[2].x)/2 center_y = frame_px[1].y + (frame_px[0].y - frame_px[1].y)/2 return mathutils.Vector((center_x, center_y)) return None """ def get_center_view(self, context, cam): ''' https://blender.stackexchange.com/questions/6377/coordinates-of-corners-of-camera-view-border Thanks to ideasman42 ''' frame = cam.data.view_frame() mat = cam.matrix_world frame = [mat @ v for v in frame] frame_px = [location_3d_to_region_2d(context.region, context.space_data.region_3d, v) for v in frame] center_x = frame_px[2].x + (frame_px[0].x - frame_px[2].x)/2 center_y = frame_px[1].y + (frame_px[0].y - frame_px[1].y)/2 return mathutils.Vector((center_x, center_y)) def execute(self, context): if self.hud: bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') context.area.tag_redraw() if self.in_cam: self.cam.rotation_mode = self.org_rotation_mode return {'FINISHED'} def modal(self, context, event): if event.type in {'MOUSEMOVE','INBETWEEN_MOUSEMOVE'}: # Get current mouse coordination (region) self.pos_current = mathutils.Vector((event.mouse_region_x, event.mouse_region_y)) # Get current vector self.vector_current = (self.pos_current - self.center).normalized() # Calculates the angle between initial and current vectors self.angle = self.vector_initial.angle_signed(self.vector_current)#radian # print (math.degrees(self.angle), self.vector_initial, self.vector_current) if self.in_cam: self.cam.matrix_world = self.cam_matrix # self.cam.rotation_euler = self.cam_init_euler self.cam.rotation_euler.rotate_axis("Z", self.angle) else:#free view context.space_data.region_3d.view_matrix = self.view_matrix rot = context.space_data.region_3d.view_rotation rot = rot.to_euler() rot.rotate_axis("Z", self.angle) context.space_data.region_3d.view_rotation = rot.to_quaternion() if event.type in {'RIGHTMOUSE', 'LEFTMOUSE', 'MIDDLEMOUSE'} and event.value == 'RELEASE': self.execute(context) return {'FINISHED'} if event.type == 'ESC':#Cancel self.execute(context) if self.in_cam: self.cam.matrix_world = self.cam_matrix else: context.space_data.region_3d.view_matrix = self.view_matrix return {'CANCELLED'} return {'RUNNING_MODAL'} def invoke(self, context, event): self.hud = False self.angle = 0.0# for draw degub, else not needed self.in_cam = context.region_data.view_perspective == 'CAMERA' if self.in_cam: # Get camera from scene self.cam = bpy.context.scene.camera ## avoid manipulating real cam or locked cams if not 'manip_cams' in [c.name for c in self.cam.users_collection]: self.report({'WARNING'}, 'Not in manipulation cam (draw/obj cam)') return {'CANCELLED'} if self.cam.lock_rotation[:] != (False, False, False): self.report({'WARNING'}, 'Camera rotation is locked') return {'CANCELLED'} self.center = self.get_center_view(context, self.cam) # store original rotation mode self.org_rotation_mode = self.cam.rotation_mode # set to euler to works with quaternions, restored at finish self.cam.rotation_mode = 'XYZ' # store camera matrix world self.cam_matrix = self.cam.matrix_world.copy() # self.cam_init_euler = self.cam.rotation_euler.copy() else: self.center = mathutils.Vector((context.area.width/2, context.area.height/2)) # store current view matrix self.view_matrix = context.space_data.region_3d.view_matrix.copy() # Get current mouse coordination self.pos_current = mathutils.Vector((event.mouse_region_x, event.mouse_region_y)) self.initial_pos = self.pos_current# for draw debug, else no need # Calculate inital vector self.vector_initial = self.pos_current - self.center self.vector_initial.normalize() # Initializes the current vector with the same initial vector. self.vector_current = self.vector_initial.copy() args = (self, context) if self.hud: self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL') context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} class PREFS_OT_rebind(bpy.types.Operator): """Rebind shortcuts canvas rotate shortcuts""" bl_idname = "prefs.rebind_shortcut" bl_label = "Rebind canvas rotate shortcut" bl_options = {'REGISTER', 'INTERNAL'} def execute(self, context): unregister_keymaps() register_keymaps() return{'FINISHED'} addon_keymaps = [] def register_keymaps(): pref = get_addon_prefs() if not pref.canvas_use_shortcut: return addon = bpy.context.window_manager.keyconfigs.addon """ ## NATIVE FREENAV BIND (left to right) km = bpy.context.window_manager.keyconfigs.addon.keymaps.get("3D View") if not km: km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D") # print('BINDING CANVAS ROTATE KEYMAPS')#Dbg if 'view3d.view_roll' not in km.keymap_items: # print('creating view3d.view_roll')#Dbg # kmi = km.keymap_items.new("view3d.view_roll", type = 'MIDDLEMOUSE', value = "PRESS", ctrl=True, shift=False, alt=True)#PRESS#CLICK_DRAG kmi = km.keymap_items.new("view3d.view_roll", type=pref.mouse_click, value = "PRESS", alt=pref.use_alt, ctrl=pref.use_ctrl, shift=pref.use_shift, any=False)#PRESS#CLICK_DRAG kmi.properties.type = 'ANGLE' addon_keymaps.append(km) """ km = bpy.context.window_manager.keyconfigs.addon.keymaps.get("3D View") if not km: km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D") if 'view3d.rotate_canvas' not in km.keymap_items: # print('creating view3d.rotate_canvas')#Dbg ## keymap to operator cam space (in grease pencil mode only ?) km = addon.keymaps.new(name='3D View', space_type='VIEW_3D')#EMPTY #Grease Pencil # kmi = km.keymap_items.new('view3d.rotate_canvas', 'MIDDLEMOUSE', 'PRESS', ctrl=True, shift=False, alt=True) kmi = km.keymap_items.new('view3d.rotate_canvas', type=pref.mouse_click, value="PRESS", alt=pref.use_alt, ctrl=pref.use_ctrl, shift=pref.use_shift, any=False) addon_keymaps.append((km, kmi)) # print(addon_keymaps) def unregister_keymaps(): # print('UNBIND CANVAS ROTATE KEYMAPS')#Dbg for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) addon_keymaps.clear() # del addon_keymaps[:] canvas_classes = ( PREFS_OT_rebind, RC_OT_RotateCanvas, # RC_OT_RotateCanvasFreeNav ) def register(): if not bpy.app.background: for cls in canvas_classes: bpy.utils.register_class(cls) register_keymaps() # wm = bpy.context.window_manager # km = wm.keyconfigs.addon.keymaps.new(name='Grease Pencil', space_type='EMPTY') # kmi = km.keymap_items.new('view3d.rotate_canvas', 'MIDDLEMOUSE', 'PRESS', ctrl=True, shift=False, alt=True) # addon_keymaps.append(km) def unregister(): if not bpy.app.background: unregister_keymaps() for cls in reversed(canvas_classes): bpy.utils.unregister_class(cls) # if __name__ == "__main__": # register()