1 # SPDX-FileCopyrightText: 2020-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 from .prefs
import get_addon_prefs
10 from bpy_extras
.view3d_utils
import location_3d_to_region_2d
15 from gpu_extras
.batch
import batch_for_shader
17 def step_value(value
, step
):
18 '''return the step closer to the passed value'''
19 abs_angle
= abs(value
)
20 diff
= abs_angle
% step
21 lower_step
= abs_angle
- diff
22 higher_step
= lower_step
+ step
23 if abs_angle
- lower_step
< higher_step
- abs_angle
:
24 return math
.copysign(lower_step
, value
)
26 return math
.copysign(higher_step
, value
)
28 def draw_callback_px(self
, context
):
29 # 50% alpha, 2 pixel width line
30 if context
.area
!= self
.current_area
:
32 shader
= gpu
.shader
.from_builtin('UNIFORM_COLOR')
33 gpu
.state
.blend_set('ALPHA')
34 gpu
.state
.line_width_set(2.0)
37 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": [self
.center
, self
.initial_pos
]})#self.vector_initial
39 shader
.uniform_float("color", (0.5, 0.5, 0.8, 0.6))
42 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": [self
.center
, self
.pos_current
]})
44 shader
.uniform_float("color", (0.3, 0.7, 0.2, 0.5))
48 # batch = batch_for_shader(shader, 'LINES', {"pos": [
49 # (0,0), (context.area.width, context.area.height),
50 # (context.area.width, 0), (0, context.area.height)
53 # shader.uniform_float("color", (0.8, 0.1, 0.1, 0.5))
56 # restore opengl defaults
57 gpu
.state
.line_width_set(1.0)
58 gpu
.state
.blend_set('NONE')
62 ## draw text debug infos
63 blf
.position(font_id
, 15, 30, 0)
64 blf
.size(font_id
, 20.0)
65 blf
.draw(font_id
, f
'angle: {math.degrees(self.angle):.1f}')
68 class RC_OT_RotateCanvas(bpy
.types
.Operator
):
69 bl_idname
= 'view3d.rotate_canvas'
70 bl_label
= 'Rotate Canvas'
71 bl_options
= {"REGISTER", "UNDO"}
73 def get_center_view(self
, context
, cam
):
75 https://blender.stackexchange.com/questions/6377/coordinates-of-corners-of-camera-view-border
79 frame
= cam
.data
.view_frame()
80 mat
= cam
.matrix_world
81 frame
= [mat
@ v
for v
in frame
]
82 frame_px
= [location_3d_to_region_2d(context
.region
, context
.space_data
.region_3d
, v
) for v
in frame
]
83 center_x
= frame_px
[2].x
+ (frame_px
[0].x
- frame_px
[2].x
)/2
84 center_y
= frame_px
[1].y
+ (frame_px
[0].y
- frame_px
[1].y
)/2
86 return mathutils
.Vector((center_x
, center_y
))
88 def set_cam_view_offset_from_angle(self
, context
, angle
):
89 '''apply inverse of the rotation on view offset in cam rotate from view center'''
91 rot_mat2d
= mathutils
.Matrix([[math
.cos(neg
), -math
.sin(neg
)], [math
.sin(neg
), math
.cos(neg
)]])
93 # scale_mat = mathutils.Matrix([[1.0, 0.0], [0.0, self.ratio]])
94 new_cam_offset
= self
.view_cam_offset
.copy()
96 ## area deformation correction
97 new_cam_offset
= mathutils
.Vector((new_cam_offset
[0], new_cam_offset
[1] * self
.ratio
))
100 new_cam_offset
.rotate(rot_mat2d
)
102 ## area deformation restore
103 new_cam_offset
= mathutils
.Vector((new_cam_offset
[0], new_cam_offset
[1] * self
.ratio_inv
))
105 context
.space_data
.region_3d
.view_camera_offset
= new_cam_offset
107 def execute(self
, context
):
109 bpy
.types
.SpaceView3D
.draw_handler_remove(self
._handle
, 'WINDOW')
110 context
.area
.tag_redraw()
112 self
.cam
.rotation_mode
= self
.org_rotation_mode
116 def modal(self
, context
, event
):
117 if event
.type in {'MOUSEMOVE'}:#,'INBETWEEN_MOUSEMOVE'
118 # Get current mouse coordination (region)
119 self
.pos_current
= mathutils
.Vector((event
.mouse_region_x
, event
.mouse_region_y
))
121 self
.vector_current
= (self
.pos_current
- self
.center
).normalized()
122 # Calculates the angle between initial and current vectors
123 self
.angle
= self
.vector_initial
.angle_signed(self
.vector_current
)#radian
124 # print (math.degrees(self.angle), self.vector_initial, self.vector_current)
129 if self
.snap_ctrl
and event
.ctrl
:
131 if self
.snap_shift
and event
.shift
:
133 if self
.snap_alt
and event
.alt
:
135 ## Snapping to specific degrees angle
137 self
.angle
= step_value(self
.angle
, self
.snap_step
)
140 self
.cam
.matrix_world
= self
.cam_matrix
141 self
.cam
.rotation_euler
.rotate_axis("Z", self
.angle
)
143 if self
.use_view_center
:
144 ## apply inverse rotation on view offset
145 self
.set_cam_view_offset_from_angle(context
, self
.angle
)
148 context
.space_data
.region_3d
.view_rotation
= self
._rotation
149 rot
= context
.space_data
.region_3d
.view_rotation
151 rot
.rotate_axis("Z", self
.angle
)
152 context
.space_data
.region_3d
.view_rotation
= rot
.to_quaternion()
154 if event
.type in {'RIGHTMOUSE', 'LEFTMOUSE', 'MIDDLEMOUSE'} and event
.value
== 'RELEASE':
155 # Trigger reset : Less than 150ms and less than 2 degrees move
156 if time() - self
.timer
< 0.15 and abs(math
.degrees(self
.angle
)) < 2:
157 # self.report({'INFO'}, 'Reset')
158 aim
= context
.space_data
.region_3d
.view_rotation
@ mathutils
.Vector((0.0, 0.0, 1.0)) # view vector
159 z_up_quat
= aim
.to_track_quat('Z','Y') # track Z, up Y
162 q
= self
.cam
.matrix_world
.to_quaternion() # store current rotation
165 q
= self
.cam
.parent
.matrix_world
.inverted().to_quaternion() @ q
166 cam_quat
= self
.cam
.parent
.matrix_world
.inverted().to_quaternion() @ z_up_quat
169 self
.cam
.rotation_euler
= cam_quat
.to_euler('XYZ')
171 # get diff angle (might be better way to get view axis rot diff)
172 diff_angle
= q
.rotation_difference(cam_quat
).to_euler('ZXY').z
173 # print('diff_angle: ', math.degrees(diff_angle))
174 self
.set_cam_view_offset_from_angle(context
, diff_angle
)
177 context
.space_data
.region_3d
.view_rotation
= z_up_quat
178 self
.execute(context
)
181 if event
.type == 'ESC':#Cancel
182 self
.execute(context
)
184 self
.cam
.matrix_world
= self
.cam_matrix
185 context
.space_data
.region_3d
.view_camera_offset
= self
.view_cam_offset
187 context
.space_data
.region_3d
.view_rotation
= self
._rotation
190 return {'RUNNING_MODAL'}
192 def invoke(self
, context
, event
):
193 self
.current_area
= context
.area
194 prefs
= get_addon_prefs()
195 self
.hud
= prefs
.canvas_use_hud
196 self
.use_view_center
= prefs
.canvas_use_view_center
198 ## Check if scene camera or local camera exists ?
199 # if (context.space_data.use_local_camera and context.space_data.camera) or context.scene.camera
200 self
.in_cam
= context
.region_data
.view_perspective
== 'CAMERA'
202 ## store ratio for view rotate correction
204 # CORRECT UI OVERLAP FROM HEADER TOOLBAR
205 regs
= context
.area
.regions
206 if context
.preferences
.system
.use_region_overlap
:
207 w
= context
.area
.width
209 h
= context
.area
.height
- regs
[0].height
211 # minus tool leftbar + sidebar right
212 w
= context
.area
.width
- regs
[2].width
- regs
[3].width
213 # minus tool header + header
214 h
= context
.area
.height
- regs
[0].height
- regs
[1].height
217 self
.ratio_inv
= w
/ h
220 # Get camera from scene
221 if context
.space_data
.use_local_camera
and context
.space_data
.camera
:
222 self
.cam
= context
.space_data
.camera
224 self
.cam
= context
.scene
.camera
226 #return if one element is locked (else bypass location)
227 if self
.cam
.lock_rotation
[:] != (False, False, False):
228 self
.report({'WARNING'}, 'Camera rotation is locked')
231 if self
.use_view_center
:
232 self
.center
= mathutils
.Vector((w
/2, h
/2))
234 self
.center
= self
.get_center_view(context
, self
.cam
)
236 # store original rotation mode
237 self
.org_rotation_mode
= self
.cam
.rotation_mode
238 # set to euler to works with quaternions, restored at finish
239 self
.cam
.rotation_mode
= 'XYZ'
240 # store camera matrix world
241 self
.cam_matrix
= self
.cam
.matrix_world
.copy()
242 # self.cam_init_euler = self.cam.rotation_euler.copy()
244 ## initialize current view_offset in camera
245 self
.view_cam_offset
= mathutils
.Vector(context
.space_data
.region_3d
.view_camera_offset
)
248 self
.center
= mathutils
.Vector((w
/2, h
/2))
249 # self.center = mathutils.Vector((context.area.width/2, context.area.height/2))
251 # store current rotation
252 self
._rotation
= context
.space_data
.region_3d
.view_rotation
.copy()
254 # Get current mouse coordination
255 self
.pos_current
= mathutils
.Vector((event
.mouse_region_x
, event
.mouse_region_y
))
257 self
.initial_pos
= self
.pos_current
# for draw debug, else no need
258 # Calculate initial vector
259 self
.vector_initial
= self
.pos_current
- self
.center
260 self
.vector_initial
.normalize()
262 # Initializes the current vector with the same initial vector.
263 self
.vector_current
= self
.vector_initial
.copy()
267 self
.snap_ctrl
= not prefs
.use_ctrl
268 self
.snap_shift
= not prefs
.use_shift
269 self
.snap_alt
= not prefs
.use_alt
270 # round to closer degree and convert back to radians
271 self
.snap_step
= math
.radians(round(math
.degrees(prefs
.rc_angle_step
)))
274 args
= (self
, context
)
276 self
._handle
= bpy
.types
.SpaceView3D
.draw_handler_add(draw_callback_px
, args
, 'WINDOW', 'POST_PIXEL')
277 context
.window_manager
.modal_handler_add(self
)
278 return {'RUNNING_MODAL'}
281 ## -- Set / Reset rotation buttons
283 class RC_OT_Set_rotation(bpy
.types
.Operator
):
284 bl_idname
= 'view3d.rotate_canvas_set'
285 bl_label
= 'Save Rotation'
286 bl_description
= 'Save active camera rotation (per camera property)'
287 bl_options
= {"REGISTER"}
290 def poll(cls
, context
):
291 return context
.area
.type == 'VIEW_3D' \
292 and context
.space_data
.region_3d
.view_perspective
== 'CAMERA'
294 def execute(self
, context
):
295 cam_ob
= context
.scene
.camera
296 cam_ob
['stored_rotation'] = cam_ob
.rotation_euler
297 if not cam_ob
.get('_RNA_UI'):
298 cam_ob
['_RNA_UI'] = {}
299 cam_ob
['_RNA_UI']["stored_rotation"] = {
300 "description":"Stored camera rotation (Gpencil tools > rotate canvas operator)",
302 # "is_overridable_library":0,
307 class RC_OT_Reset_rotation(bpy
.types
.Operator
):
308 bl_idname
= 'view3d.rotate_canvas_reset'
309 bl_label
= 'Restore Rotation'
310 bl_description
= 'Restore active camera rotation from previously saved state'
311 bl_options
= {"REGISTER", "UNDO"}
314 def poll(cls
, context
):
315 return context
.area
.type == 'VIEW_3D' \
316 and context
.space_data
.region_3d
.view_perspective
== 'CAMERA' \
317 and context
.scene
.camera
.get('stored_rotation')
319 def execute(self
, context
):
320 cam_ob
= context
.scene
.camera
321 cam_ob
.rotation_euler
= cam_ob
['stored_rotation']
330 RC_OT_Reset_rotation
,
335 bpy
.utils
.register_class(cls
)
338 for cls
in reversed(classes
):
339 bpy
.utils
.unregister_class(cls
)