select/set color/prefix improved
1.7.5 - feat: Select/set by color and by prefix now works on every displayed dopesheet layer (and react correctly to filters) - ui: exposed user prefs `Channel Group Color` prop in dopesheet > sidebar > View > Display panelgpv2
parent
56cbc04c65
commit
44ccb3d146
|
@ -1,8 +1,15 @@
|
|||
# Changelog
|
||||
|
||||
1.7.5
|
||||
|
||||
- feat: Select/set by color and by prefix now works on every displayed dopesheet layer (and react correctly to filters)
|
||||
- ui: exposed user prefs `Channel Group Color` prop in dopesheet > sidebar > View > Display panel
|
||||
- add undo step for `W`'s select layer from closest stroke
|
||||
|
||||
1.7.4
|
||||
|
||||
- added: Pick layer from closest stroke in paint mode using quick press on `W` for stroke (and `alt+W` for fills)
|
||||
- fix: copy-paste keymap error on background rendering
|
||||
|
||||
1.7.3
|
||||
|
||||
|
|
|
@ -153,38 +153,38 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow fil
|
|||
# if not color:#get active color name
|
||||
# color = gp.palettes.active.colors.active.name
|
||||
if not layers:
|
||||
#by default all visible layers
|
||||
layers = [l for l in gpl if not l.hide and not l.lock]#[]
|
||||
# by default all visible layers
|
||||
layers = [l for l in gpl if not l.hide and not l.lock] # []
|
||||
if not isinstance(layers, list):
|
||||
#if a single layer object is send put in a list
|
||||
# if a single layer object is send put in a list
|
||||
layers = [layers]
|
||||
|
||||
stroke_list = []#one stroke list for all layers.
|
||||
stroke_list = [] # one stroke list for all layers.
|
||||
|
||||
for l in layers:
|
||||
f = l.active_frame
|
||||
|
||||
if f:#active frame can be None
|
||||
if f: # active frame can be None
|
||||
if not copy:
|
||||
staylist = []#init part of strokes that must survive on this layer
|
||||
staylist = [] # init part of strokes that must survive on this layer
|
||||
|
||||
for s in f.strokes:
|
||||
if s.select:
|
||||
# separate in multiple stroke if parts of the strokes a selected.
|
||||
sel = [i for i, p in enumerate(s.points) if p.select]
|
||||
substrokes = []# list of list containing isolated selection
|
||||
substrokes = [] # list of list containing isolated selection
|
||||
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):# continuity stroke have same substract result between point index and enumerator
|
||||
group = list(map(itemgetter(1), g))
|
||||
substrokes.append(group)
|
||||
|
||||
for ss in substrokes:
|
||||
if len(ss) > 1:#avoid copy isolated points
|
||||
if len(ss) > 1: # avoid copy isolated points
|
||||
stroke_list.append(dump_gp_stroke_range(s,ss,l,obj))
|
||||
|
||||
#Cutting operation
|
||||
# Cutting operation
|
||||
if not copy:
|
||||
maxindex = len(s.points)-1
|
||||
if len(substrokes) == maxindex+1:#si un seul substroke, c'est le stroke entier
|
||||
if len(substrokes) == maxindex+1: # if only one substroke, then it's the full stroke
|
||||
f.strokes.remove(s)
|
||||
else:
|
||||
neg = [i for i, p in enumerate(s.points) if not p.select]
|
||||
|
@ -192,7 +192,7 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow fil
|
|||
staying = []
|
||||
for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]):
|
||||
group = list(map(itemgetter(1), g))
|
||||
#extend group to avoid gap when cut, a bit dirty
|
||||
# extend group to avoid gap when cut, a bit dirty
|
||||
if group[0] > 0:
|
||||
group.insert(0,group[0]-1)
|
||||
if group[-1] < maxindex:
|
||||
|
@ -202,7 +202,7 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow fil
|
|||
for ns in staying:
|
||||
if len(ns) > 1:
|
||||
staylist.append(dump_gp_stroke_range(s,ns,l,obj))
|
||||
#make a negative list containing all last index
|
||||
# make a negative list containing all last index
|
||||
|
||||
|
||||
'''#full stroke version
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from os import error
|
||||
import bpy
|
||||
import re
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
||||
from bpy.app.handlers import persistent
|
||||
from .utils import get_addon_prefs
|
||||
from .utils import get_addon_prefs, is_vector_close
|
||||
|
||||
|
||||
# --- OPS ---
|
||||
|
@ -227,6 +228,37 @@ def refresh_areas():
|
|||
for area in bpy.context.screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
def build_layers_targets_from_dopesheet(context):
|
||||
'''Return all selected layers on context GP dopesheet according to seelction and filters'''
|
||||
ob = context.object
|
||||
gpl = context.object.data.layers
|
||||
act = gpl.active
|
||||
dopeset = context.space_data.dopesheet
|
||||
|
||||
|
||||
if dopeset.show_only_selected:
|
||||
pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
|
||||
else:
|
||||
pool = [o for o in context.scene.objects if o.type == 'GPENCIL']
|
||||
if not dopeset.show_hidden:
|
||||
pool = [o for o in pool if o.visible_get()]
|
||||
|
||||
layer_pool = [l for o in pool for l in o.data.layers]
|
||||
layer_pool = list(set(layer_pool)) # remove dupli-layers from same data source with
|
||||
|
||||
# apply search filter
|
||||
if dopeset.filter_text:
|
||||
layer_pool = [l for l in layer_pool if (dopeset.filter_text.lower() in l.info.lower()) ^ dopeset.use_filter_invert]
|
||||
|
||||
return layer_pool
|
||||
|
||||
def build_dope_gp_list(layer_list):
|
||||
'''Take a list of GP layers return a dict with pairs {gp data : own layer list}'''
|
||||
from collections import defaultdict
|
||||
gps = defaultdict(list)
|
||||
for l in layer_list:
|
||||
gps[l.id_data].append(l)
|
||||
return gps
|
||||
|
||||
class GPTB_OT_select_set_same_prefix(Operator):
|
||||
bl_idname = "gp.select_same_prefix"
|
||||
|
@ -253,14 +285,35 @@ class GPTB_OT_select_set_same_prefix(Operator):
|
|||
def execute(self, context):
|
||||
prefs = get_addon_prefs()
|
||||
sep = prefs.separator # '_'
|
||||
gpl = context.object.data.layers
|
||||
act = gpl.active
|
||||
|
||||
gp = context.object.data
|
||||
act = gp.layers.active
|
||||
|
||||
pool = build_layers_targets_from_dopesheet(context)
|
||||
if not pool:
|
||||
self.report({'ERROR'}, 'No layers found in current GP dopesheet')
|
||||
return {"CANCELLED"}
|
||||
|
||||
gp_dic = build_dope_gp_list(pool)
|
||||
if not act:
|
||||
# Check in other displayed layer if there is an active one
|
||||
for gp, _layer_list in gp_dic.items():
|
||||
if gp.layers.active:
|
||||
# overwrite gp variable at the same time
|
||||
act = gp.layers.active
|
||||
break
|
||||
if not act:
|
||||
self.report({'ERROR'}, 'No active layer to base action')
|
||||
return {"CANCELLED"}
|
||||
|
||||
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.info}')
|
||||
|
||||
res = re.search(PATTERN, act.info)
|
||||
if not res:
|
||||
# self.report({'ERROR'}, f'Error scanning {act.info}')
|
||||
self.report({'ERROR'}, f'Error scanning {act.info}')
|
||||
return {"CANCELLED"}
|
||||
namespace = res.group(1)
|
||||
|
||||
namespace = res.group('tag')
|
||||
if not namespace:
|
||||
self.report({'WARNING'}, f'No prefix detected in {act.info} with separator {sep}')
|
||||
return {"CANCELLED"}
|
||||
|
@ -270,12 +323,22 @@ class GPTB_OT_select_set_same_prefix(Operator):
|
|||
# namespace = act.info.split(sep,1)[0]
|
||||
# namespace_bool_list = [l.info.split(sep,1)[0] == namespace for l in gpl]
|
||||
|
||||
## with reg
|
||||
namespace_bool_list = [l.info.split(sep,1)[0] + sep == namespace for l in gpl]
|
||||
gpl.foreach_set('select', namespace_bool_list)
|
||||
## with reg # only active
|
||||
# namespace_bool_list = [l.info.split(sep,1)[0] + sep == namespace for l in gpl]
|
||||
# gpl.foreach_set('select', namespace_bool_list)
|
||||
|
||||
## don't work Need Foreach set per gp
|
||||
# for l in pool:
|
||||
# l.select = l.info.split(sep,1)[0] + sep == namespace
|
||||
|
||||
for gp, layers in gp_dic.items():
|
||||
# check namespace + restrict selection to visible layers according to filters
|
||||
# TODO : Should use the regex pattern to detect and compare r.group('tag')
|
||||
namespace_bool_list = [(l in layers) and (l.info.split(sep,1)[0] + sep == namespace) for l in gp.layers]
|
||||
gp.layers.foreach_set('select', namespace_bool_list)
|
||||
|
||||
elif self.mode == 'SET':
|
||||
for l in gpl:
|
||||
for l in pool:
|
||||
if not l.select or l == act:
|
||||
continue
|
||||
layer_name_build(l, prefix=namespace.strip(sep))
|
||||
|
@ -284,6 +347,7 @@ class GPTB_OT_select_set_same_prefix(Operator):
|
|||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class GPTB_OT_select_set_same_color(Operator):
|
||||
bl_idname = "gp.select_same_color"
|
||||
bl_label = "Select Same Color"
|
||||
|
@ -307,19 +371,60 @@ class GPTB_OT_select_set_same_color(Operator):
|
|||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
gpl = context.object.data.layers
|
||||
act = gpl.active
|
||||
gp = context.object.data
|
||||
act = gp.layers.active
|
||||
|
||||
pool = build_layers_targets_from_dopesheet(context)
|
||||
if not pool:
|
||||
self.report({'ERROR'}, 'No layers found in current GP dopesheet')
|
||||
return {"CANCELLED"}
|
||||
|
||||
gp_dic = build_dope_gp_list(pool)
|
||||
if not act:
|
||||
# Check in other displayed layer if there is an active one
|
||||
for gp, _layer_list in gp_dic.items():
|
||||
if gp.layers.active:
|
||||
# overwrite gp variable at the same time
|
||||
act = gp.layers.active
|
||||
break
|
||||
if not act:
|
||||
self.report({'ERROR'}, 'No active layer to base action')
|
||||
return {"CANCELLED"}
|
||||
|
||||
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.info}')
|
||||
color = act.channel_color
|
||||
if self.mode == 'SELECT':
|
||||
same_color_bool = [l.channel_color == act.channel_color for l in gpl]
|
||||
gpl.foreach_set('select', same_color_bool)
|
||||
## NEED FOREACH TO APPLY SELECT
|
||||
|
||||
## Only on active object
|
||||
# same_color_bool = [l.channel_color == act.channel_color for l in gpl]
|
||||
# gpl.foreach_set('select', same_color_bool) # only
|
||||
|
||||
# On multiple objects -- don't work, need foreach
|
||||
# for l in pool:
|
||||
# print(l.id_data.name, l.info, l.channel_color == act.channel_color)
|
||||
# l.select = l.channel_color == act.channel_color
|
||||
|
||||
"""
|
||||
gps = []
|
||||
for l in pool:
|
||||
if l.id_data not in gps:
|
||||
gps.append(l.id_data)
|
||||
for gp in gps:
|
||||
same_color_bool = [(l in pool) and is_vector_close(l.channel_color, color) for l in gp.layers]
|
||||
gp.layers.foreach_set('select', same_color_bool)
|
||||
"""
|
||||
for gp, layers in gp_dic.items():
|
||||
# check color and restrict selection to visible layers according to filters
|
||||
same_color_bool = [(l in layers) and is_vector_close(l.channel_color, color) for l in gp.layers]
|
||||
gp.layers.foreach_set('select', same_color_bool)
|
||||
|
||||
elif self.mode == 'SET':
|
||||
activate_channel_group_color(context)
|
||||
for l in gpl:
|
||||
for l in pool: # only on active object use gpl
|
||||
if not l.select or l == act:
|
||||
continue
|
||||
l.channel_color = act.channel_color
|
||||
l.channel_color = color
|
||||
|
||||
refresh_areas()
|
||||
return {"FINISHED"}
|
||||
|
|
|
@ -10,7 +10,7 @@ class GP_OT_pick_closest_layer(Operator):
|
|||
bl_idname = "gp.pick_closest_layer"
|
||||
bl_label = "Active Closest Stroke Layer"
|
||||
bl_description = "Pick closest stroke layer"
|
||||
bl_options = {"REGISTER"} # , "UNDO"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
11
UI_tools.py
11
UI_tools.py
|
@ -355,7 +355,6 @@ class GPTB_PT_cam_ref_panel(bpy.types.Panel):
|
|||
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'}
|
||||
"""
|
||||
|
||||
|
||||
def palette_manager_menu(self, context):
|
||||
"""Palette menu to append in existing menu"""
|
||||
# GPENCIL_MT_material_context_menu
|
||||
|
@ -371,6 +370,14 @@ def palette_manager_menu(self, context):
|
|||
layout.operator("gp.clean_material_stack", text='Clean material Stack', icon='NODE_MATERIAL')
|
||||
|
||||
|
||||
def expose_use_channel_color_pref(self, context):
|
||||
# add in GreasePencilLayerDisplayPanel (gp dopesheet View > Display)
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
layout.label(text='Use Channel Colors (User preferences):')
|
||||
layout.prop(context.preferences.edit, 'use_anim_channel_group_colors')
|
||||
|
||||
classes = (
|
||||
GPTB_PT_sidebar_panel,
|
||||
GPTB_PT_checker,
|
||||
|
@ -385,8 +392,10 @@ def register():
|
|||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
|
||||
bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref)
|
||||
|
||||
def unregister():
|
||||
bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref)
|
||||
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
|
14
__init__.py
14
__init__.py
|
@ -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, 7, 4),
|
||||
"version": (1, 7, 5),
|
||||
"blender": (2, 91, 0),
|
||||
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
||||
"warning": "",
|
||||
|
@ -26,8 +26,9 @@ bl_info = {
|
|||
|
||||
# from . import addon_updater_ops
|
||||
|
||||
from .utils import *
|
||||
from .functions import *
|
||||
# from .utils import *
|
||||
from .utils import get_addon_prefs, draw_kmi
|
||||
# from .functions import *
|
||||
|
||||
## GMIC
|
||||
from .GP_guided_colorize import GP_colorize
|
||||
|
@ -48,7 +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_stroke_picker
|
||||
from . import OP_layer_picker
|
||||
from . import OP_material_picker
|
||||
from . import OP_eraser_brush
|
||||
from . import TOOL_eraser_brush
|
||||
|
@ -61,6 +62,7 @@ from . import UI_tools
|
|||
|
||||
from .properties import GP_PG_ToolsSettings, GP_PG_FixSettings
|
||||
|
||||
|
||||
from bpy.props import (FloatProperty,
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
|
@ -580,7 +582,7 @@ def register():
|
|||
OP_layer_manager.register()
|
||||
OP_eraser_brush.register()
|
||||
OP_material_picker.register()
|
||||
OP_stroke_picker.register()
|
||||
OP_layer_picker.register()
|
||||
TOOL_eraser_brush.register()
|
||||
handler_draw_cam.register()
|
||||
UI_tools.register()
|
||||
|
@ -607,7 +609,7 @@ def unregister():
|
|||
UI_tools.unregister()
|
||||
handler_draw_cam.unregister()
|
||||
TOOL_eraser_brush.unregister()
|
||||
OP_stroke_picker.unregister()
|
||||
OP_layer_picker.unregister()
|
||||
OP_material_picker.unregister()
|
||||
OP_eraser_brush.unregister()
|
||||
OP_layer_manager.unregister()
|
||||
|
|
115
utils.py
115
utils.py
|
@ -3,6 +3,7 @@ import numpy as np
|
|||
import bmesh
|
||||
import mathutils
|
||||
from mathutils import Vector
|
||||
import math
|
||||
from math import sqrt
|
||||
from sys import platform
|
||||
import subprocess
|
||||
|
@ -702,6 +703,11 @@ def detect_OS():
|
|||
print("Cannot detect OS, python 'sys.platform' give :", myOS)
|
||||
return None
|
||||
|
||||
def is_vector_close(a, b, rel_tol=1e-03):
|
||||
'''compare Vector or sequence of value
|
||||
by default tolerance is set on 1e-03 (0.001)'''
|
||||
return all([math.isclose(i, j, rel_tol=rel_tol) for i, j in zip(a,b)])
|
||||
|
||||
def convert_attr(Attr):
|
||||
'''Convert given value to a Json serializable format'''
|
||||
if isinstance(Attr, (mathutils.Vector,mathutils.Color)):
|
||||
|
@ -741,4 +747,111 @@ def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
|
|||
|
||||
if isinstance(_message, str):
|
||||
_message = [_message]
|
||||
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)
|
||||
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)
|
||||
|
||||
|
||||
### UI utils
|
||||
|
||||
## kmi draw for addon without delete button
|
||||
def draw_kmi(km, kmi, layout):
|
||||
map_type = kmi.map_type
|
||||
|
||||
# col = _indented_layout(layout)
|
||||
col = layout.column()
|
||||
if kmi.show_expanded:
|
||||
col = col.column(align=True)
|
||||
box = col.box()
|
||||
else:
|
||||
box = col.column()
|
||||
|
||||
split = box.split()
|
||||
|
||||
# header bar
|
||||
row = split.row(align=True)
|
||||
row.prop(kmi, "show_expanded", text="", emboss=False)
|
||||
row.prop(kmi, "active", text="", emboss=False)
|
||||
|
||||
if km.is_modal:
|
||||
row.separator()
|
||||
row.prop(kmi, "propvalue", text="")
|
||||
else:
|
||||
row.label(text=kmi.name)
|
||||
|
||||
row = split.row()
|
||||
row.prop(kmi, "map_type", text="")
|
||||
if map_type == 'KEYBOARD':
|
||||
row.prop(kmi, "type", text="", full_event=True)
|
||||
elif map_type == 'MOUSE':
|
||||
row.prop(kmi, "type", text="", full_event=True)
|
||||
elif map_type == 'NDOF':
|
||||
row.prop(kmi, "type", text="", full_event=True)
|
||||
elif map_type == 'TWEAK':
|
||||
subrow = row.row()
|
||||
subrow.prop(kmi, "type", text="")
|
||||
subrow.prop(kmi, "value", text="")
|
||||
elif map_type == 'TIMER':
|
||||
row.prop(kmi, "type", text="")
|
||||
else:
|
||||
row.label()
|
||||
|
||||
|
||||
### / Hided delete button
|
||||
if (not kmi.is_user_defined) and kmi.is_user_modified:
|
||||
row.operator("preferences.keyitem_restore", text="", icon='BACK').item_id = kmi.id
|
||||
else:
|
||||
pass ### NO REMOVB
|
||||
# row.operator(
|
||||
# "preferences.keyitem_remove",
|
||||
# text="",
|
||||
# # Abusing the tracking icon, but it works pretty well here.
|
||||
# icon=('TRACKING_CLEAR_BACKWARDS' if kmi.is_user_defined else 'X')
|
||||
# ).item_id = kmi.id
|
||||
### Hided delete button /
|
||||
|
||||
# Expanded, additional event settings
|
||||
if kmi.show_expanded:
|
||||
box = col.box()
|
||||
|
||||
split = box.split(factor=0.4)
|
||||
sub = split.row()
|
||||
|
||||
if km.is_modal:
|
||||
sub.prop(kmi, "propvalue", text="")
|
||||
else:
|
||||
# One day...
|
||||
# sub.prop_search(kmi, "idname", bpy.context.window_manager, "operators_all", text="")
|
||||
sub.prop(kmi, "idname", text="")
|
||||
|
||||
if map_type not in {'TEXTINPUT', 'TIMER'}:
|
||||
sub = split.column()
|
||||
subrow = sub.row(align=True)
|
||||
|
||||
if map_type == 'KEYBOARD':
|
||||
subrow.prop(kmi, "type", text="", event=True)
|
||||
subrow.prop(kmi, "value", text="")
|
||||
subrow_repeat = subrow.row(align=True)
|
||||
subrow_repeat.active = kmi.value in {'ANY', 'PRESS'}
|
||||
subrow_repeat.prop(kmi, "repeat", text="Repeat")
|
||||
elif map_type in {'MOUSE', 'NDOF'}:
|
||||
subrow.prop(kmi, "type", text="")
|
||||
subrow.prop(kmi, "value", text="")
|
||||
|
||||
subrow = sub.row()
|
||||
subrow.scale_x = 0.75
|
||||
subrow.prop(kmi, "any", toggle=True)
|
||||
subrow.prop(kmi, "shift", toggle=True)
|
||||
subrow.prop(kmi, "ctrl", toggle=True)
|
||||
subrow.prop(kmi, "alt", toggle=True)
|
||||
subrow.prop(kmi, "oskey", text="Cmd", toggle=True)
|
||||
subrow.prop(kmi, "key_modifier", text="", event=True)
|
||||
|
||||
# Operator properties
|
||||
box.template_keymap_item_properties(kmi)
|
||||
|
||||
|
||||
## Modal key maps attached to this operator
|
||||
# if not km.is_modal:
|
||||
# kmm = kc.keymaps.find_modal(kmi.idname)
|
||||
# if kmm:
|
||||
# draw_km(display_keymaps, kc, kmm, None, layout + 1)
|
||||
# layout.context_pointer_set("keymap", km)
|
Loading…
Reference in New Issue