346 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			346 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
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) |