layer group - named regex - fill tool mat picker

1.6.0

- feat: Namespace upgrade
  - support pseudo group naming
  - add group and indent button
- feat: Fill tool shortcut for material color picker (from closest stroke)
  - `S` : get material closest *fill* stroke
  - `Àlt+S` : get closest stroke (fill or stroke)
gpv2
Pullusb 2021-07-27 18:48:38 +02:00
parent 7ff96ad205
commit 3974a15ff0
4 changed files with 353 additions and 16 deletions

View File

@ -1,5 +1,14 @@
# Changelog
1.6.0
- feat: Namespace upgrade
- support pseudo group naming
- add group and indent button
- feat: Fill tool shortcut for material color picker (from closest stroke)
- `S` : get material closest *fill* stroke
- `Àlt+S` : get closest stroke (fill or stroke)
1.5.8
- feat: Namespace improvement:

View File

@ -9,24 +9,76 @@ from .utils import get_addon_prefs
# --- OPS ---
# pattern = r'([A-Z]{2})?_?([A-Z]{2})?_?(.*)' # bad ! match whithout separator
# PATTERN = r'([A-Z]{2})?_?([A-Z]{2})?_?(.*)' # bad ! match whithout separator
# pattern = r'(?:(^[A-Z]{2})_)?(?:([A-Z]{2})_)?(.*)' # matching only two letter
# pattern = r'^([A-Z]{2}_)?([A-Z]{2}_)?(.*)' # matching letters with separator
# pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*)' # matching capital letters from one to six
pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*?)(_[A-Z]{2})?$' # 2 letter suffix
# pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*?)(_[A-Z]{2})?$' # 2 letter suffix
# pattern = r'^(?P<tag>[A-Z]{1,6}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?$' # named
# pattern = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?$' # group start ' - '
# PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
def layer_name_build(layer, prefix='', desc='', suffix=''):
'''GET a layer and argumen to build and assign name'''
prefs = get_addon_prefs()
sep = prefs.separator
name = layer.info
pattern = PATTERN.replace('_', sep) # set separator
res = re.search(pattern, name.strip())
# prefix -> tag
# prefix2 -> tag2
# desc -> name
# suffix -> sfix
grp = '' if res.group('grp') is None else res.group('grp')
tag = '' if res.group('tag') is None else res.group('tag')
# tag2 = '' if res.group('tag2') is None else res.group('tag2')
name = '' if res.group('name') is None else res.group('name')
sfix = '' if res.group('sfix') is None else res.group('sfix')
inc = '' if res.group('inc') is None else res.group('inc')
if grp:
grp = ' ' + grp # name is strip(), so grp first spaces are gones.
if prefix:
if prefix == 'prefixkillcode':
tag = ''
else:
tag = prefix.upper().strip() + sep
# if prefix2:
# tag2 = prefix2.upper().strip() + sep
if desc:
name = desc
if suffix:
if suffix == 'suffixkillcode':
sfix = ''
else:
sfix = sep + suffix.upper().strip()
# check if name is available without the increment ending
new = f'{grp}{tag}{name}{sfix}'
layer.info = new
"""
def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
'''GET a layer and infos to build name
Can take one or two prefix and description/name of the layer)
'''
global pattern
prefs = get_addon_prefs()
sep = prefs.separator
name = layer.info
pattern = pattern.replace('_', sep) # set separator
res = re.search(pattern, name.strip())
@ -55,6 +107,7 @@ def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
new = f'{p1}{p2}{p3}{p4}'
layer.info = new
"""
## multi-prefix solution (Caps letters)
class GPTB_OT_layer_name_build(Operator):
@ -68,7 +121,7 @@ class GPTB_OT_layer_name_build(Operator):
return True
prefix : StringProperty(default='', options={'SKIP_SAVE'})
prefix2 : StringProperty(default='', options={'SKIP_SAVE'})
# prefix2 : StringProperty(default='', options={'SKIP_SAVE'})
desc : StringProperty(default='', options={'SKIP_SAVE'})
suffix : StringProperty(default='', options={'SKIP_SAVE'})
@ -82,7 +135,69 @@ class GPTB_OT_layer_name_build(Operator):
for l in gpl:
if l.select or l == act:
layer_name_build(l, prefix=self.prefix, prefix2=self.prefix2, desc=self.desc, suffix=self.suffix)
layer_name_build(l, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
return {"FINISHED"}
def grp_toggle(l):
grp_item_id = ' - '
res = re.search(r'^(\s{1,3}-\s{0,3})(.*)', l.info)
if not res:
# add group prefix after stripping all space and dash
l.info = grp_item_id + l.info.lstrip(' -')
else:
# delete group prefix
l.info = res.group(2)
class GPTB_OT_layer_group_toggle(Operator):
bl_idname = "gp.layer_group_toggle"
bl_label = "Group Toggle"
bl_description = "Group or ungroup a layer"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return True
# group : StringProperty(default='', options={'SKIP_SAVE'})
def execute(self, context):
ob = context.object
gpl = ob.data.layers
act = gpl.active
if not act:
self.report({'ERROR'}, 'no layer active')
return {"CANCELLED"}
for l in gpl:
if l.select or l == act:
grp_toggle(l)
return {"FINISHED"}
class GPTB_OT_layer_new_group(Operator):
bl_idname = "gp.layer_new_group"
bl_label = "New Group"
bl_description = "Create a group from active layer"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
ob = context.object
gpl = ob.data.layers
act = gpl.active
if not act:
self.report({'ERROR'}, 'no layer active')
return {"CANCELLED"}
res = re.search(PATTERN, act.info)
if not res:
self.report({'ERROR'}, 'Could not create a group name, create a layer manually')
return {"CANCELLED"}
name = res.group('name')
if not name:
self.report({'ERROR'}, f'No name found in {act.info}')
return {"CANCELLED"}
if name in [l.info for l in gpl]:
self.report({'WARNING'}, f'Name already exists: {act.info}')
return {"FINISHED"}
gpl.new(name, set_active=False)
return {"FINISHED"}
@ -120,13 +235,12 @@ class GPTB_OT_select_set_same_prefix(Operator):
return self.execute(context)
def execute(self, context):
global pattern
prefs = get_addon_prefs()
sep = prefs.separator # '_'
gpl = context.object.data.layers
act = gpl.active
res = re.search(pattern, act.info)
res = re.search(PATTERN, act.info)
if not res:
# self.report({'ERROR'}, f'Error scanning {act.info}')
return {"CANCELLED"}
@ -288,7 +402,7 @@ class GPTB_OT_rename_gp_layer(Operator):
## --- UI layer panel---
def layer_name_builder(self, context):
def layer_name_builder_ui(self, context):
'''appended to DATA_PT_gpencil_layers'''
prefs = get_addon_prefs()
@ -319,6 +433,8 @@ def layer_name_builder(self, context):
## name (description)
row = col.row(align=True)
row.prop(context.scene.gptoolprops, 'layer_name', text='')
row.operator("gp.layer_new_group", text='', icon='COLLECTION_NEW')
row.operator("gp.layer_group_toggle", text='', icon='OUTLINER_OB_GROUP_INSTANCE')
## no need for desc ops, already trigerred from update
# row.operator("gp.layer_name_build", text='', icon='EVENT_RETURN').desc = context.scene.gptoolprops.layer_name
@ -328,6 +444,8 @@ def layer_name_builder(self, context):
row = col.row(align=True)
row.operator("gp.layer_name_build", text=suffix.upper() ).suffix = suffix
row.operator("gp.layer_name_build", text='', icon='X').suffix = 'suffixkillcode'
## --- UI dopesheet ---
@ -360,13 +478,16 @@ def obj_layer_name_callback():
return
if not ob.data.layers.active:
return
pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*?)(_[A-Z]{2})?$'
res = re.search(pattern, ob.data.layers.active.info)
for l in ob.data.layers:
l.select = l == ob.data.layers.active
res = re.search(PATTERN, ob.data.layers.active.info)
if not res:
return
if not res.group(3):
if not res.group('name'):
return
bpy.context.scene.gptoolprops['layer_name'] = res.group(3)
bpy.context.scene.gptoolprops['layer_name'] = res.group('name')
@persistent
def subscribe_handler(dummy):
@ -387,6 +508,8 @@ def subscribe_handler(dummy):
classes=(
GPTB_OT_rename_gp_layer,
GPTB_OT_layer_name_build,
GPTB_OT_layer_group_toggle,
GPTB_OT_layer_new_group,
GPTB_OT_select_set_same_prefix,
GPTB_OT_select_set_same_color,
)
@ -395,7 +518,7 @@ def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.DATA_PT_gpencil_layers.prepend(layer_name_builder)
bpy.types.DATA_PT_gpencil_layers.prepend(layer_name_builder_ui)
bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header)
bpy.types.GPENCIL_MT_layer_context_menu.append(gpencil_layer_dropdown_menu)
bpy.app.handlers.load_post.append(subscribe_handler) # need to restart after first activation
@ -404,7 +527,7 @@ def unregister():
bpy.app.handlers.load_post.remove(subscribe_handler)
bpy.types.GPENCIL_MT_layer_context_menu.remove(gpencil_layer_dropdown_menu)
bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header)
bpy.types.DATA_PT_gpencil_layers.remove(layer_name_builder)
bpy.types.DATA_PT_gpencil_layers.remove(layer_name_builder_ui)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

202
OP_material_picker.py Normal file
View File

@ -0,0 +1,202 @@
import bpy
from bpy.types import Operator
import mathutils
from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils
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.lock or l.hide:
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.lock or l.hide or not l.active_frame:
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'}
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",
shift=False,
ctrl=False,
alt = False,
oskey=False,
# key_modifier='S', # S like Sample
)
kmi.properties.fill_only = True
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 = 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)

View File

@ -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, 5, 7),
"version": (1, 6, 0),
"blender": (2, 91, 0),
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "",
@ -49,6 +49,7 @@ from . import OP_realign
from . import OP_depth_move
from . import OP_key_duplicate_send
from . import OP_layer_manager
from . import OP_material_picker
from . import OP_eraser_brush
from . import TOOL_eraser_brush
from . import handler_draw_cam
@ -564,6 +565,7 @@ def register():
OP_key_duplicate_send.register()
OP_layer_manager.register()
OP_eraser_brush.register()
OP_material_picker.register()
TOOL_eraser_brush.register()
handler_draw_cam.register()
UI_tools.register()
@ -589,6 +591,7 @@ def unregister():
bpy.utils.unregister_class(cls)
UI_tools.unregister()
handler_draw_cam.unregister()
OP_material_picker.unregister()
OP_eraser_brush.unregister()
TOOL_eraser_brush.unregister()
OP_layer_manager.unregister()