1 from .prefs
import get_addon_prefs
6 from bpy_extras
.view3d_utils
import location_3d_to_region_2d
7 from bpy
.props
import BoolProperty
, EnumProperty
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
)
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
:
31 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR')
32 bgl
.glEnable(bgl
.GL_BLEND
)
36 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": [self
.center
, self
.initial_pos
]})#self.vector_initial
38 shader
.uniform_float("color", (0.5, 0.5, 0.8, 0.6))
41 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": [self
.center
, self
.pos_current
]})
43 shader
.uniform_float("color", (0.3, 0.7, 0.2, 0.5))
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)
52 # shader.uniform_float("color", (0.8, 0.1, 0.1, 0.5))
55 # restore opengl defaults
57 bgl
.glDisable(bgl
.GL_BLEND
)
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
):
74 https://blender.stackexchange.com/questions/6377/coordinates-of-corners-of-camera-view-border
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
):
89 bpy
.types
.SpaceView3D
.draw_handler_remove(self
._handle
, 'WINDOW')
90 context
.area
.tag_redraw()
92 self
.cam
.rotation_mode
= self
.org_rotation_mode
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
))
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)
109 if self
.snap_ctrl
and event
.ctrl
:
111 if self
.snap_shift
and event
.shift
:
113 if self
.snap_alt
and event
.alt
:
115 ## Snapping to specific degrees angle
117 self
.angle
= step_value(self
.angle
, self
.snap_step
)
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
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
))
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
143 context
.space_data
.region_3d
.view_rotation
= self
._rotation
144 rot
= context
.space_data
.region_3d
.view_rotation
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
157 cam_quat
= self
.cam
.parent
.matrix_world
.inverted().to_quaternion() @ z_up_quat
160 self
.cam
.rotation_euler
= cam_quat
.to_euler('XYZ')
162 context
.space_data
.region_3d
.view_rotation
= z_up_quat
163 self
.execute(context
)
166 if event
.type == 'ESC':#Cancel
167 self
.execute(context
)
169 self
.cam
.matrix_world
= self
.cam_matrix
170 context
.space_data
.region_3d
.view_camera_offset
= self
.view_cam_offset
172 context
.space_data
.region_3d
.view_rotation
= self
._rotation
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
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
192 h
= context
.area
.height
- regs
[0].height
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
200 self
.ratio_inv
= w
/ h
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')
211 if self
.use_view_center
:
212 self
.center
= mathutils
.Vector((w
/2, h
/2))
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
)
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()
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
)))
254 args
= (self
, context
)
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"}
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)",
281 # "is_overridable_library":0,
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"}
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']
307 RC_OT_Reset_rotation
,
312 bpy
.utils
.register_class(cls
)
315 for cls
in reversed(classes
):
316 bpy
.utils
.unregister_class(cls
)