diff options
-rw-r--r-- | greasepencil-addon/__init__.py | 54 | ||||
-rw-r--r-- | greasepencil-addon/box_deform.py | 591 | ||||
-rw-r--r-- | greasepencil-addon/line_reshape.py | 192 | ||||
-rw-r--r-- | greasepencil-addon/prefs.py | 112 | ||||
-rw-r--r-- | greasepencil-addon/ui_panels.py | 74 |
5 files changed, 1023 insertions, 0 deletions
diff --git a/greasepencil-addon/__init__.py b/greasepencil-addon/__init__.py new file mode 100644 index 00000000..b6e759fa --- /dev/null +++ b/greasepencil-addon/__init__.py @@ -0,0 +1,54 @@ +# ##### 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", +"version": (0, 0, 5), +"blender": (2, 83, 0), +"location": "sidebar (N) > Grease pencil > Grease pencil", +"warning": "", +"doc_url": "https://github.com/Pullusb/greasepencil-addon", +"tracker_url": "https://github.com/Pullusb/greasepencil-addon/issues", +"category": "Object", +# "support": "COMMUNITY", +} + + +from . import (prefs, + box_deform, + line_reshape, + ui_panels, + ) + +def register(): + prefs.register() + box_deform.register() + line_reshape.register() + ui_panels.register() + +def unregister(): + ui_panels.unregister() + box_deform.unregister() + line_reshape.unregister() + prefs.unregister() + +if __name__ == "__main__": + register()
\ No newline at end of file diff --git a/greasepencil-addon/box_deform.py b/greasepencil-addon/box_deform.py new file mode 100644 index 00000000..8996ae70 --- /dev/null +++ b/greasepencil-addon/box_deform.py @@ -0,0 +1,591 @@ +# ##### 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''' + +import bpy +import numpy as np + +def get_addon_prefs(): + import os + addon_name = os.path.splitext(__name__)[0] + preferences = bpy.context.preferences + addon_prefs = preferences.addons[addon_name].preferences + return (addon_prefs) + + +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/Tab, cancel:Del/Backspace/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-addon/line_reshape.py b/greasepencil-addon/line_reshape.py new file mode 100644 index 00000000..608b95b2 --- /dev/null +++ b/greasepencil-addon/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-addon/prefs.py b/greasepencil-addon/prefs.py new file mode 100644 index 00000000..c3861de3 --- /dev/null +++ b/greasepencil-addon/prefs.py @@ -0,0 +1,112 @@ +# ##### 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, + # IntProperty, + ) + + +class GreasePencilAddonPrefs(bpy.types.AddonPreferences): + bl_idname = os.path.splitext(__name__)[0]#'greasepencil-addon'#can be called 'master' + # bl_idname = __name__ + + 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) + + 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': + layout.label(text='Box deform tool preferences') + layout.prop(self, "use_clic_drag") + # layout.separator() + layout.prop(self, "default_deform_type") + layout.label(text="Deformer type can be changed during modal with 'M' key, this is for default behavior", icon='INFO') + + layout.prop(self, "auto_swap_deform_type") + layout.label(text="Once 'M' is hit, auto swap is desactivated to stay in your chosen mode", icon='INFO') + + 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") + + + +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 + +def unregister(): + bpy.utils.unregister_class(GreasePencilAddonPrefs) diff --git a/greasepencil-addon/ui_panels.py b/greasepencil-addon/ui_panels.py new file mode 100644 index 00000000..f31cd5c3 --- /dev/null +++ b/greasepencil-addon/ui_panels.py @@ -0,0 +1,74 @@ +# ##### 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 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) + + +def unregister(): + 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) |