Newer
Older
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
## draw utils
import gpu
import bgl
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)
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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)
# 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"}
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'}
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)
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':
if not self.angle:
# 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:
if self.cam.parent:
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')
else:
context.space_data.region_3d.view_rotation = z_up_quat
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_rotation = self._rotation
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
prefs = get_addon_prefs()
self.hud = prefs.canvas_use_hud
self.angle = 0.0
self.in_cam = context.region_data.view_perspective == 'CAMERA'
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'}
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 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 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()
#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)))
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'}
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
## -- 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.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.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'}
classes = (
RC_OT_RotateCanvas,
RC_OT_Set_rotation,
RC_OT_Reset_rotation,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)