gp_toolbox/OP_material_picker.py

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)