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, is_locked, is_hidden) ### passing by 2D projection def get_3d_coord_on_drawing_plane_from_2d(context, co): plane_co, plane_no = get_gp_draw_plane() rv3d = context.region_data view_mat = rv3d.view_matrix.inverted() if not plane_no: plane_no = Vector((0,0,1)) plane_no.rotate(view_mat) depth_3d = view_mat @ Vector((0, 0, -1000)) org = region_to_location(co, view_mat.to_translation()) view_point = region_to_location(co, depth_3d) hit = geometry.intersect_line_plane(org, view_point, plane_co, plane_no) if hit and plane_no: return context.object, hit, plane_no return None, None, None """ class GP_OT_pick_closest_material(Operator): bl_idname = "gp.pick_closest_material" bl_label = "Get Closest Stroke Material" bl_description = "Pick closest stroke material" bl_options = {"REGISTER"} # , "UNDO" @classmethod def poll(cls, context): return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL' fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'}) def filter_stroke(self, context): # get stroke under mouse using kdtree point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space kd = mathutils.kdtree.KDTree(len(point_pair)) for i, pair in enumerate(point_pair): kd.insert(pair[0], i) kd.balance() ## Get 3D coordinate on drawing plane according to mouse 2d.position on flat 2d drawing _ob, hit, _plane_no = get_3d_coord_on_drawing_plane_from_2d(context, self.init_mouse) if not hit: return 'No hit on drawing plane', None mouse_3d = hit mouse_local = self.inv_mat @ mouse_3d # local space co, index, _dist = kd.find(mouse_local) # local space # co, index, _dist = kd.find(mouse_3d) # world space # context.scene.cursor.location = co # world space s = point_pair[index][1] ## find point index in stroke self.idx = None for i, p in enumerate(s.points): if p.position == co: self.idx = i break del point_pair return s, self.ob.matrix_world @ co def invoke(self, context, event): # self.prefs = get_addon_prefs() self.ob = context.object self.gp = self.ob.data self.stroke_list = [] self.inv_mat = self.ob.matrix_world.inverted() if context.scene.tool_settings.use_grease_pencil_multi_frame_editing: for l in self.gp.layers: if is_hidden(l):# is_locked(l) or continue for f in l.frames: if not f.select: continue for s in f.drawing.strokes: self.stroke_list.append(s) else: # [s for l in self.gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes] for l in self.gp.layers: if is_hidden(l) or not l.current_frame():# is_locked(l) or continue for s in l.current_frame().drawing.strokes: self.stroke_list.append(s) if self.fill_only: self.stroke_list = [s for s in self.stroke_list if self.ob.data.materials[s.material_index].grease_pencil.show_fill] if not self.stroke_list: self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden') return {'CANCELLED'} self.init_mouse = Vector((event.mouse_region_x, event.mouse_region_y)) self.stroke, self.coord = self.filter_stroke(context) if isinstance(self.stroke, str): self.report({'ERROR'}, self.stroke) return {'CANCELLED'} del self.stroke_list if self.idx is None: self.report({'WARNING'}, 'No coord found') return {'CANCELLED'} self.depth = self.ob.matrix_world @ self.stroke.points[self.idx].position self.init_pos = [p.position.copy() for p in self.stroke.points] # need a copy otherwise vector is updated ## directly use world position ? # self.pos_world = [self.ob.matrix_world @ co for co in self.init_pos] self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos] self.plen = len(self.stroke.points) # context.scene.cursor.location = self.coord #Dbg return self.execute(context) # context.window_manager.modal_handler_add(self) # return {'RUNNING_MODAL'} def execute(self, context): self.ob.active_material_index = self.stroke.material_index # self.report({'INFO'}, f'Mat: {self.ob.data.materials[self.stroke.material_index].name}') return {'FINISHED'} # def modal(self, context, event): # if event.type == 'MOUSEMOVE': # mouse = Vector((event.mouse_region_x, event.mouse_region_y)) # delta = mouse - self.init_mouse # if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': # print(f'{self.stroke}, points num {len(self.stroke.points)}, material index:{self.stroke.material_index}') # return {'FINISHED'} # if event.type in {'RIGHTMOUSE', 'ESC'}: # # for i, p in enumerate(self.stroke.points): # reset position # # self.stroke.points[i].position = self.init_pos[i] # context.area.tag_redraw() # return {'CANCELLED'} # return {'RUNNING_MODAL'} """ class GP_OT_pick_closest_material(Operator): bl_idname = "gp.pick_closest_material" bl_label = "Get Closest Stroke Material" bl_description = "Pick closest stroke material" bl_options = {"REGISTER"} # , "UNDO" @classmethod def poll(cls, context): return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL' # fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'}) stroke_filter : bpy.props.EnumProperty(default='FILL', items=( ('FILL', 'Fill', 'Target only Fill materials', 0), ('STROKE', 'Stroke', 'Target only Stroke materials', 1), ('ALL', 'All', 'All material', 2), ), options={'SKIP_SAVE'}) def filter_stroke(self, context): # get stroke under mouse using kdtree point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space kd = mathutils.kdtree.KDTree(len(point_pair)) for i, pair in enumerate(point_pair): kd.insert(pair[0], i) kd.balance() ## Get 3D coordinate on drawing plane according to mouse 2d.co on flat 2d drawing _ob, hit, _plane_no = get_3d_coord_on_drawing_plane_from_2d(context, self.init_mouse) if not hit: return 'No hit on drawing plane', None mouse_3d = hit mouse_local = self.inv_mat @ mouse_3d # local space co, index, _dist = kd.find(mouse_local) # local space # co, index, _dist = kd.find(mouse_3d) # world space # context.scene.cursor.location = co # world space s = point_pair[index][1] ## find point index in stroke self.idx = None for i, p in enumerate(s.points): if p.position == co: self.idx = i break del point_pair return s, self.ob.matrix_world @ co 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): # self.prefs = get_addon_prefs() self.ob = context.object gp = self.ob.data self.stroke_list = [] self.inv_mat = self.ob.matrix_world.inverted() if context.scene.tool_settings.use_grease_pencil_multi_frame_editing: for l in gp.layers: if is_hidden(l):# is_locked(l) or continue for f in l.frames: if not f.select: continue for s in f.drawing.strokes: self.stroke_list.append(s) else: # [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes] for l in gp.layers: if is_hidden(l) or not l.current_frame():# is_locked(l) or continue for s in l.current_frame().drawing.strokes: self.stroke_list.append(s) if self.stroke_filter == 'FILL': self.stroke_list = [s for s in self.stroke_list if self.ob.data.materials[s.material_index].grease_pencil.show_fill] elif self.stroke_filter == 'STROKE': self.stroke_list = [s for s in self.stroke_list if self.ob.data.materials[s.material_index].grease_pencil.show_stroke] # else ALL (no filter) if not self.stroke_list: self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden') return {'CANCELLED'} stroke, self.coord = self.filter_stroke(context) if isinstance(stroke, str): self.report({'ERROR'}, stroke) return {'CANCELLED'} del self.stroke_list if self.idx is None: self.report({'WARNING'}, 'No coord found') return {'CANCELLED'} # self.depth = self.ob.matrix_world @ stroke.points[self.idx].position # self.init_pos = [p.position.copy() for p in stroke.points] # need a copy otherwise vector is updated # self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos] # self.plen = len(stroke.points) self.ob.active_material_index = stroke.material_index ## debug show trigger time # print(f'Trigger time {time() - self.t0:.3f}') self.report({'INFO'}, f'Mat: {self.ob.data.materials[stroke.material_index].name}') return {'FINISHED'} addon_keymaps = [] def register_keymaps(): addon = bpy.context.window_manager.keyconfigs.addon # km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW') km = addon.keymaps.new(name = "Grease Pencil Fill Tool", space_type = "EMPTY", region_type='WINDOW') kmi = km.keymap_items.new( # name="", idname="gp.pick_closest_material", type="S", # type="LEFTMOUSE", value="PRESS", # key_modifier='S', # S like Sample ) kmi.properties.stroke_filter = 'FILL' addon_keymaps.append((km, kmi)) kmi = km.keymap_items.new( # name="", idname="gp.pick_closest_material", type="S", # type="LEFTMOUSE", value="PRESS", alt = True, # key_modifier='S', # S like Sample ) kmi.properties.stroke_filter = 'STROKE' # 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_material, ) def register(): if bpy.app.background: return for cls in classes: bpy.utils.register_class(cls) register_keymaps() def unregister(): if bpy.app.background: return unregister_keymaps() for cls in reversed(classes): bpy.utils.unregister_class(cls)