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
|
# 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
|
1.7.4
|
||||||
|
|
||||||
- added: Pick layer from closest stroke in paint mode using quick press on `W` for stroke (and `alt+W` for fills)
|
- 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
|
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
|
# if not color:#get active color name
|
||||||
# color = gp.palettes.active.colors.active.name
|
# color = gp.palettes.active.colors.active.name
|
||||||
if not layers:
|
if not layers:
|
||||||
#by default all visible layers
|
# by default all visible layers
|
||||||
layers = [l for l in gpl if not l.hide and not l.lock]#[]
|
layers = [l for l in gpl if not l.hide and not l.lock] # []
|
||||||
if not isinstance(layers, list):
|
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]
|
layers = [layers]
|
||||||
|
|
||||||
stroke_list = []#one stroke list for all layers.
|
stroke_list = [] # one stroke list for all layers.
|
||||||
|
|
||||||
for l in layers:
|
for l in layers:
|
||||||
f = l.active_frame
|
f = l.active_frame
|
||||||
|
|
||||||
if f:#active frame can be None
|
if f: # active frame can be None
|
||||||
if not copy:
|
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:
|
for s in f.strokes:
|
||||||
if s.select:
|
if s.select:
|
||||||
# separate in multiple stroke if parts of the strokes a selected.
|
# separate in multiple stroke if parts of the strokes a selected.
|
||||||
sel = [i for i, p in enumerate(s.points) if p.select]
|
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
|
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))
|
group = list(map(itemgetter(1), g))
|
||||||
substrokes.append(group)
|
substrokes.append(group)
|
||||||
|
|
||||||
for ss in substrokes:
|
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))
|
stroke_list.append(dump_gp_stroke_range(s,ss,l,obj))
|
||||||
|
|
||||||
#Cutting operation
|
# Cutting operation
|
||||||
if not copy:
|
if not copy:
|
||||||
maxindex = len(s.points)-1
|
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)
|
f.strokes.remove(s)
|
||||||
else:
|
else:
|
||||||
neg = [i for i, p in enumerate(s.points) if not p.select]
|
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 = []
|
staying = []
|
||||||
for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]):
|
for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]):
|
||||||
group = list(map(itemgetter(1), g))
|
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:
|
if group[0] > 0:
|
||||||
group.insert(0,group[0]-1)
|
group.insert(0,group[0]-1)
|
||||||
if group[-1] < maxindex:
|
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:
|
for ns in staying:
|
||||||
if len(ns) > 1:
|
if len(ns) > 1:
|
||||||
staylist.append(dump_gp_stroke_range(s,ns,l,obj))
|
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
|
'''#full stroke version
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from os import error
|
from os import error
|
||||||
import bpy
|
import bpy
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from bpy.types import Operator
|
from bpy.types import Operator
|
||||||
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
||||||
from bpy.app.handlers import persistent
|
from bpy.app.handlers import persistent
|
||||||
from .utils import get_addon_prefs
|
from .utils import get_addon_prefs, is_vector_close
|
||||||
|
|
||||||
|
|
||||||
# --- OPS ---
|
# --- OPS ---
|
||||||
|
@ -227,6 +228,37 @@ def refresh_areas():
|
||||||
for area in bpy.context.screen.areas:
|
for area in bpy.context.screen.areas:
|
||||||
area.tag_redraw()
|
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):
|
class GPTB_OT_select_set_same_prefix(Operator):
|
||||||
bl_idname = "gp.select_same_prefix"
|
bl_idname = "gp.select_same_prefix"
|
||||||
|
@ -253,14 +285,35 @@ class GPTB_OT_select_set_same_prefix(Operator):
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
prefs = get_addon_prefs()
|
prefs = get_addon_prefs()
|
||||||
sep = prefs.separator # '_'
|
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)
|
res = re.search(PATTERN, act.info)
|
||||||
if not res:
|
if not res:
|
||||||
# self.report({'ERROR'}, f'Error scanning {act.info}')
|
self.report({'ERROR'}, f'Error scanning {act.info}')
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
namespace = res.group(1)
|
|
||||||
|
namespace = res.group('tag')
|
||||||
if not namespace:
|
if not namespace:
|
||||||
self.report({'WARNING'}, f'No prefix detected in {act.info} with separator {sep}')
|
self.report({'WARNING'}, f'No prefix detected in {act.info} with separator {sep}')
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
@ -270,12 +323,22 @@ class GPTB_OT_select_set_same_prefix(Operator):
|
||||||
# namespace = act.info.split(sep,1)[0]
|
# namespace = act.info.split(sep,1)[0]
|
||||||
# namespace_bool_list = [l.info.split(sep,1)[0] == namespace for l in gpl]
|
# namespace_bool_list = [l.info.split(sep,1)[0] == namespace for l in gpl]
|
||||||
|
|
||||||
## with reg
|
## with reg # only active
|
||||||
namespace_bool_list = [l.info.split(sep,1)[0] + sep == namespace for l in gpl]
|
# namespace_bool_list = [l.info.split(sep,1)[0] + sep == namespace for l in gpl]
|
||||||
gpl.foreach_set('select', namespace_bool_list)
|
# 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':
|
elif self.mode == 'SET':
|
||||||
for l in gpl:
|
for l in pool:
|
||||||
if not l.select or l == act:
|
if not l.select or l == act:
|
||||||
continue
|
continue
|
||||||
layer_name_build(l, prefix=namespace.strip(sep))
|
layer_name_build(l, prefix=namespace.strip(sep))
|
||||||
|
@ -284,6 +347,7 @@ class GPTB_OT_select_set_same_prefix(Operator):
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class GPTB_OT_select_set_same_color(Operator):
|
class GPTB_OT_select_set_same_color(Operator):
|
||||||
bl_idname = "gp.select_same_color"
|
bl_idname = "gp.select_same_color"
|
||||||
bl_label = "Select Same Color"
|
bl_label = "Select Same Color"
|
||||||
|
@ -307,19 +371,60 @@ class GPTB_OT_select_set_same_color(Operator):
|
||||||
return self.execute(context)
|
return self.execute(context)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
gpl = context.object.data.layers
|
gp = context.object.data
|
||||||
act = gpl.active
|
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':
|
if self.mode == 'SELECT':
|
||||||
same_color_bool = [l.channel_color == act.channel_color for l in gpl]
|
## NEED FOREACH TO APPLY SELECT
|
||||||
gpl.foreach_set('select', same_color_bool)
|
|
||||||
|
## 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':
|
elif self.mode == 'SET':
|
||||||
activate_channel_group_color(context)
|
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:
|
if not l.select or l == act:
|
||||||
continue
|
continue
|
||||||
l.channel_color = act.channel_color
|
l.channel_color = color
|
||||||
|
|
||||||
refresh_areas()
|
refresh_areas()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
|
@ -10,7 +10,7 @@ class GP_OT_pick_closest_layer(Operator):
|
||||||
bl_idname = "gp.pick_closest_layer"
|
bl_idname = "gp.pick_closest_layer"
|
||||||
bl_label = "Active Closest Stroke Layer"
|
bl_label = "Active Closest Stroke Layer"
|
||||||
bl_description = "Pick closest stroke layer"
|
bl_description = "Pick closest stroke layer"
|
||||||
bl_options = {"REGISTER"} # , "UNDO"
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
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'}
|
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def palette_manager_menu(self, context):
|
def palette_manager_menu(self, context):
|
||||||
"""Palette menu to append in existing menu"""
|
"""Palette menu to append in existing menu"""
|
||||||
# GPENCIL_MT_material_context_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')
|
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 = (
|
classes = (
|
||||||
GPTB_PT_sidebar_panel,
|
GPTB_PT_sidebar_panel,
|
||||||
GPTB_PT_checker,
|
GPTB_PT_checker,
|
||||||
|
@ -385,8 +392,10 @@ def register():
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
bpy.utils.register_class(cls)
|
bpy.utils.register_class(cls)
|
||||||
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
|
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():
|
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)
|
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
||||||
for cls in reversed(classes):
|
for cls in reversed(classes):
|
||||||
bpy.utils.unregister_class(cls)
|
bpy.utils.unregister_class(cls)
|
||||||
|
|
14
__init__.py
14
__init__.py
|
@ -15,7 +15,7 @@ bl_info = {
|
||||||
"name": "GP toolbox",
|
"name": "GP toolbox",
|
||||||
"description": "Set of tools for Grease Pencil in animation production",
|
"description": "Set of tools for Grease Pencil in animation production",
|
||||||
"author": "Samuel Bernou, Christophe Seux",
|
"author": "Samuel Bernou, Christophe Seux",
|
||||||
"version": (1, 7, 4),
|
"version": (1, 7, 5),
|
||||||
"blender": (2, 91, 0),
|
"blender": (2, 91, 0),
|
||||||
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
||||||
"warning": "",
|
"warning": "",
|
||||||
|
@ -26,8 +26,9 @@ bl_info = {
|
||||||
|
|
||||||
# from . import addon_updater_ops
|
# from . import addon_updater_ops
|
||||||
|
|
||||||
from .utils import *
|
# from .utils import *
|
||||||
from .functions import *
|
from .utils import get_addon_prefs, draw_kmi
|
||||||
|
# from .functions import *
|
||||||
|
|
||||||
## GMIC
|
## GMIC
|
||||||
from .GP_guided_colorize import GP_colorize
|
from .GP_guided_colorize import GP_colorize
|
||||||
|
@ -48,7 +49,7 @@ from . import OP_realign
|
||||||
from . import OP_depth_move
|
from . import OP_depth_move
|
||||||
from . import OP_key_duplicate_send
|
from . import OP_key_duplicate_send
|
||||||
from . import OP_layer_manager
|
from . import OP_layer_manager
|
||||||
from . import OP_stroke_picker
|
from . import OP_layer_picker
|
||||||
from . import OP_material_picker
|
from . import OP_material_picker
|
||||||
from . import OP_eraser_brush
|
from . import OP_eraser_brush
|
||||||
from . import TOOL_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 .properties import GP_PG_ToolsSettings, GP_PG_FixSettings
|
||||||
|
|
||||||
|
|
||||||
from bpy.props import (FloatProperty,
|
from bpy.props import (FloatProperty,
|
||||||
BoolProperty,
|
BoolProperty,
|
||||||
EnumProperty,
|
EnumProperty,
|
||||||
|
@ -580,7 +582,7 @@ def register():
|
||||||
OP_layer_manager.register()
|
OP_layer_manager.register()
|
||||||
OP_eraser_brush.register()
|
OP_eraser_brush.register()
|
||||||
OP_material_picker.register()
|
OP_material_picker.register()
|
||||||
OP_stroke_picker.register()
|
OP_layer_picker.register()
|
||||||
TOOL_eraser_brush.register()
|
TOOL_eraser_brush.register()
|
||||||
handler_draw_cam.register()
|
handler_draw_cam.register()
|
||||||
UI_tools.register()
|
UI_tools.register()
|
||||||
|
@ -607,7 +609,7 @@ def unregister():
|
||||||
UI_tools.unregister()
|
UI_tools.unregister()
|
||||||
handler_draw_cam.unregister()
|
handler_draw_cam.unregister()
|
||||||
TOOL_eraser_brush.unregister()
|
TOOL_eraser_brush.unregister()
|
||||||
OP_stroke_picker.unregister()
|
OP_layer_picker.unregister()
|
||||||
OP_material_picker.unregister()
|
OP_material_picker.unregister()
|
||||||
OP_eraser_brush.unregister()
|
OP_eraser_brush.unregister()
|
||||||
OP_layer_manager.unregister()
|
OP_layer_manager.unregister()
|
||||||
|
|
115
utils.py
115
utils.py
|
@ -3,6 +3,7 @@ import numpy as np
|
||||||
import bmesh
|
import bmesh
|
||||||
import mathutils
|
import mathutils
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
|
import math
|
||||||
from math import sqrt
|
from math import sqrt
|
||||||
from sys import platform
|
from sys import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -702,6 +703,11 @@ def detect_OS():
|
||||||
print("Cannot detect OS, python 'sys.platform' give :", myOS)
|
print("Cannot detect OS, python 'sys.platform' give :", myOS)
|
||||||
return None
|
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):
|
def convert_attr(Attr):
|
||||||
'''Convert given value to a Json serializable format'''
|
'''Convert given value to a Json serializable format'''
|
||||||
if isinstance(Attr, (mathutils.Vector,mathutils.Color)):
|
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):
|
if isinstance(_message, str):
|
||||||
_message = [_message]
|
_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