284 lines
11 KiB
Python
284 lines
11 KiB
Python
|
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()
|