336 lines
12 KiB
Python
336 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
|
|
|
|
### passing by 2D projection
|
|
def get_3d_coord_on_drawing_plane_from_2d(context, co):
|
|
plane_co, plane_no = get_gp_draw_plane(context)
|
|
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 == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
|
|
|
|
fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
|
|
|
|
def filter_stroke(self, context):
|
|
# get stroke under mouse using kdtree
|
|
point_pair = [(p.co, 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.co == 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 self.gp.use_multiedit:
|
|
for l in self.gp.layers:
|
|
if l.hide:# l.lock or
|
|
continue
|
|
for f in l.frames:
|
|
if not f.select:
|
|
continue
|
|
for s in f.strokes:
|
|
self.stroke_list.append(s)
|
|
|
|
else:
|
|
# [s for l in self.gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
|
|
for l in self.gp.layers:
|
|
if l.hide or not l.active_frame:# l.lock or
|
|
continue
|
|
for s in l.active_frame.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].co
|
|
self.init_pos = [p.co.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].co = 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 == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
|
|
|
|
# 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.co, 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.co == 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 gp.use_multiedit:
|
|
for l in gp.layers:
|
|
if l.hide:# l.lock or
|
|
continue
|
|
for f in l.frames:
|
|
if not f.select:
|
|
continue
|
|
for s in f.strokes:
|
|
self.stroke_list.append(s)
|
|
|
|
else:
|
|
# [s for l in gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
|
|
for l in gp.layers:
|
|
if l.hide or not l.active_frame:# l.lock or
|
|
continue
|
|
for s in l.active_frame.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].co
|
|
# self.init_pos = [p.co.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 Stroke Paint (Draw brush)", space_type = "EMPTY", region_type='WINDOW')
|
|
# km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY", region_type='WINDOW')
|
|
km = addon.keymaps.new(name = "Grease Pencil Stroke Paint (Fill)", 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():
|
|
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) |