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"}
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
113 # Undo step is only needed if used within camera
114 bpy
.ops
.ed
.undo_push(message
='Rotate Canvas')
118 def modal(self
, context
, event
):
119 if event
.type in {'MOUSEMOVE'}:#,'INBETWEEN_MOUSEMOVE'
120 # Get current mouse coordination (region)
121 self
.pos_current
= mathutils
.Vector((event
.mouse_region_x
, event
.mouse_region_y
))
123 self
.vector_current
= (self
.pos_current
- self
.center
).normalized()
124 # Calculates the angle between initial and current vectors
125 self
.angle
= self
.vector_initial
.angle_signed(self
.vector_current
)#radian
126 # print (math.degrees(self.angle), self.vector_initial, self.vector_current)
131 if self
.snap_ctrl
and event
.ctrl
:
133 if self
.snap_shift
and event
.shift
:
135 if self
.snap_alt
and event
.alt
:
137 ## Snapping to specific degrees angle
139 self
.angle
= step_value(self
.angle
, self
.snap_step
)
142 self
.cam
.matrix_world
= self
.cam_matrix
143 self
.cam
.rotation_euler
.rotate_axis("Z", self
.angle
)
145 if self
.use_view_center
:
146 ## apply inverse rotation on view offset
147 self
.set_cam_view_offset_from_angle(context
, self
.angle
)
150 context
.space_data
.region_3d
.view_rotation
= self
._rotation
151 rot
= context
.space_data
.region_3d
.view_rotation
153 rot
.rotate_axis("Z", self
.angle
)
154 context
.space_data
.region_3d
.view_rotation
= rot
.to_quaternion()
156 if event
.type in {'RIGHTMOUSE', 'LEFTMOUSE', 'MIDDLEMOUSE'} and event
.value
== 'RELEASE':
157 # Trigger reset : Less than 150ms and less than 2 degrees move
158 if time() - self
.timer
< 0.15 and abs(math
.degrees(self
.angle
)) < 2:
159 # self.report({'INFO'}, 'Reset')
160 aim
= context
.space_data
.region_3d
.view_rotation
@ mathutils
.Vector((0.0, 0.0, 1.0)) # view vector
161 z_up_quat
= aim
.to_track_quat('Z','Y') # track Z, up Y
164 q
= self
.cam
.matrix_world
.to_quaternion() # store current rotation
167 q
= self
.cam
.parent
.matrix_world
.inverted().to_quaternion() @ q
168 cam_quat
= self
.cam
.parent
.matrix_world
.inverted().to_quaternion() @ z_up_quat
171 self
.cam
.rotation_euler
= cam_quat
.to_euler('XYZ')
173 # get diff angle (might be better way to get view axis rot diff)
174 diff_angle
= q
.rotation_difference(cam_quat
).to_euler('ZXY').z
175 # print('diff_angle: ', math.degrees(diff_angle))
176 self
.set_cam_view_offset_from_angle(context
, diff_angle
)
179 context
.space_data
.region_3d
.view_rotation
= z_up_quat
180 self
.execute(context
)
183 if event
.type == 'ESC':#Cancel
184 self
.execute(context
)
186 self
.cam
.matrix_world
= self
.cam_matrix
187 context
.space_data
.region_3d
.view_camera_offset
= self
.view_cam_offset
189 context
.space_data
.region_3d
.view_rotation
= self
._rotation
192 return {'RUNNING_MODAL'}
194 def invoke(self
, context
, event
):
195 self
.current_area
= context
.area
196 prefs
= get_addon_prefs()
197 self
.hud
= prefs
.canvas_use_hud
198 self
.use_view_center
= prefs
.canvas_use_view_center
200 ## Check if scene camera or local camera exists ?
201 # if (context.space_data.use_local_camera and context.space_data.camera) or context.scene.camera
202 self
.in_cam
= context
.region_data
.view_perspective
== 'CAMERA'
204 ## store ratio for view rotate correction
206 # CORRECT UI OVERLAP FROM HEADER TOOLBAR
207 regs
= context
.area
.regions
208 if context
.preferences
.system
.use_region_overlap
:
209 w
= context
.area
.width
211 h
= context
.area
.height
- regs
[0].height
213 # minus tool leftbar + sidebar right
214 w
= context
.area
.width
- regs
[2].width
- regs
[3].width
215 # minus tool header + header
216 h
= context
.area
.height
- regs
[0].height
- regs
[1].height
219 self
.ratio_inv
= w
/ h
222 # Get camera from scene
223 if context
.space_data
.use_local_camera
and context
.space_data
.camera
:
224 self
.cam
= context
.space_data
.camera
226 self
.cam
= context
.scene
.camera
228 #return if one element is locked (else bypass location)
229 if self
.cam
.lock_rotation
[:] != (False, False, False):
230 self
.report({'WARNING'}, 'Camera rotation is locked')
233 if self
.use_view_center
:
234 self
.center
= mathutils
.Vector((w
/2, h
/2))
236 self
.center
= self
.get_center_view(context
, self
.cam
)
238 # store original rotation mode
239 self
.org_rotation_mode
= self
.cam
.rotation_mode
240 # set to euler to works with quaternions, restored at finish
241 self
.cam
.rotation_mode
= 'XYZ'
242 # store camera matrix world
243 self
.cam_matrix
= self
.cam
.matrix_world
.copy()
244 # self.cam_init_euler = self.cam.rotation_euler.copy()
246 ## initialize current view_offset in camera
247 self
.view_cam_offset
= mathutils
.Vector(context
.space_data
.region_3d
.view_camera_offset
)
250 self
.center
= mathutils
.Vector((w
/2, h
/2))
251 # self.center = mathutils.Vector((context.area.width/2, context.area.height/2))
253 # store current rotation
254 self
._rotation
= context
.space_data
.region_3d
.view_rotation
.copy()
256 # Get current mouse coordination
257 self
.pos_current
= mathutils
.Vector((event
.mouse_region_x
, event
.mouse_region_y
))
259 self
.initial_pos
= self
.pos_current
# for draw debug, else no need
260 # Calculate initial vector
261 self
.vector_initial
= self
.pos_current
- self
.center
262 self
.vector_initial
.normalize()
264 # Initializes the current vector with the same initial vector.
265 self
.vector_current
= self
.vector_initial
.copy()
269 self
.snap_ctrl
= not prefs
.use_ctrl
270 self
.snap_shift
= not prefs
.use_shift
271 self
.snap_alt
= not prefs
.use_alt
272 # round to closer degree and convert back to radians
273 self
.snap_step
= math
.radians(round(math
.degrees(prefs
.rc_angle_step
)))
276 args
= (self
, context
)
278 self
._handle
= bpy
.types
.SpaceView3D
.draw_handler_add(draw_callback_px
, args
, 'WINDOW', 'POST_PIXEL')
279 context
.window_manager
.modal_handler_add(self
)
280 return {'RUNNING_MODAL'}
283 ## -- Set / Reset rotation buttons
285 class RC_OT_Set_rotation(bpy
.types
.Operator
):
286 bl_idname
= 'view3d.rotate_canvas_set'
287 bl_label
= 'Save Rotation'
288 bl_description
= 'Save active camera rotation (per camera property)'
289 bl_options
= {"REGISTER"}
292 def poll(cls
, context
):
293 return context
.area
.type == 'VIEW_3D' \
294 and context
.space_data
.region_3d
.view_perspective
== 'CAMERA'
296 def execute(self
, context
):
297 cam_ob
= context
.scene
.camera
298 cam_ob
['stored_rotation'] = cam_ob
.rotation_euler
299 if not cam_ob
.get('_RNA_UI'):
300 cam_ob
['_RNA_UI'] = {}
301 cam_ob
['_RNA_UI']["stored_rotation"] = {
302 "description":"Stored camera rotation (Gpencil tools > rotate canvas operator)",
304 # "is_overridable_library":0,
309 class RC_OT_Reset_rotation(bpy
.types
.Operator
):
310 bl_idname
= 'view3d.rotate_canvas_reset'
311 bl_label
= 'Restore Rotation'
312 bl_description
= 'Restore active camera rotation from previously saved state'
313 bl_options
= {"REGISTER", "UNDO"}
316 def poll(cls
, context
):
317 return context
.area
.type == 'VIEW_3D' \
318 and context
.space_data
.region_3d
.view_perspective
== 'CAMERA' \
319 and context
.scene
.camera
.get('stored_rotation')
321 def execute(self
, context
):
322 cam_ob
= context
.scene
.camera
323 cam_ob
.rotation_euler
= cam_ob
['stored_rotation']
332 RC_OT_Reset_rotation
,
337 bpy
.utils
.register_class(cls
)
340 for cls
in reversed(classes
):
341 bpy
.utils
.unregister_class(cls
)