diff --git a/greasepencil_tools/Official_GP_brush_pack_V1.blend b/greasepencil_tools/Official_GP_brush_pack_V1.blend
new file mode 100644
index 0000000000000000000000000000000000000000..ca9c4205b4c1b0291d2eceda7a36488a9097461a
Binary files /dev/null and b/greasepencil_tools/Official_GP_brush_pack_V1.blend differ
diff --git a/greasepencil_tools/__init__.py b/greasepencil_tools/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..bad7f2e76b242c17cf0ba94a8ef0e02d7a4ce6b0
--- /dev/null
+++ b/greasepencil_tools/__init__.py
@@ -0,0 +1,63 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+
+bl_info = {
+"name": "Grease Pencil Tools",
+"description": "Pack of tools for Grease pencil drawing",
+"author": "Samuel Bernou, Antonio Vazquez, Daniel Martinez Lara, Matias Mendiola",
+"version": (1, 1, 2),
+"blender": (2, 91, 0),
+"location": "Sidebar > Grease pencil > Grease pencil",
+"warning": "",
+"doc_url": "https://docs.blender.org/manual/en/dev/addons/object/grease_pencil_tools.html",
+"tracker_url": "https://github.com/Pullusb/greasepencil-addon/issues",
+"category": "Object",
+"support": "OFFICIAL",
+}
+
+import bpy
+from .  import (prefs,
+                box_deform,
+                line_reshape,
+                rotate_canvas,
+                import_brush_pack,
+                ui_panels,
+                )
+
+def register():
+    prefs.register()
+    box_deform.register()
+    line_reshape.register()
+    rotate_canvas.register()
+    import_brush_pack.register()
+    ui_panels.register()
+
+    ## update tab name with update in pref file (passing addon_prefs)
+    prefs.update_panel(prefs.get_addon_prefs(), bpy.context)
+
+def unregister():
+    ui_panels.unregister()
+    import_brush_pack.unregister()
+    rotate_canvas.unregister()
+    box_deform.unregister()
+    line_reshape.unregister()
+    prefs.unregister()
+
+if __name__ == "__main__":
+    register()
\ No newline at end of file
diff --git a/greasepencil_tools/box_deform.py b/greasepencil_tools/box_deform.py
new file mode 100644
index 0000000000000000000000000000000000000000..6354f019a77dea74b280a4475f9d1bbe8fd3fa76
--- /dev/null
+++ b/greasepencil_tools/box_deform.py
@@ -0,0 +1,585 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+'''Based on Box_deform standalone addon - Author: Samuel Bernou'''
+
+from .prefs import get_addon_prefs
+
+import bpy
+import numpy as np
+
+def location_to_region(worldcoords):
+    from bpy_extras import view3d_utils
+    return view3d_utils.location_3d_to_region_2d(bpy.context.region, bpy.context.space_data.region_3d, worldcoords)
+
+def region_to_location(viewcoords, depthcoords):
+    from bpy_extras import view3d_utils
+    return view3d_utils.region_2d_to_location_3d(bpy.context.region, bpy.context.space_data.region_3d, viewcoords, depthcoords)
+
+def assign_vg(obj, vg_name):
+    ## create vertex group
+    vg = obj.vertex_groups.get(vg_name)
+    if vg:
+        # remove to start clean
+        obj.vertex_groups.remove(vg)
+    vg = obj.vertex_groups.new(name=vg_name)
+    bpy.ops.gpencil.vertex_group_assign()
+    return vg
+
+def view_cage(obj):
+    prefs = get_addon_prefs()
+    lattice_interp = prefs.default_deform_type
+
+    gp = obj.data
+    gpl = gp.layers
+
+    coords = []
+    initial_mode = bpy.context.mode
+
+    ## get points
+    if bpy.context.mode == 'EDIT_GPENCIL':
+        for l in gpl:
+            if l.lock or l.hide or not l.active_frame:#or len(l.frames)
+                continue
+            if gp.use_multiedit:
+                target_frames = [f for f in l.frames if f.select]
+            else:
+                target_frames = [l.active_frame]
+            
+            for f in target_frames:
+                for s in f.strokes:
+                    if not s.select:
+                        continue
+                    for p in s.points:
+                        if p.select:
+                            # get real location
+                            coords.append(obj.matrix_world @ p.co)
+
+    elif bpy.context.mode == 'OBJECT':#object mode -> all points
+        for l in gpl:# if l.hide:continue# only visible ? (might break things)
+            if not len(l.frames):
+                continue#skip frameless layer
+            for s in l.active_frame.strokes:
+                for p in s.points:
+                    coords.append(obj.matrix_world @ p.co)
+    
+    elif bpy.context.mode == 'PAINT_GPENCIL':
+        # get last stroke points coordinated
+        if not gpl.active or not gpl.active.active_frame:
+            return 'No frame to deform'
+
+        if not len(gpl.active.active_frame.strokes):
+            return 'No stroke found to deform'
+        
+        paint_id = -1
+        if bpy.context.scene.tool_settings.use_gpencil_draw_onback:
+            paint_id = 0
+        coords = [obj.matrix_world @ p.co for p in gpl.active.active_frame.strokes[paint_id].points]
+    
+    else:
+        return 'Wrong mode!'
+
+    if not coords:
+        ## maybe silent return instead (need special str code to manage errorless return)
+        return 'No points found!'
+
+    if bpy.context.mode in ('EDIT_GPENCIL', 'PAINT_GPENCIL') and len(coords) < 2:
+        # Dont block object mod
+        return 'Less than two point selected'
+
+    vg_name = 'lattice_cage_deform_group'
+
+    if bpy.context.mode == 'EDIT_GPENCIL':
+        vg = assign_vg(obj, vg_name)
+    
+    if bpy.context.mode == 'PAINT_GPENCIL':
+        # points cannot be assign to API yet(ugly and slow workaround but only way)
+        # -> https://developer.blender.org/T56280 so, hop'in'ops !
+        
+        # store selection and deselect all
+        plist = []
+        for s in gpl.active.active_frame.strokes:
+            for p in s.points:
+                plist.append([p, p.select])
+                p.select = False
+        
+        # select
+        ## foreach_set does not update
+        # gpl.active.active_frame.strokes[paint_id].points.foreach_set('select', [True]*len(gpl.active.active_frame.strokes[paint_id].points))
+        for p in gpl.active.active_frame.strokes[paint_id].points:
+            p.select = True
+        
+        # assign
+        bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
+        vg = assign_vg(obj, vg_name)
+
+        # restore
+        for pl in plist:
+            pl[0].select = pl[1]
+        
+
+    ## View axis Mode ---
+
+    ## get view coordinate of all points
+    coords2D = [location_to_region(co) for co in coords]
+
+    # find centroid for depth (or more economic, use obj origin...)
+    centroid = np.mean(coords, axis=0)
+
+    # not a mean ! a mean of extreme ! centroid2d = np.mean(coords2D, axis=0)
+    all_x, all_y = np.array(coords2D)[:, 0], np.array(coords2D)[:, 1]
+    min_x, min_y = np.min(all_x), np.min(all_y)
+    max_x, max_y = np.max(all_x), np.max(all_y)
+
+    width = (max_x - min_x)
+    height = (max_y - min_y)
+    center_x = min_x + (width/2)
+    center_y = min_y + (height/2)
+
+    centroid2d = (center_x,center_y)
+    center = region_to_location(centroid2d, centroid)
+    # bpy.context.scene.cursor.location = center#Dbg
+
+
+    #corner Bottom-left to Bottom-right
+    x0 = region_to_location((min_x, min_y), centroid)
+    x1 = region_to_location((max_x, min_y), centroid)
+    x_worldsize = (x0 - x1).length
+
+    #corner Bottom-left to top-left
+    y0 = region_to_location((min_x, min_y), centroid)
+    y1 = region_to_location((min_x, max_y), centroid)
+    y_worldsize = (y0 - y1).length
+
+    ## in case of 3
+
+    lattice_name = 'lattice_cage_deform'
+    # cleaning
+    cage = bpy.data.objects.get(lattice_name)
+    if cage:
+        bpy.data.objects.remove(cage)
+
+    lattice = bpy.data.lattices.get(lattice_name)
+    if lattice:
+        bpy.data.lattices.remove(lattice)
+
+    # create lattice object
+    lattice = bpy.data.lattices.new(lattice_name)
+    cage = bpy.data.objects.new(lattice_name, lattice)
+    cage.show_in_front = True
+
+    ## Master (root) collection
+    bpy.context.scene.collection.objects.link(cage)
+
+    # spawn cage and align it to view (Again ! align something to a vector !!! argg)
+
+    r3d = bpy.context.space_data.region_3d
+    viewmat = r3d.view_matrix
+
+    cage.matrix_world = viewmat.inverted()
+    cage.scale = (x_worldsize, y_worldsize, 1)
+    ## Z aligned in view direction (need minus X 90 degree to be aligned FRONT)
+    # cage.rotation_euler.x -= radians(90)
+    # cage.scale = (x_worldsize, 1, y_worldsize)
+    cage.location = center
+
+    lattice.points_u = 2
+    lattice.points_v = 2
+    lattice.points_w = 1
+
+    lattice.interpolation_type_u = lattice_interp#'KEY_LINEAR'-'KEY_BSPLINE'
+    lattice.interpolation_type_v = lattice_interp#'KEY_LINEAR'-'KEY_BSPLINE'
+    lattice.interpolation_type_w = lattice_interp#'KEY_LINEAR'-'KEY_BSPLINE'
+
+    mod = obj.grease_pencil_modifiers.new('tmp_lattice', 'GP_LATTICE')
+
+    # move to top if modifiers exists
+    for _ in range(len(obj.grease_pencil_modifiers)):
+        bpy.ops.object.gpencil_modifier_move_up(modifier='tmp_lattice')
+
+    mod.object = cage
+
+    if initial_mode == 'PAINT_GPENCIL':
+        mod.layer = gpl.active.info
+    
+    # note : if initial was Paint, changed to Edit
+    #        so vertex attribution is valid even for paint
+    if bpy.context.mode == 'EDIT_GPENCIL':
+        mod.vertex_group = vg.name
+
+    #Go in object mode if not already
+    if bpy.context.mode != 'OBJECT':
+        bpy.ops.object.mode_set(mode='OBJECT')
+
+    # Store name of deformed object in case of 'revive modal' 
+    cage.vertex_groups.new(name=obj.name)
+
+    ## select and make cage active
+    # cage.select_set(True)
+    bpy.context.view_layer.objects.active = cage
+    obj.select_set(False)#deselect GP object
+    bpy.ops.object.mode_set(mode='EDIT')# go in lattice edit mode
+    bpy.ops.lattice.select_all(action='SELECT')# select all points
+
+    if prefs.use_clic_drag:
+        ## Eventually change tool mode to tweak for direct point editing (reset after before leaving)
+        bpy.ops.wm.tool_set_by_id(name="builtin.select")# Tweaktoolcode
+    return cage
+
+
+def back_to_obj(obj, gp_mode, org_lattice_toolset, context):
+    if context.mode == 'EDIT_LATTICE' and org_lattice_toolset:# Tweaktoolcode - restore the active tool used by lattice edit..
+        bpy.ops.wm.tool_set_by_id(name = org_lattice_toolset)# Tweaktoolcode
+    
+    # gp object active and selected
+    bpy.ops.object.mode_set(mode='OBJECT')
+    obj.select_set(True)
+    bpy.context.view_layer.objects.active = obj
+
+
+def delete_cage(cage):
+    lattice = cage.data
+    bpy.data.objects.remove(cage)
+    bpy.data.lattices.remove(lattice)
+
+def apply_cage(gp_obj, cage):
+    mod = gp_obj.grease_pencil_modifiers.get('tmp_lattice')
+    if mod:
+        bpy.ops.object.gpencil_modifier_apply(apply_as='DATA', modifier=mod.name)
+    else:
+        print('tmp_lattice modifier not found to apply...')
+
+    delete_cage(cage)
+
+def cancel_cage(gp_obj, cage):
+    #remove modifier
+    mod = gp_obj.grease_pencil_modifiers.get('tmp_lattice')
+    if mod:
+        gp_obj.grease_pencil_modifiers.remove(mod)
+    else:
+        print('tmp_lattice modifier not found to remove...')
+    
+    delete_cage(cage)
+    
+
+class GP_OT_latticeGpDeform(bpy.types.Operator):
+    """Create a lattice to use as quad corner transform"""
+    bl_idname = "gp.latticedeform"
+    bl_label = "Box Deform"
+    bl_description = "Use lattice for free box transforms on grease pencil points (Ctrl+T)"
+    bl_options = {"REGISTER", "UNDO"}
+
+    @classmethod
+    def poll(cls, context):
+        return context.object is not None and context.object.type in ('GPENCIL','LATTICE')
+
+    # local variable
+    tab_press_ct = 0
+
+    def modal(self, context, event):
+        display_text = f"Deform Cage size: {self.lat.points_u}x{self.lat.points_v} (1-9 or ctrl + ←→↑↓)  | \
+mode (M) : {'Linear' if self.lat.interpolation_type_u == 'KEY_LINEAR' else 'Spline'} | \
+valid:Spacebar/Enter, cancel:Del/Backspace/Tab/Ctrl+T"
+        context.area.header_text_set(display_text)
+
+
+        ## Handle ctrl+Z
+        if event.type in {'Z'} and event.value == 'PRESS' and event.ctrl:
+            ## Disable (capture key)
+            return {"RUNNING_MODAL"}
+            ## Not found how possible to find modal start point in undo stack to 
+            # print('ops list', context.window_manager.operators.keys())
+            # if context.window_manager.operators:#can be empty
+            #     print('\nlast name', context.window_manager.operators[-1].name)
+
+        # Auto interpo check
+        if self.auto_interp:
+            if event.type in {'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'ZERO',} and event.value == 'PRESS':
+                self.set_lattice_interp('KEY_BSPLINE')
+            if event.type in {'DOWN_ARROW', "UP_ARROW", "RIGHT_ARROW", "LEFT_ARROW"} and event.value == 'PRESS' and event.ctrl:
+                self.set_lattice_interp('KEY_BSPLINE')
+            if event.type in {'ONE'} and event.value == 'PRESS':
+                self.set_lattice_interp('KEY_LINEAR')
+
+        # Single keys
+        if event.type in {'H'} and event.value == 'PRESS':
+            # self.report({'INFO'}, "Can't hide")
+            return {"RUNNING_MODAL"}
+        
+        if event.type in {'ONE'} and event.value == 'PRESS':# , 'NUMPAD_1'
+            self.lat.points_u = self.lat.points_v = 2
+            return {"RUNNING_MODAL"}
+
+        if event.type in {'TWO'} and event.value == 'PRESS':# , 'NUMPAD_2'
+            self.lat.points_u = self.lat.points_v = 3
+            return {"RUNNING_MODAL"}
+
+        if event.type in {'THREE'} and event.value == 'PRESS':# , 'NUMPAD_3'
+            self.lat.points_u = self.lat.points_v = 4
+            return {"RUNNING_MODAL"}
+
+        if event.type in {'FOUR'} and event.value == 'PRESS':# , 'NUMPAD_4'
+            self.lat.points_u = self.lat.points_v = 5
+            return {"RUNNING_MODAL"}
+
+        if event.type in {'FIVE'} and event.value == 'PRESS':# , 'NUMPAD_5'
+            self.lat.points_u = self.lat.points_v = 6
+            return {"RUNNING_MODAL"}
+
+        if event.type in {'SIX'} and event.value == 'PRESS':# , 'NUMPAD_6'
+            self.lat.points_u = self.lat.points_v = 7
+            return {"RUNNING_MODAL"}
+
+        if event.type in {'SEVEN'} and event.value == 'PRESS':# , 'NUMPAD_7'
+            self.lat.points_u = self.lat.points_v = 8
+            return {"RUNNING_MODAL"}
+
+        if event.type in {'EIGHT'} and event.value == 'PRESS':# , 'NUMPAD_8'
+            self.lat.points_u = self.lat.points_v = 9
+            return {"RUNNING_MODAL"}
+
+        if event.type in {'NINE'} and event.value == 'PRESS':# , 'NUMPAD_9'
+            self.lat.points_u = self.lat.points_v = 10
+            return {"RUNNING_MODAL"}
+
+        if event.type in {'ZERO'} and event.value == 'PRESS':# , 'NUMPAD_0'
+            self.lat.points_u = 2
+            self.lat.points_v = 1
+            return {"RUNNING_MODAL"}
+        
+        if event.type in {'RIGHT_ARROW'} and event.value == 'PRESS' and event.ctrl:
+            if self.lat.points_u < 20:
+                self.lat.points_u += 1 
+            return {"RUNNING_MODAL"}
+
+        if event.type in {'LEFT_ARROW'} and event.value == 'PRESS' and event.ctrl:
+            if self.lat.points_u > 1:
+                self.lat.points_u -= 1 
+            return {"RUNNING_MODAL"}
+
+        if event.type in {'UP_ARROW'} and event.value == 'PRESS' and event.ctrl:
+            if self.lat.points_v < 20:
+                self.lat.points_v += 1 
+            return {"RUNNING_MODAL"}
+
+        if event.type in {'DOWN_ARROW'} and event.value == 'PRESS' and event.ctrl:
+            if self.lat.points_v > 1:
+                self.lat.points_v -= 1 
+            return {"RUNNING_MODAL"}
+
+
+        # change modes
+        if event.type in {'M'} and event.value == 'PRESS':
+            self.auto_interp = False
+            interp = 'KEY_BSPLINE' if self.lat.interpolation_type_u == 'KEY_LINEAR' else 'KEY_LINEAR'
+            self.set_lattice_interp(interp)
+            return {"RUNNING_MODAL"}
+
+        # Valid
+        if event.type in {'RET', 'SPACE'}:
+            if event.value == 'PRESS':
+                context.window_manager.boxdeform_running = False
+                self.restore_prefs(context)
+                back_to_obj(self.gp_obj, self.gp_mode, self.org_lattice_toolset, context)
+                apply_cage(self.gp_obj, self.cage)#must be in object mode
+                
+                # back to original mode 
+                if self.gp_mode != 'OBJECT':
+                    bpy.ops.object.mode_set(mode=self.gp_mode)
+
+                context.area.header_text_set(None)#reset header
+
+                return {'FINISHED'}
+        
+        # Abort ---
+        # One Warning for Tab cancellation.
+        if event.type == 'TAB' and event.value == 'PRESS':
+            self.tab_press_ct += 1
+            if self.tab_press_ct < 2:
+                self.report({'WARNING'}, "Pressing TAB again will Cancel")
+                return {"RUNNING_MODAL"}
+
+        if event.type in {'T'} and event.value == 'PRESS' and event.ctrl:# Retyped same shortcut
+            self.cancel(context)
+            return {'CANCELLED'}
+
+        if event.type in {'DEL', 'BACK_SPACE'} or self.tab_press_ct >= 2:#'ESC',
+            self.cancel(context)
+            return {'CANCELLED'}
+
+        return {'PASS_THROUGH'}
+
+    def set_lattice_interp(self, interp):
+        self.lat.interpolation_type_u = self.lat.interpolation_type_v = self.lat.interpolation_type_w = interp
+
+    def cancel(self, context):
+        context.window_manager.boxdeform_running = False
+        self.restore_prefs(context)
+        back_to_obj(self.gp_obj, self.gp_mode, self.org_lattice_toolset, context)
+        cancel_cage(self.gp_obj, self.cage)
+        context.area.header_text_set(None)     
+        if self.gp_mode != 'OBJECT':
+            bpy.ops.object.mode_set(mode=self.gp_mode)
+
+    def store_prefs(self, context):
+        # store_valierables <-< preferences
+        self.use_drag_immediately = context.preferences.inputs.use_drag_immediately 
+        self.drag_threshold_mouse = context.preferences.inputs.drag_threshold_mouse 
+        self.drag_threshold_tablet = context.preferences.inputs.drag_threshold_tablet
+        self.use_overlays = context.space_data.overlay.show_overlays
+        # maybe store in windows manager to keep around in case of modal revival ?
+
+    def restore_prefs(self, context):
+        # preferences <-< store_valierables
+        context.preferences.inputs.use_drag_immediately = self.use_drag_immediately
+        context.preferences.inputs.drag_threshold_mouse = self.drag_threshold_mouse
+        context.preferences.inputs.drag_threshold_tablet = self.drag_threshold_tablet
+        context.space_data.overlay.show_overlays = self.use_overlays
+    
+    def set_prefs(self, context):
+        context.preferences.inputs.use_drag_immediately = True
+        context.preferences.inputs.drag_threshold_mouse = 1
+        context.preferences.inputs.drag_threshold_tablet = 3
+        context.space_data.overlay.show_overlays = True
+
+    def invoke(self, context, event):
+        ## Restrict to 3D view
+        if context.area.type != 'VIEW_3D':
+            self.report({'WARNING'}, "View3D not found, cannot run operator")
+            return {'CANCELLED'}
+
+        if not context.object:#do it in poll ?
+            self.report({'ERROR'}, "No active objects found")
+            return {'CANCELLED'}
+
+        if context.window_manager.boxdeform_running:
+            return {'CANCELLED'}
+
+        self.prefs = get_addon_prefs()#get_prefs
+        self.auto_interp = self.prefs.auto_swap_deform_type
+        self.org_lattice_toolset = None
+        ## usability toggles
+        if self.prefs.use_clic_drag:#Store the active tool since we will change it
+            self.org_lattice_toolset = bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname# Tweaktoolcode    
+        
+        #store (scene properties needed in case of ctrlZ revival)
+        self.store_prefs(context)
+        self.gp_mode = 'EDIT_GPENCIL'
+
+        # --- special Case of lattice revive modal, just after ctrl+Z back into lattice with modal stopped
+        if context.mode == 'EDIT_LATTICE' and context.object.name == 'lattice_cage_deform' and len(context.object.vertex_groups):
+            self.gp_obj = context.scene.objects.get(context.object.vertex_groups[0].name)
+            if not self.gp_obj:
+                self.report({'ERROR'}, "/!\\ Box Deform : Cannot find object to target")
+                return {'CANCELLED'}
+            if not self.gp_obj.grease_pencil_modifiers.get('tmp_lattice'):
+                self.report({'ERROR'}, "/!\\ No 'tmp_lattice' modifiers on GP object")
+                return {'CANCELLED'}
+            self.cage = context.object
+            self.lat = self.cage.data
+            self.set_prefs(context)
+
+            if self.prefs.use_clic_drag:
+                bpy.ops.wm.tool_set_by_id(name="builtin.select")
+            context.window_manager.boxdeform_running = True
+            context.window_manager.modal_handler_add(self)
+            return {'RUNNING_MODAL'}
+
+        if context.object.type != 'GPENCIL':
+            # self.report({'ERROR'}, "Works only on gpencil objects")
+            ## silent return 
+            return {'CANCELLED'}
+
+        #paint need VG workaround. object need good shortcut
+        if context.mode not in ('EDIT_GPENCIL', 'OBJECT', 'PAINT_GPENCIL'):
+            # self.report({'WARNING'}, "Works only in following GPencil modes: edit")# ERROR
+            ## silent return 
+            return {'CANCELLED'}
+
+        # bpy.ops.ed.undo_push(message="Box deform step")#don't work as expected (+ might be obsolete)
+        # https://developer.blender.org/D6147 <- undo forget 
+
+        self.gp_obj = context.object
+        # Clean potential failed previous job (delete tmp lattice)
+        mod = self.gp_obj.grease_pencil_modifiers.get('tmp_lattice')
+        if mod:
+            print('Deleted remaining lattice modifiers')
+            self.gp_obj.grease_pencil_modifiers.remove(mod)
+
+        phantom_obj = context.scene.objects.get('lattice_cage_deform')
+        if phantom_obj:
+            print('Deleted remaining lattice object')
+            delete_cage(phantom_obj)
+
+        if [m for m in self.gp_obj.grease_pencil_modifiers if m.type == 'GP_LATTICE']:
+            self.report({'ERROR'}, "Grease pencil object already has a lattice modifier (can only have one)")
+            return {'CANCELLED'}
+        
+
+        self.gp_mode = context.mode#store mode for restore
+        
+        # All good, create lattice and start modal
+
+        # Create lattice (and switch to lattice edit) ----
+        self.cage = view_cage(self.gp_obj)
+        if isinstance(self.cage, str):#error, cage not created, display error
+            self.report({'ERROR'}, self.cage)
+            return {'CANCELLED'}
+        
+        self.lat = self.cage.data
+
+        self.set_prefs(context)
+        context.window_manager.boxdeform_running = True
+        context.window_manager.modal_handler_add(self)
+        return {'RUNNING_MODAL'}
+
+## --- KEYMAP
+
+addon_keymaps = []
+def register_keymaps():
+    addon = bpy.context.window_manager.keyconfigs.addon
+
+    km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY", region_type='WINDOW')
+    kmi = km.keymap_items.new("gp.latticedeform", type ='T', value = "PRESS", ctrl = True)
+    kmi.repeat = False
+    addon_keymaps.append(km)
+
+def unregister_keymaps():
+    for km in addon_keymaps:
+        for kmi in km.keymap_items:
+            km.keymap_items.remove(kmi)
+    addon_keymaps.clear()
+
+### --- REGISTER ---
+
+def register():
+    if bpy.app.background:
+        return
+    bpy.types.WindowManager.boxdeform_running = bpy.props.BoolProperty(default=False)
+    bpy.utils.register_class(GP_OT_latticeGpDeform)
+    register_keymaps()
+
+def unregister():
+    if bpy.app.background:
+        return
+    unregister_keymaps()
+    bpy.utils.unregister_class(GP_OT_latticeGpDeform)
+    wm = bpy.context.window_manager
+    p = 'boxdeform_running'
+    if p in wm:
+        del wm[p]
\ No newline at end of file
diff --git a/greasepencil_tools/icos/tex_01.jpg b/greasepencil_tools/icos/tex_01.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..e8f88b259c129be8e27fd169a94480c960847104
Binary files /dev/null and b/greasepencil_tools/icos/tex_01.jpg differ
diff --git a/greasepencil_tools/icos/tex_02.jpg b/greasepencil_tools/icos/tex_02.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..61ee77009c5f92f5213765846d1fc02aecf53f41
Binary files /dev/null and b/greasepencil_tools/icos/tex_02.jpg differ
diff --git a/greasepencil_tools/icos/tex_03.jpg b/greasepencil_tools/icos/tex_03.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..7a091d26b7d9bc7f4c739c58af88117cd1e431a1
Binary files /dev/null and b/greasepencil_tools/icos/tex_03.jpg differ
diff --git a/greasepencil_tools/icos/tex_04.jpg b/greasepencil_tools/icos/tex_04.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..880814e433a3e8af5ae2a9dc31a3cb4b0d068288
Binary files /dev/null and b/greasepencil_tools/icos/tex_04.jpg differ
diff --git a/greasepencil_tools/icos/tex_05.jpg b/greasepencil_tools/icos/tex_05.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..c7220c6e9afc898c32e8daa3c5ec4751c8ffbfea
Binary files /dev/null and b/greasepencil_tools/icos/tex_05.jpg differ
diff --git a/greasepencil_tools/icos/tex_06.jpg b/greasepencil_tools/icos/tex_06.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..23d37141992bc5a565c9980e1e4119842a064292
Binary files /dev/null and b/greasepencil_tools/icos/tex_06.jpg differ
diff --git a/greasepencil_tools/icos/tex_07.jpg b/greasepencil_tools/icos/tex_07.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..5e1b1cb3ec69e7d3700de7d064c331511d2bcb82
Binary files /dev/null and b/greasepencil_tools/icos/tex_07.jpg differ
diff --git a/greasepencil_tools/icos/tex_08.jpg b/greasepencil_tools/icos/tex_08.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..44376fddf4b396cb0be22436c188b985df06997c
Binary files /dev/null and b/greasepencil_tools/icos/tex_08.jpg differ
diff --git a/greasepencil_tools/icos/tex_09.jpg b/greasepencil_tools/icos/tex_09.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..45aff49030e3babb5a3d5225c85e22b576c9c72f
Binary files /dev/null and b/greasepencil_tools/icos/tex_09.jpg differ
diff --git a/greasepencil_tools/icos/tex_10.jpg b/greasepencil_tools/icos/tex_10.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..952d18d9ebc60401c7d282daf8e3bbf8b2ba89e3
Binary files /dev/null and b/greasepencil_tools/icos/tex_10.jpg differ
diff --git a/greasepencil_tools/icos/tex_11.jpg b/greasepencil_tools/icos/tex_11.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..032bcd0c75672ff8b89d0ce61646e4b644c54e99
Binary files /dev/null and b/greasepencil_tools/icos/tex_11.jpg differ
diff --git a/greasepencil_tools/icos/tex_12.jpg b/greasepencil_tools/icos/tex_12.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..9e07a1b59de2f9db911cf0d97946f5a2603aac07
Binary files /dev/null and b/greasepencil_tools/icos/tex_12.jpg differ
diff --git a/greasepencil_tools/icos/tex_13.jpg b/greasepencil_tools/icos/tex_13.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..05c89ca7e57d0acb16fc6ca4bd0732e223050223
Binary files /dev/null and b/greasepencil_tools/icos/tex_13.jpg differ
diff --git a/greasepencil_tools/icos/tex_14.jpg b/greasepencil_tools/icos/tex_14.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..8d36c5307ba7606e575ab86cd277b278859ca27c
Binary files /dev/null and b/greasepencil_tools/icos/tex_14.jpg differ
diff --git a/greasepencil_tools/icos/tex_15.jpg b/greasepencil_tools/icos/tex_15.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..b84732d20194a4e0c3ffa24f566e339f4359f70a
Binary files /dev/null and b/greasepencil_tools/icos/tex_15.jpg differ
diff --git a/greasepencil_tools/icos/tex_16.jpg b/greasepencil_tools/icos/tex_16.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..3ba1bc239b2dd565a358e675ed47dedba935b835
Binary files /dev/null and b/greasepencil_tools/icos/tex_16.jpg differ
diff --git a/greasepencil_tools/icos/tex_17.jpg b/greasepencil_tools/icos/tex_17.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..b9751c7cea9adcde15a813c2fb70ea7cd1f48b49
Binary files /dev/null and b/greasepencil_tools/icos/tex_17.jpg differ
diff --git a/greasepencil_tools/import_brush_pack.py b/greasepencil_tools/import_brush_pack.py
new file mode 100644
index 0000000000000000000000000000000000000000..fbeb25f14d7477dde65e99345593d22c443f230c
--- /dev/null
+++ b/greasepencil_tools/import_brush_pack.py
@@ -0,0 +1,40 @@
+import bpy
+
+class GP_OT_install_brush_pack(bpy.types.Operator):
+    bl_idname = "gp.import_brush_pack"
+    bl_label = "Import texture brush pack"
+    bl_description = "import Grease Pencil brush pack from Grease Pencil addon"
+    bl_options = {"REGISTER", "INTERNAL"}
+
+    def execute(self, context):
+        from pathlib import Path
+
+        blendname = 'Official_GP_brush_pack_V1.blend'        
+        blend_fp = Path(__file__).parent / blendname
+        print('blend_fp: ', blend_fp)
+
+        cur_brushes = [b.name for b in bpy.data.brushes]
+        with bpy.data.libraries.load(str(blend_fp), link=False) as (data_from, data_to):
+            # load brushes starting with 'tex' prefix if there are not already there
+            data_to.brushes = [b for b in data_from.brushes if b.startswith('tex_') and not b in cur_brushes]
+            # Add holdout
+            if 'z_holdout' in data_from.brushes:
+                data_to.brushes.append('z_holdout')
+
+        brush_count = len(data_to.brushes)
+        ## force fake user for the brushes
+        for b in data_to.brushes:
+            b.use_fake_user = True
+
+        if brush_count:
+            self.report({'INFO'}, f'{brush_count} brushes installed')
+        else:
+            self.report({'WARNING'}, 'Brushes already loaded')
+        return {"FINISHED"}
+
+
+def register():
+    bpy.utils.register_class(GP_OT_install_brush_pack)
+
+def unregister():
+    bpy.utils.unregister_class(GP_OT_install_brush_pack)
\ No newline at end of file
diff --git a/greasepencil_tools/line_reshape.py b/greasepencil_tools/line_reshape.py
new file mode 100644
index 0000000000000000000000000000000000000000..608b95b267ec2daf1668671e798086e5078f80d1
--- /dev/null
+++ b/greasepencil_tools/line_reshape.py
@@ -0,0 +1,192 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+ 
+'''Based on GP_refine_stroke 0.2.4 - Author: Samuel Bernou'''
+
+import bpy
+
+### --- Vector utils
+
+def mean(*args):
+    '''
+    return mean of all passed value (multiple)
+    If it's a list or tuple return mean of it (only on first list passed).
+    '''
+    if isinstance(args[0], list) or isinstance(args[0], tuple):
+        return mean(*args[0])#send the first list UNPACKED (else infinite recursion as it always evaluate as list)
+    return sum(args) / len(args)
+
+def vector_len_from_coord(a, b):
+    '''
+    Get two points (that has coordinate 'co' attribute) or Vectors (2D or 3D)
+    Return length as float
+    '''
+    from mathutils import Vector    
+    if type(a) is Vector:
+        return (a - b).length
+    else:   
+        return (a.co - b.co).length
+
+def point_from_dist_in_segment_3d(a, b, ratio):
+    '''return the tuple coords of a point on 3D segment ab according to given ratio (some distance divided by total segment lenght)'''
+    ## ref:https://math.stackexchange.com/questions/175896/finding-a-point-along-a-line-a-certain-distance-away-from-another-point
+    # ratio = dist / seglenght
+    return ( ((1 - ratio) * a[0] + (ratio*b[0])), ((1 - ratio) * a[1] + (ratio*b[1])), ((1 - ratio) * a[2] + (ratio*b[2])) )
+
+def get_stroke_length(s):
+    '''return 3D total length of the stroke'''
+    all_len = 0.0
+    for i in range(0, len(s.points)-1):
+        #print(vector_len_from_coord(s.points[i],s.points[i+1]))
+        all_len += vector_len_from_coord(s.points[i],s.points[i+1])   
+    return (all_len)
+
+### --- Functions
+
+def to_straight_line(s, keep_points=True, influence=100, straight_pressure=True):
+    '''
+    keep points : if false only start and end point stay
+    straight_pressure : (not available with keep point) take the mean pressure of all points and apply to stroke.
+    '''
+    
+    p_len = len(s.points)
+    if p_len <= 2: # 1 or 2 points only, cancel
+        return
+
+    if not keep_points:
+        if straight_pressure: mean_pressure = mean([p.pressure for p in s.points])#can use a foreach_get but might not be faster.
+        for i in range(p_len-2):
+            s.points.pop(index=1)
+        if straight_pressure:
+            for p in s.points:
+                p.pressure = mean_pressure
+
+    else:
+        A = s.points[0].co
+        B = s.points[-1].co
+        # ab_dist = vector_len_from_coord(A,B)
+        full_dist = get_stroke_length(s)
+        dist_from_start = 0.0
+        coord_list = []
+        
+        for i in range(1, p_len-1):#all but first and last
+            dist_from_start += vector_len_from_coord(s.points[i-1],s.points[i])
+            ratio = dist_from_start / full_dist
+            # dont apply directly (change line as we measure it in loop)
+            coord_list.append( point_from_dist_in_segment_3d(A, B, ratio) )
+        
+        # apply change
+        for i in range(1, p_len-1):
+            ## Direct super straight 100%
+            #s.points[i].co = coord_list[i-1]
+            
+            ## With influence
+            s.points[i].co = point_from_dist_in_segment_3d(s.points[i].co, coord_list[i-1], influence / 100)
+    
+    return
+
+def get_last_index(context=None):
+    if not context:
+        context = bpy.context
+    return 0 if context.tool_settings.use_gpencil_draw_onback else -1
+
+### --- OPS
+
+class GP_OT_straightStroke(bpy.types.Operator):
+    bl_idname = "gp.straight_stroke"
+    bl_label = "Straight Stroke"
+    bl_description = "Make stroke a straight line between first and last point, tweak influence in the redo panel\
+        \nshift+click to reset infuence to 100%"
+    bl_options = {"REGISTER", "UNDO"}
+
+    @classmethod
+    def poll(cls, context):
+        return context.active_object is not None and context.object.type == 'GPENCIL'
+        #and context.mode in ('PAINT_GPENCIL', 'EDIT_GPENCIL')
+
+    influence_val : bpy.props.FloatProperty(name="Straight force", description="Straight interpolation percentage", 
+    default=100, min=0, max=100, step=2, precision=1, subtype='PERCENTAGE', unit='NONE')
+    
+    def execute(self, context):
+        gp = context.object.data
+        gpl = gp.layers
+        if not gpl:
+            return {"CANCELLED"}
+
+        if context.mode == 'PAINT_GPENCIL':
+            if not gpl.active or not gpl.active.active_frame:
+                self.report({'ERROR'}, 'No Grease pencil frame found') 
+                return {"CANCELLED"}
+
+            if not len(gpl.active.active_frame.strokes):
+                self.report({'ERROR'}, 'No strokes found.') 
+                return {"CANCELLED"}
+
+            s = gpl.active.active_frame.strokes[get_last_index(context)]
+            to_straight_line(s, keep_points=True, influence=self.influence_val)
+        
+        elif context.mode == 'EDIT_GPENCIL':
+            ct = 0
+            for l in gpl:
+                if l.lock or l.hide or not l.active_frame:
+                    # avoid locked, hided, empty layers
+                    continue
+                if gp.use_multiedit:
+                    target_frames = [f for f in l.frames if f.select]
+                else:
+                    target_frames = [l.active_frame]
+                
+                for f in target_frames:
+                    for s in f.strokes:
+                        if s.select:
+                            ct += 1
+                            to_straight_line(s, keep_points=True, influence=self.influence_val)
+            
+            if not ct:
+                self.report({'ERROR'}, 'No selected stroke found.') 
+                return {"CANCELLED"}
+
+        ## filter method
+        # if context.mode == 'PAINT_GPENCIL':
+        #     L, F, S = 'ACTIVE', 'ACTIVE', 'LAST'
+        # elif context.mode == 'EDIT_GPENCIL'
+        #     L, F, S = 'ALL', 'ACTIVE', 'SELECT'
+        #     if gp.use_multiedit: F = 'SELECT'
+        # else : return {"CANCELLED"}
+        # for s in strokelist(t_layer=L, t_frame=F, t_stroke=S):
+        #     to_straight_line(s, keep_points=True, influence = self.influence_val)#, straight_pressure=True
+
+        return {"FINISHED"}
+    
+    def draw(self, context):
+        layout = self.layout
+        layout.prop(self, "influence_val")
+
+    def invoke(self, context, event):
+        if context.mode not in ('PAINT_GPENCIL', 'EDIT_GPENCIL'):
+            return {"CANCELLED"}
+        if event.shift:
+            self.influence_val = 100
+        return self.execute(context)
+
+
+def register():
+    bpy.utils.register_class(GP_OT_straightStroke)
+
+def unregister():
+    bpy.utils.unregister_class(GP_OT_straightStroke)
diff --git a/greasepencil_tools/prefs.py b/greasepencil_tools/prefs.py
new file mode 100644
index 0000000000000000000000000000000000000000..814341367b335f4a6be865b89017579f1ae219ab
--- /dev/null
+++ b/greasepencil_tools/prefs.py
@@ -0,0 +1,250 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+import bpy
+import os
+from bpy.props import (
+        BoolProperty,
+        EnumProperty,
+        StringProperty,
+        # IntProperty,
+        )
+
+from .ui_panels import GP_PT_sidebarPanel
+
+def get_addon_prefs():
+    import os
+    addon_name = os.path.splitext(__name__)[0]
+    addon_prefs = bpy.context.preferences.addons[addon_name].preferences
+    return (addon_prefs)
+
+## Addons Preferences Update Panel
+def update_panel(self, context):
+    try:
+        bpy.utils.unregister_class(GP_PT_sidebarPanel)
+    except:
+        pass
+    GP_PT_sidebarPanel.bl_category = get_addon_prefs().category
+    bpy.utils.register_class(GP_PT_sidebarPanel)
+
+## keymap binder for rotate canvas
+def auto_rebind(self, context):
+    unregister_keymaps()
+    register_keymaps()
+
+class GreasePencilAddonPrefs(bpy.types.AddonPreferences):
+    bl_idname = os.path.splitext(__name__)[0]#'greasepencil-addon' ... __package__ ?
+    # bl_idname = __name__
+
+    category : StringProperty(
+            name="Category",
+            description="Choose a name for the category of the panel",
+            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',
+        description="Change the active tool to 'tweak' during modal, Allow to direct clic-drag points of the box",
+        default=True)
+    
+    default_deform_type : EnumProperty(
+        items=(('KEY_LINEAR', "Linear (perspective mode)", "Linear interpolation, like corner deform / perspective tools of classic 2D", 'IPO_LINEAR',0),
+               ('KEY_BSPLINE', "Spline (smooth deform)", "Spline interpolation transformation\nBest when lattice is subdivided", 'IPO_CIRC',1),
+               ),
+        name='Starting interpolation', default='KEY_LINEAR', description='Choose default interpolation when entering mode')
+    
+    # About interpolation : https://docs.blender.org/manual/en/2.83/animation/shape_keys/shape_keys_panel.html#fig-interpolation-type
+
+    auto_swap_deform_type : BoolProperty(
+        name='Auto swap interpolation mode',
+        description="Automatically set interpolation to 'spline' when subdividing lattice\n Back to 'linear' when",
+        default=True)
+
+    ## rotate canvas variables
+
+    ## Use HUD
+    canvas_use_hud: BoolProperty(
+        name = "Use Hud",
+        description = "Display angle lines and angle value as text on viewport",
+        default = False)
+
+    ## Canvas rotate
+    canvas_use_shortcut: BoolProperty(
+        name = "Use Default Shortcut",
+        description = "Use default shortcut: mouse double-click + modifier",
+        default = True,
+        update=auto_rebind)
+
+    mouse_click : EnumProperty(
+        name="Mouse button", description="click on right/left/middle mouse button in combination with a modifier to trigger alignement",
+        default='MIDDLEMOUSE',
+        items=(
+            ('RIGHTMOUSE', 'Right click', 'Use click on Right mouse button', 'MOUSE_RMB', 0),
+            ('LEFTMOUSE', 'Left click', 'Use click on Left mouse button', 'MOUSE_LMB', 1),
+            ('MIDDLEMOUSE', 'Mid click', 'Use click on Mid mouse button', 'MOUSE_MMB', 2),
+            ),
+        update=auto_rebind)
+    
+    use_shift: BoolProperty(
+            name = "combine with shift",
+            description = "add shift",
+            default = False,
+            update=auto_rebind)
+
+    use_alt: BoolProperty(
+            name = "combine with alt",
+            description = "add alt",
+            default = True,
+            update=auto_rebind)
+
+    use_ctrl: BoolProperty(
+            name = "combine with ctrl",
+            description = "add ctrl",
+            default = True,
+            update=auto_rebind)
+
+    def draw(self, context):
+            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="")
+
+                ## 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')
+
+                ## ROTATE CANVAS
+                box = layout.box()
+                box.label(text='Rotate canvas:')
+
+                box.prop(self, "canvas_use_shortcut", text='Bind shortcuts')
+
+                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
+
+                    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
+
+                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':
+
+                #**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")
+
+                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()
+    if not pref.canvas_use_shortcut:
+        return
+    addon = bpy.context.window_manager.keyconfigs.addon
+
+    km = bpy.context.window_manager.keyconfigs.addon.keymaps.get("3D View")
+    if not km:
+        km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D")
+    
+    if 'view3d.rotate_canvas' not in km.keymap_items:
+        km = addon.keymaps.new(name='3D View', space_type='VIEW_3D')
+        kmi = km.keymap_items.new('view3d.rotate_canvas',
+        type=pref.mouse_click, value="PRESS", alt=pref.use_alt, ctrl=pref.use_ctrl, shift=pref.use_shift, any=False)
+
+        addon_keymaps.append(km)
+
+def unregister_keymaps():
+    for km in addon_keymaps:
+        for kmi in km.keymap_items:
+            km.keymap_items.remove(kmi)
+    addon_keymaps.clear()
+
+
+
+### REGISTER ---
+
+def register():
+    bpy.utils.register_class(GreasePencilAddonPrefs)
+    # Force box deform running to false
+    bpy.context.preferences.addons[os.path.splitext(__name__)[0]].preferences.boxdeform_running = False
+    register_keymaps()
+
+def unregister():
+    unregister_keymaps()
+    bpy.utils.unregister_class(GreasePencilAddonPrefs)
diff --git a/greasepencil_tools/rotate_canvas.py b/greasepencil_tools/rotate_canvas.py
new file mode 100644
index 0000000000000000000000000000000000000000..2ba5ed345b1d545004bd87ce11665e9680acafdb
--- /dev/null
+++ b/greasepencil_tools/rotate_canvas.py
@@ -0,0 +1,170 @@
+from .prefs import get_addon_prefs
+
+import bpy
+import math
+import mathutils
+from bpy_extras.view3d_utils import location_3d_to_region_2d
+from bpy.props import BoolProperty, EnumProperty
+## draw utils
+import gpu
+import bgl
+import blf
+from gpu_extras.batch import batch_for_shader
+from gpu_extras.presets import draw_circle_2d
+
+
+def draw_callback_px(self, context):
+    # 50% alpha, 2 pixel width line
+    shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
+    bgl.glEnable(bgl.GL_BLEND)
+    bgl.glLineWidth(2)
+
+    # init
+    batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": [self.center, self.initial_pos]})#self.vector_initial
+    shader.bind()
+    shader.uniform_float("color", (0.5, 0.5, 0.8, 0.6))
+    batch.draw(shader)
+
+    batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": [self.center, self.pos_current]})
+    shader.bind()
+    shader.uniform_float("color", (0.3, 0.7, 0.2, 0.5))
+    batch.draw(shader)
+
+    # restore opengl defaults
+    bgl.glLineWidth(1)
+    bgl.glDisable(bgl.GL_BLEND)
+
+    ## text
+    font_id = 0
+    ## draw text debug infos
+    blf.position(font_id, 15, 30, 0)
+    blf.size(font_id, 20, 72)
+    blf.draw(font_id, f'angle: {math.degrees(self.angle):.1f}')
+
+
+class RC_OT_RotateCanvas(bpy.types.Operator):
+    bl_idname = 'view3d.rotate_canvas'
+    bl_label = 'Rotate Canvas'
+    bl_options = {"REGISTER", "UNDO"}
+
+    def get_center_view(self, context, cam):
+        '''
+        https://blender.stackexchange.com/questions/6377/coordinates-of-corners-of-camera-view-border
+        Thanks to ideasman42
+        '''
+
+        frame = cam.data.view_frame()
+        mat = cam.matrix_world
+        frame = [mat @ v for v in frame]
+        frame_px = [location_3d_to_region_2d(context.region, context.space_data.region_3d, v) for v in frame]
+        center_x = frame_px[2].x + (frame_px[0].x - frame_px[2].x)/2
+        center_y = frame_px[1].y + (frame_px[0].y - frame_px[1].y)/2
+
+        return mathutils.Vector((center_x, center_y))
+
+    def execute(self, context):
+        if self.hud:
+            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
+            context.area.tag_redraw()
+        if self.in_cam:
+                self.cam.rotation_mode = self.org_rotation_mode
+        return {'FINISHED'}
+
+    def modal(self, context, event):
+        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
+            self.vector_current = (self.pos_current - self.center).normalized()
+            # 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)
+
+            if self.in_cam:
+                self.cam.matrix_world = self.cam_matrix
+                self.cam.rotation_euler.rotate_axis("Z", self.angle)
+            
+            else:#free view
+                context.space_data.region_3d.view_rotation = self._rotation
+                rot = context.space_data.region_3d.view_rotation
+                rot = rot.to_euler()
+                rot.rotate_axis("Z", self.angle)
+                context.space_data.region_3d.view_rotation = rot.to_quaternion()
+        
+        if event.type in {'RIGHTMOUSE', 'LEFTMOUSE', 'MIDDLEMOUSE'} and event.value == 'RELEASE':
+            if not self.angle:
+                # self.report({'INFO'}, 'Reset')
+                aim = context.space_data.region_3d.view_rotation @ mathutils.Vector((0.0, 0.0, 1.0))#view vector
+                context.space_data.region_3d.view_rotation = aim.to_track_quat('Z','Y')#track Z, up Y
+            self.execute(context)
+            return {'FINISHED'}
+        
+        if event.type == 'ESC':#Cancel
+            self.execute(context)
+            if self.in_cam:
+                self.cam.matrix_world = self.cam_matrix
+            else:
+                context.space_data.region_3d.view_rotation = self._rotation
+            return {'CANCELLED'}
+
+
+        return {'RUNNING_MODAL'}
+
+    def invoke(self, context, event):
+        self.hud = get_addon_prefs().canvas_use_hud
+        self.angle = 0.0
+        self.in_cam = context.region_data.view_perspective == 'CAMERA'
+
+        if self.in_cam:
+            # Get camera from scene
+            self.cam = bpy.context.scene.camera
+            
+            #return if one element is locked (else bypass location)
+            if self.cam.lock_rotation[:] != (False, False, False):
+                self.report({'WARNING'}, 'Camera rotation is locked') 
+                return {'CANCELLED'}
+
+            self.center = self.get_center_view(context, self.cam)
+            # store original rotation mode
+            self.org_rotation_mode = self.cam.rotation_mode
+            # set to euler to works with quaternions, restored at finish
+            self.cam.rotation_mode = 'XYZ'
+            # store camera matrix world
+            self.cam_matrix = self.cam.matrix_world.copy()
+            # self.cam_init_euler = self.cam.rotation_euler.copy()
+
+        else:
+            self.center = mathutils.Vector((context.area.width/2, context.area.height/2))
+            
+            # store current rotation
+            self._rotation = context.space_data.region_3d.view_rotation.copy()
+
+        # Get current mouse coordination
+        self.pos_current = mathutils.Vector((event.mouse_region_x, event.mouse_region_y))
+        
+        self.initial_pos = self.pos_current# for draw debug, else no need
+        # Calculate inital vector
+        self.vector_initial = self.pos_current - self.center
+        self.vector_initial.normalize()
+        
+        # Initializes the current vector with the same initial vector.
+        self.vector_current = self.vector_initial.copy()
+        
+        args = (self, context)
+        if self.hud:
+            self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
+        context.window_manager.modal_handler_add(self)
+        return {'RUNNING_MODAL'}
+
+
+### --- REGISTER
+
+def register():
+    bpy.utils.register_class(RC_OT_RotateCanvas)
+
+
+def unregister():
+    bpy.utils.unregister_class(RC_OT_RotateCanvas)
+
+# if __name__ == "__main__":
+#     register()
\ No newline at end of file
diff --git a/greasepencil_tools/ui_panels.py b/greasepencil_tools/ui_panels.py
new file mode 100644
index 0000000000000000000000000000000000000000..ecbc9a2458e3a5d9f221feb6b2ca78f010136d18
--- /dev/null
+++ b/greasepencil_tools/ui_panels.py
@@ -0,0 +1,83 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+import bpy
+
+class GP_PT_sidebarPanel(bpy.types.Panel):
+    bl_label = "Grease Pencil tools"
+    bl_space_type = "VIEW_3D"
+    bl_region_type = "UI"
+    bl_category = "Grease pencil"
+
+    def draw(self, context):
+        layout = self.layout
+        layout.use_property_split = True
+        
+        # Box deform ops
+        self.layout.operator_context = 'INVOKE_DEFAULT'
+        layout.operator('gp.latticedeform', icon ="MOD_MESHDEFORM")# MOD_LATTICE, LATTICE_DATA
+
+        # Straight line ops
+        layout.operator('gp.straight_stroke', icon ="CURVE_PATH")# IPO_LINEAR
+
+
+        # Expose Native view operators
+        # if context.scene.camera:
+        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')
+
+
+def menu_boxdeform_entry(self, context):
+    """Transform shortcut to append in existing menu"""
+    layout = self.layout
+    obj = bpy.context.object
+    # {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
+    if obj and obj.type == 'GPENCIL' and context.mode in {'OBJECT', 'EDIT_GPENCIL', 'PAINT_GPENCIL'}:
+        self.layout.operator_context = 'INVOKE_DEFAULT'
+        layout.operator('gp.latticedeform', text='Box Deform')
+
+def menu_stroke_entry(self, context):
+    layout = self.layout
+    # Gpencil modes : {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
+    if context.mode in {'EDIT_GPENCIL', 'PAINT_GPENCIL'}:
+        self.layout.operator_context = 'INVOKE_DEFAULT'
+        layout.operator('gp.straight_stroke', text='Straight Stroke')
+
+def menu_brush_pack(self, context):
+    layout = self.layout
+    # if context.mode in {'EDIT_GPENCIL', 'PAINT_GPENCIL'}:
+    self.layout.operator_context = 'INVOKE_DEFAULT'
+    layout.operator('gp.import_brush_pack')#, text='Import brush pack'
+
+
+def register():
+    bpy.utils.register_class(GP_PT_sidebarPanel)
+    ## VIEW3D_MT_edit_gpencil.append# Grease pencil menu
+    bpy.types.VIEW3D_MT_transform_object.append(menu_boxdeform_entry)
+    bpy.types.VIEW3D_MT_edit_gpencil_transform.append(menu_boxdeform_entry)
+    bpy.types.VIEW3D_MT_edit_gpencil_stroke.append(menu_stroke_entry)
+    bpy.types.VIEW3D_MT_brush_gpencil_context_menu.append(menu_brush_pack)
+
+
+def unregister():
+    bpy.types.VIEW3D_MT_brush_gpencil_context_menu.remove(menu_brush_pack)
+    bpy.types.VIEW3D_MT_transform_object.remove(menu_boxdeform_entry)
+    bpy.types.VIEW3D_MT_edit_gpencil_transform.remove(menu_boxdeform_entry)
+    bpy.types.VIEW3D_MT_edit_gpencil_stroke.remove(menu_stroke_entry)
+    bpy.utils.unregister_class(GP_PT_sidebarPanel)