GPencil Tools: Canvas rotation improvements

- Added operators to store/restore camera rotation (handy to quickly get back to a prefered angle)
- Use a modifier key to step the rotation by a user-chosen angle
- Addon preferences UI cleanup
This commit is contained in:
Samuel Bernou 2021-01-31 21:44:03 +01:00
parent 9d30d2bccb
commit 7bc0fa6bf2
4 changed files with 183 additions and 80 deletions

View File

@ -21,7 +21,7 @@ bl_info = {
"name": "Grease Pencil Tools",
"description": "Extra tools for Grease Pencil",
"author": "Samuel Bernou, Antonio Vazquez, Daniel Martinez Lara, Matias Mendiola",
"version": (1, 2, 2),
"version": (1, 3, 0),
"blender": (2, 91, 0),
"location": "Sidebar > Grease Pencil > Grease Pencil Tools",
"warning": "",

View File

@ -23,6 +23,7 @@ from bpy.props import (
EnumProperty,
StringProperty,
PointerProperty,
FloatProperty,
# IntProperty,
)
@ -62,13 +63,6 @@ class GreasePencilAddonPrefs(bpy.types.AddonPreferences):
default="Grease Pencil",
update=update_panel)
pref_tabs : EnumProperty(
items=(('PREF', "Preferences", "Preferences properties of GP"),
('TUTO', "Tutorial", "How to use the tool"),
# ('KEYMAP', "Keymap", "customise the default keymap"),
),
default='PREF')
# --- props
use_clic_drag : BoolProperty(
name='Use click drag directly on points',
@ -131,95 +125,124 @@ class GreasePencilAddonPrefs(bpy.types.AddonPreferences):
default = True,
update=auto_rebind)
rc_angle_step: FloatProperty(
name="Angle Steps",
description="Step the rotation using this angle when using rotate canvas step modifier",
default=0.0872664600610733, # 5
min=0.01745329238474369, # 1
max=3.1415927410125732, # # 180
soft_min=0.01745329238474369, # 1
soft_max=1.5707963705062866, # 90
step=10, precision=1, subtype='ANGLE', unit='ROTATION')
def draw(self, context):
prefs = get_addon_prefs()
layout = self.layout
# layout.use_property_split = True
row= layout.row(align=True)
row.prop(self, "pref_tabs", expand=True)
if self.pref_tabs == 'PREF':
## TAB CATEGORY
box = layout.box()
row = box.row(align=True)
row.label(text="Panel Category:")
row.prop(self, "category", text="")
## TAB CATEGORY
box = layout.box()
row = box.row(align=True)
row.label(text="Panel Category:")
row.prop(self, "category", text="")
## BOX DEFORM
box = layout.box()
row = box.row(align=True)
row.label(text='Box Deform:')
row.operator("wm.call_menu", text="", icon='QUESTION').name = "GPT_MT_box_deform_doc"
box.prop(self, "use_clic_drag")
# box.separator()
box.prop(self, "default_deform_type")
box.label(text="Deformer type can be changed during modal with 'M' key, this is for default behavior", icon='INFO')
## BOX DEFORM
box = layout.box()
box.label(text='Box Deform:')
box.prop(self, "use_clic_drag")
# box.separator()
box.prop(self, "default_deform_type")
box.label(text="Deformer type can be changed during modal with 'M' key, this is for default behavior", icon='INFO')
box.prop(self, "auto_swap_deform_type")
box.label(text="Once 'M' is hit, auto swap is desactivated to stay in your chosen mode", icon='INFO')
box.prop(self, "auto_swap_deform_type")
box.label(text="Once 'M' is hit, auto swap is desactivated to stay in your chosen mode", icon='INFO')
## ROTATE CANVAS
box = layout.box()
box.label(text='Rotate canvas:')
## ROTATE CANVAS
box = layout.box()
box.label(text='Rotate canvas:')
box.prop(self, "canvas_use_shortcut", text='Bind Shortcuts')
box.prop(self, "canvas_use_shortcut", text='Bind Shortcuts')
if self.canvas_use_shortcut:
if self.canvas_use_shortcut:
row = box.row()
row.label(text="(Auto rebind when changing shortcut)")#icon=""
# row.operator("prefs.rebind_shortcut", text='Bind/Rebind shortcuts', icon='FILE_REFRESH')#EVENT_SPACEKEY
row = box.row(align = True)
row.prop(self, "use_ctrl", text='Ctrl')#, expand=True
row.prop(self, "use_alt", text='Alt')#, expand=True
row.prop(self, "use_shift", text='Shift')#, expand=True
row.prop(self, "mouse_click",text='')#expand=True
row = box.row()
row.label(text="(Auto rebind when changing shortcut)")#icon=""
# row.operator("prefs.rebind_shortcut", text='Bind/Rebind shortcuts', icon='FILE_REFRESH')#EVENT_SPACEKEY
if not self.use_ctrl and not self.use_alt and not self.use_shift:
box.label(text="Choose at least one modifier to combine with click (default: Ctrl+Alt)", icon="ERROR")# INFO
if not all((self.use_ctrl, self.use_alt, self.use_shift)):
row = box.row(align = True)
row.prop(self, "use_ctrl", text='Ctrl')#, expand=True
row.prop(self, "use_alt", text='Alt')#, expand=True
row.prop(self, "use_shift", text='Shift')#, expand=True
row.prop(self, "mouse_click",text='')#expand=True
snap_key_list = []
if not self.use_ctrl:
snap_key_list.append('Ctrl')
if not self.use_shift:
snap_key_list.append('Shift')
if not self.use_alt:
snap_key_list.append('Alt')
if not self.use_ctrl and not self.use_alt and not self.use_shift:
box.label(text="Choose at least one modifier to combine with click (default: Ctrl+Alt)", icon="ERROR")# INFO
row.label(text=f"Step rotation with: {' or '.join(snap_key_list)}", icon='DRIVER_ROTATIONAL_DIFFERENCE')
row.prop(self, "rc_angle_step", text='Angle Steps')
else:
box.label(text="No hotkey has been set automatically. Following operators needs to be set manually:", icon="ERROR")
box.label(text="view3d.rotate_canvas")
box.prop(self, 'canvas_use_hud')
## SCRUB TIMELINE
box = layout.box()
draw_ts_pref(prefs.ts, box)
else:
box.label(text="No hotkey has been set automatically. Following operators needs to be set manually:", icon="ERROR")
box.label(text="view3d.rotate_canvas")
box.prop(self, 'canvas_use_hud')
if self.pref_tabs == 'TUTO':
## SCRUB TIMELINE
box = layout.box()
draw_ts_pref(prefs.ts, box)
#**Behavior from context mode**
col = layout.column()
col.label(text='Box Deform Tool')
col.label(text="Usage:", icon='MOD_LATTICE')
col.label(text="Use the shortcut 'Ctrl+T' in available modes (listed below)")
col.label(text="The lattice box is generated facing your view (be sure to face canvas if you want to stay on it)")
col.label(text="Use shortcuts below to deform (a help will be displayed in the topbar)")
col.separator()
col.label(text="Shortcuts:", icon='HAND')
col.label(text="Spacebar / Enter : Confirm")
col.label(text="Delete / Backspace / Tab(twice) / Ctrl+T : Cancel")
col.label(text="M : Toggle between Linear and Spline mode at any moment")
col.label(text="1-9 top row number : Subdivide the box")
col.label(text="Ctrl + arrows-keys : Subdivide the box incrementally in individual X/Y axis")
# def box_deform_tuto(layout):
class GPT_MT_box_deform_doc(bpy.types.Menu):
# bl_idname = "OBJECT_MT_custom_menu"
bl_label = "Box Deform Infos Sheet"
col.separator()
col.label(text="Modes and deformation target:", icon='PIVOT_BOUNDBOX')
col.label(text="- Object mode : The whole GP object is deformed (including all frames)")
col.label(text="- GPencil Edit mode : Deform Selected points")
col.label(text="- Gpencil Paint : Deform last Strokes")
# col.label(text="- Lattice edit : Revive the modal after a ctrl+Z")
def draw(self, context):
layout = self.layout
# call another menu
#layout.operator("wm.call_menu", text="Unwrap").name = "VIEW3D_MT_uv_map"
#**Behavior from context mode**
col = layout.column()
col.label(text='Box Deform Tool')
col.label(text="Usage:", icon='MOD_LATTICE')
col.label(text="Use the shortcut 'Ctrl+T' in available modes (listed below)")
col.label(text="The lattice box is generated facing your view (be sure to face canvas if you want to stay on it)")
col.label(text="Use shortcuts below to deform (a help will be displayed in the topbar)")
col.separator()
col.label(text="Notes:", icon='TEXT')
col.label(text="- If you return in box deform after applying (with a ctrl+Z), you need to hit 'Ctrl+T' again to revive the modal.")
col.label(text="- A cancel warning will be displayed the first time you hit Tab")
col.separator()
col.label(text="Shortcuts:", icon='HAND')
col.label(text="Spacebar / Enter : Confirm")
col.label(text="Delete / Backspace / Tab(twice) / Ctrl+T : Cancel")
col.label(text="M : Toggle between Linear and Spline mode at any moment")
col.label(text="1-9 top row number : Subdivide the box")
col.label(text="Ctrl + arrows-keys : Subdivide the box incrementally in individual X/Y axis")
col.separator()
col.label(text="Modes and deformation target:", icon='PIVOT_BOUNDBOX')
col.label(text="- Object mode : The whole GP object is deformed (including all frames)")
col.label(text="- GPencil Edit mode : Deform Selected points")
col.label(text="- Gpencil Paint : Deform last Strokes")
# col.label(text="- Lattice edit : Revive the modal after a ctrl+Z")
col.separator()
col.label(text="Notes:", icon='TEXT')
col.label(text="- If you return in box deform after applying (with a ctrl+Z), you need to hit 'Ctrl+T' again to revive the modal.")
col.label(text="- A cancel warning will be displayed the first time you hit Tab")
### rotate canvas keymap
addon_keymaps = []
def register_keymaps():
pref = get_addon_prefs()
@ -242,11 +265,11 @@ def unregister_keymaps():
addon_keymaps.clear()
### REGISTER ---
classes = (
GPTS_timeline_settings,
GPT_MT_box_deform_doc,
GreasePencilAddonPrefs,
)

View File

@ -12,6 +12,16 @@ 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)
def draw_callback_px(self, context):
# 50% alpha, 2 pixel width line
@ -70,8 +80,9 @@ class RC_OT_RotateCanvas(bpy.types.Operator):
self.cam.rotation_mode = self.org_rotation_mode
return {'FINISHED'}
def modal(self, context, event):
if event.type in {'MOUSEMOVE','INBETWEEN_MOUSEMOVE'}:
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
@ -79,6 +90,19 @@ class RC_OT_RotateCanvas(bpy.types.Operator):
# 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
@ -119,7 +143,8 @@ class RC_OT_RotateCanvas(bpy.types.Operator):
return {'RUNNING_MODAL'}
def invoke(self, context, event):
self.hud = get_addon_prefs().canvas_use_hud
prefs = get_addon_prefs()
self.hud = prefs.canvas_use_hud
self.angle = 0.0
self.in_cam = context.region_data.view_perspective == 'CAMERA'
@ -158,6 +183,13 @@ class RC_OT_RotateCanvas(bpy.types.Operator):
# 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')
@ -165,14 +197,59 @@ class RC_OT_RotateCanvas(bpy.types.Operator):
return {'RUNNING_MODAL'}
## -- 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'}
### --- REGISTER
def register():
bpy.utils.register_class(RC_OT_RotateCanvas)
classes = (
RC_OT_RotateCanvas,
RC_OT_Set_rotation,
RC_OT_Reset_rotation,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
bpy.utils.unregister_class(RC_OT_RotateCanvas)
# if __name__ == "__main__":
# register()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -41,6 +41,9 @@ class GP_PT_sidebarPanel(bpy.types.Panel):
row = layout.row(align=True)
row.operator('view3d.zoom_camera_1_to_1', text = 'Zoom 1:1', icon = 'ZOOM_PREVIOUS')# FULLSCREEN_EXIT?
row.operator('view3d.view_center_camera', text = 'Zoom Fit', icon = 'FULLSCREEN_ENTER')
row = layout.row(align=True)
row.operator('view3d.rotate_canvas_reset', text = 'Reset Rotation', icon = 'FILE_REFRESH')
row.operator('view3d.rotate_canvas_set', text = 'Save Rotation', icon = 'DRIVER_ROTATIONAL_DIFFERENCE')
def menu_boxdeform_entry(self, context):