335 lines
12 KiB
Python
335 lines
12 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
from .prefs import get_addon_prefs
|
|
|
|
import bpy
|
|
import math
|
|
import mathutils
|
|
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
|
from bpy.props import BoolProperty, EnumProperty
|
|
from time import time
|
|
## draw utils
|
|
import gpu
|
|
import blf
|
|
from gpu_extras.batch import batch_for_shader
|
|
from gpu_extras.presets import draw_circle_2d
|
|
|
|
def step_value(value, step):
|
|
'''return the step closer to the passed value'''
|
|
abs_angle = abs(value)
|
|
diff = abs_angle % step
|
|
lower_step = abs_angle - diff
|
|
higher_step = lower_step + step
|
|
if abs_angle - lower_step < higher_step - abs_angle:
|
|
return math.copysign(lower_step, value)
|
|
else:
|
|
return math.copysign(higher_step, value)
|
|
|
|
def draw_callback_px(self, context):
|
|
# 50% alpha, 2 pixel width line
|
|
if context.area != self.current_area:
|
|
return
|
|
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
|
|
gpu.state.blend_set('ALPHA')
|
|
gpu.state.line_width_set(2.0)
|
|
|
|
# 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)
|
|
|
|
## debug lines
|
|
# batch = batch_for_shader(shader, 'LINES', {"pos": [
|
|
# (0,0), (context.area.width, context.area.height),
|
|
# (context.area.width, 0), (0, context.area.height)
|
|
# ]})
|
|
# shader.bind()
|
|
# shader.uniform_float("color", (0.8, 0.1, 0.1, 0.5))
|
|
# batch.draw(shader)
|
|
|
|
# restore opengl defaults
|
|
gpu.state.line_width_set(1.0)
|
|
gpu.state.blend_set('NONE')
|
|
|
|
## 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"}
|
|
|
|
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 set_cam_view_offset_from_angle(self, context, angle):
|
|
'''apply inverse of the rotation on view offset in cam rotate from view center'''
|
|
neg = -angle
|
|
rot_mat2d = mathutils.Matrix([[math.cos(neg), -math.sin(neg)], [math.sin(neg), math.cos(neg)]])
|
|
|
|
# scale_mat = mathutils.Matrix([[1.0, 0.0], [0.0, self.ratio]])
|
|
new_cam_offset = self.view_cam_offset.copy()
|
|
|
|
## area deformation correction
|
|
new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio))
|
|
|
|
## rotate by matrix
|
|
new_cam_offset.rotate(rot_mat2d)
|
|
|
|
## area deformation restore
|
|
new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio_inv))
|
|
|
|
context.space_data.region_3d.view_camera_offset = new_cam_offset
|
|
|
|
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)
|
|
|
|
|
|
## handle snap key
|
|
snap = False
|
|
if self.snap_ctrl and event.ctrl:
|
|
snap = True
|
|
if self.snap_shift and event.shift:
|
|
snap = True
|
|
if self.snap_alt and event.alt:
|
|
snap = True
|
|
## Snapping to specific degrees angle
|
|
if snap:
|
|
self.angle = step_value(self.angle, self.snap_step)
|
|
|
|
if self.in_cam:
|
|
self.cam.matrix_world = self.cam_matrix
|
|
self.cam.rotation_euler.rotate_axis("Z", self.angle)
|
|
|
|
if self.use_view_center:
|
|
## apply inverse rotation on view offset
|
|
self.set_cam_view_offset_from_angle(context, self.angle)
|
|
|
|
else: # free view
|
|
context.space_data.region_3d.view_rotation = self._rotation
|
|
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':
|
|
# Trigger reset : Less than 150ms and less than 2 degrees move
|
|
if time() - self.timer < 0.15 and abs(math.degrees(self.angle)) < 2:
|
|
# self.report({'INFO'}, 'Reset')
|
|
aim = context.space_data.region_3d.view_rotation @ mathutils.Vector((0.0, 0.0, 1.0)) # view vector
|
|
z_up_quat = aim.to_track_quat('Z','Y') # track Z, up Y
|
|
if self.in_cam:
|
|
|
|
q = self.cam.matrix_world.to_quaternion() # store current rotation
|
|
|
|
if self.cam.parent:
|
|
q = self.cam.parent.matrix_world.inverted().to_quaternion() @ q
|
|
cam_quat = self.cam.parent.matrix_world.inverted().to_quaternion() @ z_up_quat
|
|
else:
|
|
cam_quat = z_up_quat
|
|
self.cam.rotation_euler = cam_quat.to_euler('XYZ')
|
|
|
|
# get diff angle (might be better way to get view axis rot diff)
|
|
diff_angle = q.rotation_difference(cam_quat).to_euler('ZXY').z
|
|
# print('diff_angle: ', math.degrees(diff_angle))
|
|
self.set_cam_view_offset_from_angle(context, diff_angle)
|
|
|
|
else:
|
|
context.space_data.region_3d.view_rotation = z_up_quat
|
|
self.execute(context)
|
|
return {'FINISHED'}
|
|
|
|
if event.type == 'ESC':#Cancel
|
|
self.execute(context)
|
|
if self.in_cam:
|
|
self.cam.matrix_world = self.cam_matrix
|
|
context.space_data.region_3d.view_camera_offset = self.view_cam_offset
|
|
else:
|
|
context.space_data.region_3d.view_rotation = self._rotation
|
|
return {'CANCELLED'}
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def invoke(self, context, event):
|
|
self.current_area = context.area
|
|
prefs = get_addon_prefs()
|
|
self.hud = prefs.canvas_use_hud
|
|
self.use_view_center = prefs.canvas_use_view_center
|
|
self.angle = 0.0
|
|
self.in_cam = context.region_data.view_perspective == 'CAMERA'
|
|
|
|
## store ratio for view rotate correction
|
|
|
|
# CORRECT UI OVERLAP FROM HEADER TOOLBAR
|
|
regs = context.area.regions
|
|
if context.preferences.system.use_region_overlap:
|
|
w = context.area.width
|
|
# minus tool header
|
|
h = context.area.height - regs[0].height
|
|
else:
|
|
# minus tool leftbar + sidebar right
|
|
w = context.area.width - regs[2].width - regs[3].width
|
|
# minus tool header + header
|
|
h = context.area.height - regs[0].height - regs[1].height
|
|
|
|
self.ratio = h / w
|
|
self.ratio_inv = w / h
|
|
|
|
if self.in_cam:
|
|
# Get camera from scene
|
|
self.cam = bpy.context.scene.camera
|
|
|
|
#return if one element is locked (else bypass location)
|
|
if self.cam.lock_rotation[:] != (False, False, False):
|
|
self.report({'WARNING'}, 'Camera rotation is locked')
|
|
return {'CANCELLED'}
|
|
|
|
if self.use_view_center:
|
|
self.center = mathutils.Vector((w/2, h/2))
|
|
else:
|
|
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()
|
|
|
|
## initialize current view_offset in camera
|
|
self.view_cam_offset = mathutils.Vector(context.space_data.region_3d.view_camera_offset)
|
|
|
|
else:
|
|
self.center = mathutils.Vector((w/2, h/2))
|
|
# self.center = mathutils.Vector((context.area.width/2, context.area.height/2))
|
|
|
|
# store current rotation
|
|
self._rotation = context.space_data.region_3d.view_rotation.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 initial 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()
|
|
|
|
|
|
#Snap keys
|
|
self.snap_ctrl = not prefs.use_ctrl
|
|
self.snap_shift = not prefs.use_shift
|
|
self.snap_alt = not prefs.use_alt
|
|
# round to closer degree and convert back to radians
|
|
self.snap_step = math.radians(round(math.degrees(prefs.rc_angle_step)))
|
|
|
|
self.timer = time()
|
|
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'}
|
|
|
|
|
|
## -- Set / Reset rotation buttons
|
|
|
|
class RC_OT_Set_rotation(bpy.types.Operator):
|
|
bl_idname = 'view3d.rotate_canvas_set'
|
|
bl_label = 'Save Rotation'
|
|
bl_description = 'Save active camera rotation (per camera property)'
|
|
bl_options = {"REGISTER"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.area.type == 'VIEW_3D' \
|
|
and context.space_data.region_3d.view_perspective == 'CAMERA'
|
|
|
|
def execute(self, context):
|
|
cam_ob = context.scene.camera
|
|
cam_ob['stored_rotation'] = cam_ob.rotation_euler
|
|
if not cam_ob.get('_RNA_UI'):
|
|
cam_ob['_RNA_UI'] = {}
|
|
cam_ob['_RNA_UI']["stored_rotation"] = {
|
|
"description":"Stored camera rotation (Gpencil tools > rotate canvas operator)",
|
|
"subtype":'EULER',
|
|
# "is_overridable_library":0,
|
|
}
|
|
|
|
return {'FINISHED'}
|
|
|
|
class RC_OT_Reset_rotation(bpy.types.Operator):
|
|
bl_idname = 'view3d.rotate_canvas_reset'
|
|
bl_label = 'Restore Rotation'
|
|
bl_description = 'Restore active camera rotation from previously saved state'
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.area.type == 'VIEW_3D' \
|
|
and context.space_data.region_3d.view_perspective == 'CAMERA' \
|
|
and context.scene.camera.get('stored_rotation')
|
|
|
|
def execute(self, context):
|
|
cam_ob = context.scene.camera
|
|
cam_ob.rotation_euler = cam_ob['stored_rotation']
|
|
return {'FINISHED'}
|
|
|
|
|
|
### --- REGISTER
|
|
|
|
classes = (
|
|
RC_OT_RotateCanvas,
|
|
RC_OT_Set_rotation,
|
|
RC_OT_Reset_rotation,
|
|
)
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
|
|
def unregister():
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|