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)