Cleanup: quiet character escape warnings
[blender-addons.git] / greasepencil_tools / rotate_canvas.py
blob36da5ee856895102d7a2be56596aa51330997b56
1 from .prefs import get_addon_prefs
3 import bpy
4 import math
5 import mathutils
6 from bpy_extras.view3d_utils import location_3d_to_region_2d
7 from bpy.props import BoolProperty, EnumProperty
8 from time import time
9 ## draw utils
10 import gpu
11 import bgl
12 import blf
13 from gpu_extras.batch import batch_for_shader
14 from gpu_extras.presets import draw_circle_2d
16 def step_value(value, step):
17 '''return the step closer to the passed value'''
18 abs_angle = abs(value)
19 diff = abs_angle % step
20 lower_step = abs_angle - diff
21 higher_step = lower_step + step
22 if abs_angle - lower_step < higher_step - abs_angle:
23 return math.copysign(lower_step, value)
24 else:
25 return math.copysign(higher_step, value)
27 def draw_callback_px(self, context):
28 # 50% alpha, 2 pixel width line
29 if context.area != self.current_area:
30 return
31 shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
32 bgl.glEnable(bgl.GL_BLEND)
33 bgl.glLineWidth(2)
35 # init
36 batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": [self.center, self.initial_pos]})#self.vector_initial
37 shader.bind()
38 shader.uniform_float("color", (0.5, 0.5, 0.8, 0.6))
39 batch.draw(shader)
41 batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": [self.center, self.pos_current]})
42 shader.bind()
43 shader.uniform_float("color", (0.3, 0.7, 0.2, 0.5))
44 batch.draw(shader)
46 ## debug lines
47 # batch = batch_for_shader(shader, 'LINES', {"pos": [
48 # (0,0), (context.area.width, context.area.height),
49 # (context.area.width, 0), (0, context.area.height)
50 # ]})
51 # shader.bind()
52 # shader.uniform_float("color", (0.8, 0.1, 0.1, 0.5))
53 # batch.draw(shader)
55 # restore opengl defaults
56 bgl.glLineWidth(1)
57 bgl.glDisable(bgl.GL_BLEND)
59 ## text
60 font_id = 0
61 ## draw text debug infos
62 blf.position(font_id, 15, 30, 0)
63 blf.size(font_id, 20, 72)
64 blf.draw(font_id, f'angle: {math.degrees(self.angle):.1f}')
67 class RC_OT_RotateCanvas(bpy.types.Operator):
68 bl_idname = 'view3d.rotate_canvas'
69 bl_label = 'Rotate Canvas'
70 bl_options = {"REGISTER", "UNDO"}
72 def get_center_view(self, context, cam):
73 '''
74 https://blender.stackexchange.com/questions/6377/coordinates-of-corners-of-camera-view-border
75 Thanks to ideasman42
76 '''
78 frame = cam.data.view_frame()
79 mat = cam.matrix_world
80 frame = [mat @ v for v in frame]
81 frame_px = [location_3d_to_region_2d(context.region, context.space_data.region_3d, v) for v in frame]
82 center_x = frame_px[2].x + (frame_px[0].x - frame_px[2].x)/2
83 center_y = frame_px[1].y + (frame_px[0].y - frame_px[1].y)/2
85 return mathutils.Vector((center_x, center_y))
87 def execute(self, context):
88 if self.hud:
89 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
90 context.area.tag_redraw()
91 if self.in_cam:
92 self.cam.rotation_mode = self.org_rotation_mode
93 return {'FINISHED'}
96 def modal(self, context, event):
97 if event.type in {'MOUSEMOVE'}:#,'INBETWEEN_MOUSEMOVE'
98 # Get current mouse coordination (region)
99 self.pos_current = mathutils.Vector((event.mouse_region_x, event.mouse_region_y))
100 # Get current vector
101 self.vector_current = (self.pos_current - self.center).normalized()
102 # Calculates the angle between initial and current vectors
103 self.angle = self.vector_initial.angle_signed(self.vector_current)#radian
104 # print (math.degrees(self.angle), self.vector_initial, self.vector_current)
107 ## handle snap key
108 snap = False
109 if self.snap_ctrl and event.ctrl:
110 snap = True
111 if self.snap_shift and event.shift:
112 snap = True
113 if self.snap_alt and event.alt:
114 snap = True
115 ## Snapping to specific degrees angle
116 if snap:
117 self.angle = step_value(self.angle, self.snap_step)
119 if self.in_cam:
120 self.cam.matrix_world = self.cam_matrix
121 self.cam.rotation_euler.rotate_axis("Z", self.angle)
123 if self.use_view_center:
124 ## apply inverse of the rotation on view offset in cam rotate from view center
125 neg = -self.angle
126 rot_mat2d = mathutils.Matrix([[math.cos(neg), -math.sin(neg)], [math.sin(neg), math.cos(neg)]])
128 # scale_mat = mathutils.Matrix([[1.0, 0.0], [0.0, self.ratio]])
129 new_cam_offset = self.view_cam_offset.copy()
131 ## area deformation correction
132 new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio))
134 ## rotate by matrix
135 new_cam_offset.rotate(rot_mat2d)
137 ## area deformation restore
138 new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio_inv))
140 context.space_data.region_3d.view_camera_offset = new_cam_offset
142 else: # free view
143 context.space_data.region_3d.view_rotation = self._rotation
144 rot = context.space_data.region_3d.view_rotation
145 rot = rot.to_euler()
146 rot.rotate_axis("Z", self.angle)
147 context.space_data.region_3d.view_rotation = rot.to_quaternion()
149 if event.type in {'RIGHTMOUSE', 'LEFTMOUSE', 'MIDDLEMOUSE'} and event.value == 'RELEASE':
150 # Trigger reset : Less than 150ms and less than 2 degrees move
151 if time() - self.timer < 0.15 and abs(math.degrees(self.angle)) < 2:
152 # self.report({'INFO'}, 'Reset')
153 aim = context.space_data.region_3d.view_rotation @ mathutils.Vector((0.0, 0.0, 1.0))#view vector
154 z_up_quat = aim.to_track_quat('Z','Y')#track Z, up Y
155 if self.in_cam:
156 if self.cam.parent:
157 cam_quat = self.cam.parent.matrix_world.inverted().to_quaternion() @ z_up_quat
158 else:
159 cam_quat = z_up_quat
160 self.cam.rotation_euler = cam_quat.to_euler('XYZ')
161 else:
162 context.space_data.region_3d.view_rotation = z_up_quat
163 self.execute(context)
164 return {'FINISHED'}
166 if event.type == 'ESC':#Cancel
167 self.execute(context)
168 if self.in_cam:
169 self.cam.matrix_world = self.cam_matrix
170 context.space_data.region_3d.view_camera_offset = self.view_cam_offset
171 else:
172 context.space_data.region_3d.view_rotation = self._rotation
173 return {'CANCELLED'}
175 return {'RUNNING_MODAL'}
177 def invoke(self, context, event):
178 self.current_area = context.area
179 prefs = get_addon_prefs()
180 self.hud = prefs.canvas_use_hud
181 self.use_view_center = prefs.canvas_use_view_center
182 self.angle = 0.0
183 self.in_cam = context.region_data.view_perspective == 'CAMERA'
185 ## store ratio for view rotate correction
187 # CORRECT UI OVERLAP FROM HEADER TOOLBAR
188 regs = context.area.regions
189 if context.preferences.system.use_region_overlap:
190 w = context.area.width
191 # minus tool header
192 h = context.area.height - regs[0].height
193 else:
194 # minus tool leftbar + sidebar right
195 w = context.area.width - regs[2].width - regs[3].width
196 # minus tool header + header
197 h = context.area.height - regs[0].height - regs[1].height
199 self.ratio = h / w
200 self.ratio_inv = w / h
202 if self.in_cam:
203 # Get camera from scene
204 self.cam = bpy.context.scene.camera
206 #return if one element is locked (else bypass location)
207 if self.cam.lock_rotation[:] != (False, False, False):
208 self.report({'WARNING'}, 'Camera rotation is locked')
209 return {'CANCELLED'}
211 if self.use_view_center:
212 self.center = mathutils.Vector((w/2, h/2))
213 else:
214 self.center = self.get_center_view(context, self.cam)
216 # store original rotation mode
217 self.org_rotation_mode = self.cam.rotation_mode
218 # set to euler to works with quaternions, restored at finish
219 self.cam.rotation_mode = 'XYZ'
220 # store camera matrix world
221 self.cam_matrix = self.cam.matrix_world.copy()
222 # self.cam_init_euler = self.cam.rotation_euler.copy()
224 ## initialize current view_offset in camera
225 self.view_cam_offset = mathutils.Vector(context.space_data.region_3d.view_camera_offset)
227 else:
228 self.center = mathutils.Vector((w/2, h/2))
229 # self.center = mathutils.Vector((context.area.width/2, context.area.height/2))
231 # store current rotation
232 self._rotation = context.space_data.region_3d.view_rotation.copy()
234 # Get current mouse coordination
235 self.pos_current = mathutils.Vector((event.mouse_region_x, event.mouse_region_y))
237 self.initial_pos = self.pos_current# for draw debug, else no need
238 # Calculate inital vector
239 self.vector_initial = self.pos_current - self.center
240 self.vector_initial.normalize()
242 # Initializes the current vector with the same initial vector.
243 self.vector_current = self.vector_initial.copy()
246 #Snap keys
247 self.snap_ctrl = not prefs.use_ctrl
248 self.snap_shift = not prefs.use_shift
249 self.snap_alt = not prefs.use_alt
250 # round to closer degree and convert back to radians
251 self.snap_step = math.radians(round(math.degrees(prefs.rc_angle_step)))
253 self.timer = time()
254 args = (self, context)
255 if self.hud:
256 self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
257 context.window_manager.modal_handler_add(self)
258 return {'RUNNING_MODAL'}
261 ## -- Set / Reset rotation buttons
263 class RC_OT_Set_rotation(bpy.types.Operator):
264 bl_idname = 'view3d.rotate_canvas_set'
265 bl_label = 'Save Rotation'
266 bl_description = 'Save active camera rotation (per camera property)'
267 bl_options = {"REGISTER"}
269 @classmethod
270 def poll(cls, context):
271 return context.space_data.region_3d.view_perspective == 'CAMERA'
273 def execute(self, context):
274 cam_ob = context.scene.camera
275 cam_ob['stored_rotation'] = cam_ob.rotation_euler
276 if not cam_ob.get('_RNA_UI'):
277 cam_ob['_RNA_UI'] = {}
278 cam_ob['_RNA_UI']["stored_rotation"] = {
279 "description":"Stored camera rotation (Gpencil tools > rotate canvas operator)",
280 "subtype":'EULER',
281 # "is_overridable_library":0,
284 return {'FINISHED'}
286 class RC_OT_Reset_rotation(bpy.types.Operator):
287 bl_idname = 'view3d.rotate_canvas_reset'
288 bl_label = 'Restore Rotation'
289 bl_description = 'Restore active camera rotation from previously saved state'
290 bl_options = {"REGISTER", "UNDO"}
292 @classmethod
293 def poll(cls, context):
294 return context.space_data.region_3d.view_perspective == 'CAMERA' and context.scene.camera.get('stored_rotation')
296 def execute(self, context):
297 cam_ob = context.scene.camera
298 cam_ob.rotation_euler = cam_ob['stored_rotation']
299 return {'FINISHED'}
302 ### --- REGISTER
304 classes = (
305 RC_OT_RotateCanvas,
306 RC_OT_Set_rotation,
307 RC_OT_Reset_rotation,
310 def register():
311 for cls in classes:
312 bpy.utils.register_class(cls)
314 def unregister():
315 for cls in reversed(classes):
316 bpy.utils.unregister_class(cls)