diff options
Diffstat (limited to 'greasepencil-addon/box_deform.py')
-rw-r--r-- | greasepencil-addon/box_deform.py | 591 |
1 files changed, 591 insertions, 0 deletions
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 |