From 56cbc04c65bd8733e799b061a8edf95409e7d5e4 Mon Sep 17 00:00:00 2001 From: Pullusb Date: Tue, 26 Oct 2021 02:24:19 +0200 Subject: [PATCH] paint mode precise layer picker on W 1.7.4 - added: Pick layer from closest stroke in paint mode using quick press on `W` for stroke (and `alt+W` for fills) --- CHANGELOG.md | 6 +- OP_copy_paste.py | 6 ++ OP_stroke_picker.py | 161 ++++++++++++++++++++++++++++++++++++++++++++ __init__.py | 7 +- 4 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 OP_stroke_picker.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e1bdc..ed166da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +1.7.4 + +- added: Pick layer from closest stroke in paint mode using quick press on `W` for stroke (and `alt+W` for fills) + 1.7.3 - added: show/hide gp modifiers if they are showed in render (in subpanel `Animation Manager`) @@ -30,7 +34,7 @@ 1.6.9 -- change: material picker (`S` and `Alt+S`) quick trigger, cahnge is only triggered if ley is pressed less than 200ms +- change: material picker (`S` and `Alt+S`) quick trigger, change is only triggered if key is pressed less than 200ms - this is made to let other operator use functionality on long press using `S` - feat: material picker shortcut has now an enum choice to filter targeted stroke (fill, stroke, all) - by default `S` is still fill only diff --git a/OP_copy_paste.py b/OP_copy_paste.py index 654c3ab..3e5ed6a 100644 --- a/OP_copy_paste.py +++ b/OP_copy_paste.py @@ -715,6 +715,9 @@ GPCLIP_PT_clipboard_ui, ) def register(): + if bpy.app.background: + return + for cl in classes: bpy.utils.register_class(cl) @@ -722,6 +725,9 @@ def register(): register_keymaps() def unregister(): + if bpy.app.background: + return + unregister_keymaps() for cl in reversed(classes): bpy.utils.unregister_class(cl) diff --git a/OP_stroke_picker.py b/OP_stroke_picker.py new file mode 100644 index 0000000..81292e2 --- /dev/null +++ b/OP_stroke_picker.py @@ -0,0 +1,161 @@ +import bpy +from bpy.types import Operator +import mathutils +from mathutils import Vector, Matrix, geometry +from bpy_extras import view3d_utils +from time import time +from .utils import get_gp_draw_plane, location_to_region, region_to_location + +class GP_OT_pick_closest_layer(Operator): + bl_idname = "gp.pick_closest_layer" + bl_label = "Active Closest Stroke Layer" + bl_description = "Pick closest stroke layer" + bl_options = {"REGISTER"} # , "UNDO" + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'GPENCIL' and context.mode == 'PAINT_GPENCIL' + + stroke_filter : bpy.props.EnumProperty(default='STROKE', + items=( + ('STROKE', 'Stroke', 'Target only Stroke', 0), + ('FILL', 'Fill', 'Target only Fill', 1), + ('ALL', 'All', 'All stroke types', 2), + ), + options={'SKIP_SAVE'}) + + def filter_stroke(self, context): + kd = mathutils.kdtree.KDTree(len(self.point_pair)) + for i, pair in enumerate(self.point_pair): + kd.insert(pair[0], i) + kd.balance() + + mouse_vec3 = Vector((*self.init_mouse, 0)) + co, index, _dist = kd.find(mouse_vec3) + lid = self.point_pair[index][1] + return lid + + def invoke(self, context, event): + self.t0 = time() + self.limit = self.t0 + 0.2 # 200 miliseconds + self.init_mouse = Vector((event.mouse_region_x, event.mouse_region_y)) + self.idx = None + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + + def modal(self, context, event): + if time() > self.limit: + return {'CANCELLED'} + + if event.value == 'RELEASE': # if a key was release (any key in case shortcut was customised) + if time() > self.limit: + # dont know if condition is neeed + return {'CANCELLED'} + + return self.execute(context) + # return {'FINISHED'} + + return {'PASS_THROUGH'} + # return {'RUNNING_MODAL'} + + def execute(self, context): + t1 = time() + # self.prefs = get_addon_prefs() + self.ob = context.object + mat = self.ob.matrix_world + gp = self.ob.data + + self.inv_mat = self.ob.matrix_world.inverted() + self.point_pair = [] + if gp.use_multiedit: + for layer_id, l in enumerate(gp.layers): + if l.hide:# l.lock or + continue + for f in l.frames: + if not f.select: + continue + for s in f.strokes: + if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke: + continue + elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill: + continue + self.point_pair += [(Vector((*location_to_region(mat @ p.co), 0)), layer_id) for p in s.points] + + else: + # [s for l in gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes] + for layer_id, l in enumerate(gp.layers): + if l.hide or not l.active_frame:# l.lock or + continue + for s in l.active_frame.strokes: + if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke: + continue + elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill: + continue + self.point_pair += [(Vector((*location_to_region(mat @ p.co), 0)), layer_id) for p in s.points] + + if not self.point_pair: + self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden') + return {'CANCELLED'} + + + lid = self.filter_stroke(context) + if isinstance(lid, str): + self.report({'ERROR'}, lid) + return {'CANCELLED'} + + del self.point_pair # auto garbage collected ? + + self.ob.data.layers.active_index = lid + + ## debug show trigger time + # print(f'Trigger time {time() - self.t0:.3f}') + # print(f'Search time {time() - t1:.3f}') + self.report({'INFO'}, f'Layer: {self.ob.data.layers.active.info}') + return {'FINISHED'} + + + +addon_keymaps = [] +def register_keymaps(): + addon = bpy.context.window_manager.keyconfigs.addon + km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY", region_type='WINDOW') + + kmi = km.keymap_items.new( + # name="", + idname="gp.pick_closest_layer", + type="W", + value="PRESS", + ) + kmi.properties.stroke_filter = 'STROKE' + addon_keymaps.append((km, kmi)) + + kmi = km.keymap_items.new( + # name="", + idname="gp.pick_closest_layer", + type="W", + value="PRESS", + alt = True, + ) + kmi.properties.stroke_filter = 'FILL' + # kmi = km.keymap_items.new('catname.opsname', type='F5', value='PRESS') + addon_keymaps.append((km, kmi)) + +def unregister_keymaps(): + for km, kmi in addon_keymaps: + km.keymap_items.remove(kmi) + addon_keymaps.clear() + + +classes=( +GP_OT_pick_closest_layer, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + register_keymaps() + +def unregister(): + unregister_keymaps() + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/__init__.py b/__init__.py index de10abc..f7cbc14 100755 --- a/__init__.py +++ b/__init__.py @@ -15,7 +15,7 @@ bl_info = { "name": "GP toolbox", "description": "Set of tools for Grease Pencil in animation production", "author": "Samuel Bernou, Christophe Seux", -"version": (1, 7, 3), +"version": (1, 7, 4), "blender": (2, 91, 0), "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "warning": "", @@ -48,6 +48,7 @@ from . import OP_realign from . import OP_depth_move from . import OP_key_duplicate_send from . import OP_layer_manager +from . import OP_stroke_picker from . import OP_material_picker from . import OP_eraser_brush from . import TOOL_eraser_brush @@ -579,6 +580,7 @@ def register(): OP_layer_manager.register() OP_eraser_brush.register() OP_material_picker.register() + OP_stroke_picker.register() TOOL_eraser_brush.register() handler_draw_cam.register() UI_tools.register() @@ -604,9 +606,10 @@ def unregister(): bpy.utils.unregister_class(cls) UI_tools.unregister() handler_draw_cam.unregister() + TOOL_eraser_brush.unregister() + OP_stroke_picker.unregister() OP_material_picker.unregister() OP_eraser_brush.unregister() - TOOL_eraser_brush.unregister() OP_layer_manager.unregister() OP_key_duplicate_send.unregister() OP_depth_move.unregister()