diff --git a/CHANGELOG.md b/CHANGELOG.md index 481e787..8ad8db1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog + +1.9.0 + +- feat: New shortcuts: + - `F2` in Paint and Edit to rename active layer + - `Insert` add a new layer (same as Krita) + - `Shift + Insert` add a new layer and immediately pop-up a rename box + - `page up / page down` change active layer up/down with a temporary fade (settings in addon prefs) +- fix: error when tweaking `gp.duplicate_send_to_layer` shortcut + 1.8.1 - fix: Gp clipboard paste `Paste layers` don't skip empty frames anymore @@ -13,7 +23,6 @@ 1.7.8 - fix: reset rotation in draw cam mode keep view in the same place (counter camera rotation) -- code: initial enhancement for palette linking 1.7.7 diff --git a/OP_breakdowner.py b/OP_breakdowner.py index fc83787..aef291f 100644 --- a/OP_breakdowner.py +++ b/OP_breakdowner.py @@ -302,7 +302,7 @@ class OBJ_OT_breakdown_obj_anim(bpy.types.Operator): ### --- KEYMAP --- -breakdowner_addon_keymaps = [] +addon_keymaps = [] def register_keymaps(): if bpy.app.background: return @@ -322,16 +322,15 @@ def register_keymaps(): if ops_id not in km.keymap_items: km = addon.keymaps.new(name='3D View', space_type='VIEW_3D')#EMPTY kmi = km.keymap_items.new(ops_id, type="E", value="PRESS", shift=True) - breakdowner_addon_keymaps.append((km, kmi)) + addon_keymaps.append((km, kmi)) def unregister_keymaps(): if bpy.app.background: return - for km, kmi in breakdowner_addon_keymaps: + for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) - breakdowner_addon_keymaps.clear() - # del breakdowner_addon_keymaps[:] + addon_keymaps.clear() ### --- REGISTER --- diff --git a/OP_key_duplicate_send.py b/OP_key_duplicate_send.py index d2254c6..4f84a08 100644 --- a/OP_key_duplicate_send.py +++ b/OP_key_duplicate_send.py @@ -4,8 +4,10 @@ from bpy.types import Operator def get_layer_list(self, context): '''return (identifier, name, description) of enum content''' + if not context: + return [('None', 'None','None')] if not context.object: - return + return [('None', 'None','None')] return [(l.info, l.info, '') for l in context.object.data.layers if l != context.object.data.layers.active] # try: # except: @@ -23,7 +25,8 @@ class GPTB_OT_duplicate_send_to_layer(Operator) : layers_enum : bpy.props.EnumProperty( name="Duplicate to layers", description="Duplicate selected keys in active layer and send them to choosen layer", - items=get_layer_list + items=get_layer_list, + options={'HIDDEN'}, ) delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'}) @@ -110,12 +113,10 @@ def register_keymaps(): # if not pref.kfj_use_shortcut: # return addon = bpy.context.window_manager.keyconfigs.addon - # km = addon.keymaps.new(name = "Screen", space_type = "EMPTY") km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR") kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='D', value="PRESS", ctrl=True, shift=True) addon_keymaps.append((km,kmi)) - # km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR") # try duplicating km (seem to be error at unregsiter) kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='X', value="PRESS", ctrl=True, shift=True) kmi.properties.delete_source = True diff --git a/OP_layer_manager.py b/OP_layer_manager.py index 0db67e4..f22ee37 100644 --- a/OP_layer_manager.py +++ b/OP_layer_manager.py @@ -633,6 +633,107 @@ def subscribe_handler(dummy): ) +##--- Add layers + +class GPTB_PT_layer_name_ui(bpy.types.Panel): + bl_space_type = 'TOPBAR' # dummy + bl_region_type = 'HEADER' + bl_options = {'INSTANCED'} + bl_label = 'Layer Rename' + bl_ui_units_x = 14 + + def invoke(self, context, event): + # all_addons_l = get_modifier_list() + wm = context.window_manager + wm.invoke_props_dialog(self) # , width=600 + return {'FINISHED'} + + def draw(self, context): + layout = self.layout + + # def row_with_icon(layout, icon): + # # Edit first editable button in popup + # row = layout.row() + # row.activate_init = True + # row.label(icon=icon) + # return row + # row = row_with_icon(layout, 'OUTLINER_DATA_GP_LAYER') + + row = layout.row() + row.activate_init = True + row.label(icon='OUTLINER_DATA_GP_LAYER') + row.prop(context.object.data.layers.active, 'info', text='') + +def add_layer(context): + bpy.ops.gpencil.layer_add() + context.object.data.layers.active.use_lights = False + +class GPTB_OT_add_gp_layer_with_rename(Operator): + bl_idname = "gp.add_layer_rename" + bl_label = "Add Rename GPencil Layer" + bl_description = "Create a new gp layer with use light toggled off and popup a rename box" + bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'GPENCIL' + + def execute(self, context): + add_layer(context) + bpy.ops.wm.call_panel(name="GPTB_PT_layer_name_ui", keep_open = False) + return {"FINISHED"} + +class GPTB_OT_add_gp_layer(Operator): + bl_idname = "gp.add_layer" + bl_label = "Add GPencil Layer" + bl_description = "Create a new gp layer with use light toggled off" + bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'GPENCIL' + + def execute(self, context): + add_layer(context) + return {"FINISHED"} + + +addon_keymaps = [] +def register_keymaps(): + addon = bpy.context.window_manager.keyconfigs.addon + + ##---# Insert Layers + ## Insert new gp layer (with no use_light) + km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY") # global (only paint ?) + kmi = km.keymap_items.new('gp.add_layer', type='INSERT', value='PRESS') + addon_keymaps.append((km, kmi)) + + ## Insert new gp layer (with no use_light and immediately pop up a box to rename) + # km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY") # global (only paint ?) + kmi = km.keymap_items.new('gp.add_layer_rename', type='INSERT', value='PRESS', shift=True) + addon_keymaps.append((km, kmi)) + + ##---# F2 rename calls + ## Direct rename active layer in Paint mode + km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY") + kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS') + kmi.properties.name = 'GPTB_PT_layer_name_ui' + kmi.properties.keep_open = False + addon_keymaps.append((km, kmi)) + + ## Same in edit mode + km = addon.keymaps.new(name = "Grease Pencil Stroke Edit Mode", space_type = "EMPTY") + kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS') + kmi.properties.name = 'GPTB_PT_layer_name_ui' + kmi.properties.keep_open = False + addon_keymaps.append((km, kmi)) + + +def unregister_keymaps(): + for km, kmi in addon_keymaps: + km.keymap_items.remove(kmi) + addon_keymaps.clear() + + + classes=( GPTB_OT_rename_gp_layer, GPTB_OT_layer_name_build, @@ -640,9 +741,14 @@ classes=( GPTB_OT_layer_new_group, GPTB_OT_select_set_same_prefix, GPTB_OT_select_set_same_color, + + ## Layer add and pop-up rename + GPTB_PT_layer_name_ui, # pop-up + GPTB_OT_add_gp_layer_with_rename, # shift+Ins + GPTB_OT_add_gp_layer, # Ins ) -def register(): +def register(): for cls in classes: bpy.utils.register_class(cls) @@ -650,8 +756,10 @@ def register(): 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 + register_keymaps() def unregister(): + unregister_keymaps() 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) diff --git a/OP_layer_nav.py b/OP_layer_nav.py new file mode 100644 index 0000000..c8c354a --- /dev/null +++ b/OP_layer_nav.py @@ -0,0 +1,142 @@ +import bpy +from . import utils + +class GPT_OT_layer_nav(bpy.types.Operator): + bl_idname = "gp.layer_nav" + bl_label = "GP Layer Navigator" + bl_description = "Change active GP layer and highlight active for a moment" + bl_options = {'REGISTER', 'INTERNAL', 'UNDO'} + + direction : bpy.props.EnumProperty( + name='direction', + items=(('NONE', 'None', ''),('UP', 'Up', ''),('DOWN', 'Down', '')), + default='NONE', + description='Direction to change layer in active GPencil stack', + options={'SKIP_SAVE'}) + + ## hardcoded values + # interval = 0.04 # 0.1 + # limit = 1.8 + # fade_val = 0.35 + # use_fade_in = True + # fade_in_time = 0.5 + + def invoke(self, context, event): + ## initialise vvalue from prefs + prefs = utils.get_addon_prefs() + + if not prefs.nav_use_fade: + if self.direction == 'DOWN' or (event.type == 'PAGE_DOWN' and event.value == 'PRESS'): + utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'info') + + if self.direction == 'UP' or (event.type == 'PAGE_UP' and event.value == 'PRESS'): + utils.iterate_selector(context.object.data.layers, 'active_index', 1, info_attr = 'info') + return {'FINISHED'} + + self.interval = prefs.nav_interval + self.limit = prefs.nav_limit + self.fade_val = prefs.nav_fade_val + self.use_fade_in = prefs.nav_use_fade_in + self.fade_in_time = prefs.nav_fade_in_time + + self.lapse = 0 + wm = context.window_manager + args = (self, context) + + if context.space_data.overlay.use_gpencil_fade_layers: + self.fade_target = context.space_data.overlay.gpencil_fade_layer + else: + self.fade_target = 1.0 + + self.fade_start = self.limit - self.fade_in_time + + self.first = True + self._timer = wm.event_timer_add(self.interval, window=context.window) # 0.1 + wm.modal_handler_add(self) + return {'RUNNING_MODAL'} + + def store_settings(self, context): + self.org_use_gpencil_fade_layers = context.space_data.overlay.use_gpencil_fade_layers + self.org_gpencil_fade_layer = context.space_data.overlay.gpencil_fade_layer + context.space_data.overlay.use_gpencil_fade_layers = True + context.space_data.overlay.gpencil_fade_layer = self.fade_val + + def modal(self, context, event): + trigger = False + if event.type in {'RIGHTMOUSE', 'ESC', 'LEFTMOUSE'}: + self.stop_mod(context) + return {'CANCELLED'} + + if event.type == 'TIMER': + self.lapse += self.interval + + if self.lapse >= self.limit: + self.stop_mod(context) + return {'FINISHED'} + + ## Fade + if self.use_fade_in and (self.lapse > self.fade_start): + fade = utils.transfer_value(self.lapse, self.fade_start, self.limit, self.fade_val, self.fade_target) + # print(f'lapse {self.lapse} - fade {fade}') + context.space_data.overlay.gpencil_fade_layer = fade + + if self.direction == 'DOWN' or (event.type == 'PAGE_DOWN' and event.value == 'PRESS'): + _val = utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'info') + trigger = True + + if self.direction == 'UP' or (event.type == 'PAGE_UP' and event.value == 'PRESS'): + _val = utils.iterate_selector(context.object.data.layers, 'active_index', 1, info_attr = 'info') + # utils.iterate_selector(bpy.context.scene.grease_pencil.layers, 'active_index', 1, info_attr = 'info')#layers + trigger = True + + if trigger: + self.direction = 'NONE' + if self.first: + self.store_settings(context) + self.first=False + + if self.use_fade_in: + # reset fade to wanted value + context.space_data.overlay.gpencil_fade_layer = self.fade_val + + self.lapse = 0 # reset counter + return {'RUNNING_MODAL'}#running modal prevent original usage to be triggered (capture keys) + + return {'PASS_THROUGH'} + + def stop_mod(self, context): + # restore fade + context.space_data.overlay.use_gpencil_fade_layers = self.org_use_gpencil_fade_layers + context.space_data.overlay.gpencil_fade_layer = self.org_gpencil_fade_layer + wm = context.window_manager + wm.event_timer_remove(self._timer) + + +addon_keymaps = [] + +def register_keymap(): + addon = bpy.context.window_manager.keyconfigs.addon + + km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY") + + kmi = km.keymap_items.new('gp.layer_nav', type='PAGE_UP', value='PRESS') + kmi.properties.direction = 'UP' + addon_keymaps.append((km, kmi)) + + kmi = km.keymap_items.new('gp.layer_nav', type='PAGE_DOWN', value='PRESS') + kmi.properties.direction = 'DOWN' + addon_keymaps.append((km, kmi)) + +def unregister_keymap(): + for km, kmi in addon_keymaps: + km.keymap_items.remove(kmi) + + addon_keymaps.clear() + +def register(): + bpy.utils.register_class(GPT_OT_layer_nav) + register_keymap() + +def unregister(): + unregister_keymap() + bpy.utils.unregister_class(GPT_OT_layer_nav) diff --git a/__init__.py b/__init__.py index e3283c4..dba2bde 100755 --- a/__init__.py +++ b/__init__.py @@ -15,7 +15,7 @@ bl_info = { "name": "GP toolbox", "description": "Tool set for Grease Pencil in animation production", "author": "Samuel Bernou, Christophe Seux", -"version": (1, 8, 1), +"version": (1, 9, 0), "blender": (2, 91, 0), "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "warning": "", @@ -51,6 +51,7 @@ from . import OP_depth_move from . import OP_key_duplicate_send from . import OP_layer_manager from . import OP_layer_picker +from . import OP_layer_nav from . import OP_material_picker from . import OP_eraser_brush from . import TOOL_eraser_brush @@ -310,6 +311,38 @@ class GPTB_prefs(bpy.types.AddonPreferences): fixprops: bpy.props.PointerProperty(type = GP_PG_FixSettings) + ## GP Layer navigator + + nav_use_fade : BoolProperty( + name='Fade Inactive Layers', + description='Fade Inactive layers to determine active layer in a glimpse', + default=True) + + nav_fade_val : FloatProperty( + name='Fade Value', + description='Fade value for other layers when navigating (0=invisible)', + default=0.35, min=0.0, max=0.95, step=1, precision=2) + + nav_limit : FloatProperty( + name='Fade Duration', + description='Time of other layer faded when using layer navigation', + default=1.4, min=0.1, max=5, step=2, precision=1, subtype='TIME', unit='TIME') + + nav_use_fade_in : BoolProperty( + name='Progressive Fade Back', + description='Use a fade on other layer when navigating', + default=True) + + nav_fade_in_time : FloatProperty( + name='Fade-In Time', + description='Duration of the fade', + default=0.5, min=0.1, max=5, step=2, precision=2, subtype='TIME', unit='TIME') + + nav_interval : FloatProperty( + name='Refresh Rate', + description='Refresh rate for fade updating (upper value means stepped fade)', + default=0.04, min=0.01, max=0.5, step=3, precision=2, subtype='TIME', unit='TIME') + ## Temp cutter # temp_cutter_use_shortcut: BoolProperty( # name = "Use temp cutter Shortcut", @@ -415,6 +448,21 @@ class GPTB_prefs(bpy.types.AddonPreferences): box = layout.box() box.label(text='Tools options:') + + subbox= box.box() + subbox.label(text='Layer Navigation') + col = subbox.column() + col.prop(self, 'nav_use_fade') + if self.nav_use_fade: + row = col.row() + row.prop(self, 'nav_fade_val') + row.prop(self, 'nav_limit') + row = subbox.row(align=False) + row.prop(self, 'nav_use_fade_in') + if self.nav_use_fade_in: + row.prop(self, 'nav_fade_in_time', text='Fade Back Time') + # row.prop(self, 'nav_interval') # Do not expose refresh rate for now, not usefull to user... + box.prop(self, 'use_precise_eraser') if self.pref_tabs == 'KEYS': @@ -427,14 +475,15 @@ class GPTB_prefs(bpy.types.AddonPreferences): ## TOOL_eraser_brush.addon_keymaps # has a checkbox in prev_key_category = '' - kmi_see_list = [] for kms in [ OP_keyframe_jump.addon_keymaps, OP_copy_paste.addon_keymaps, - OP_breakdowner.breakdowner_addon_keymaps, + OP_breakdowner.addon_keymaps, OP_key_duplicate_send.addon_keymaps, OP_layer_picker.addon_keymaps, OP_material_picker.addon_keymaps, + OP_layer_nav.addon_keymaps, + # OP_layer_manager.addon_keymaps, # Do not display, wm.call_panel call panel ops mixed with natives shortcut (F2) ]: ct = 0 @@ -644,6 +693,7 @@ def register(): OP_eraser_brush.register() OP_material_picker.register() OP_layer_picker.register() + OP_layer_nav.register() TOOL_eraser_brush.register() handler_draw_cam.register() UI_tools.register() @@ -670,6 +720,7 @@ def unregister(): UI_tools.unregister() handler_draw_cam.unregister() TOOL_eraser_brush.unregister() + OP_layer_nav.unregister() OP_layer_picker.unregister() OP_material_picker.unregister() OP_eraser_brush.unregister() diff --git a/keymaps.py b/keymaps.py index c72d094..56faf82 100755 --- a/keymaps.py +++ b/keymaps.py @@ -8,6 +8,8 @@ def register_keymaps(): # km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D")# in 3D context # km = addon.keymaps.new(name = "Window", space_type = "EMPTY")# from everywhere + + ## Sculpt mode toggles km = addon.keymaps.new(name = "Grease Pencil Stroke Sculpt Mode", space_type = "EMPTY", region_type='WINDOW') kmi = km.keymap_items.new('wm.context_toggle', type='ONE', value='PRESS') @@ -22,6 +24,12 @@ def register_keymaps(): kmi.properties.data_path='scene.tool_settings.use_gpencil_select_mask_segment' addon_keymaps.append((km, kmi)) + ## T temp cutter (need disabling of native T shortcut, maybe expose a button to set the shortcut as user ?) + # km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY") + # kmi = km.keymap_items.new('gpencil.stroke_cutter', type='LEFTMOUSE', value='PRESS', key_modifier='T') + # kmi.properties.flat_caps=False + # addon_keymaps.append((km, kmi)) + def unregister_keymaps(): for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) diff --git a/utils.py b/utils.py index 6825731..543714a 100644 --- a/utils.py +++ b/utils.py @@ -30,19 +30,14 @@ def set_matrix(gp_frame,mat): # get view vector location (the 2 methods work fine) def get_view_origin_position(): - #method 1 + ## method 1 # from bpy_extras import view3d_utils # region = bpy.context.region # rv3d = bpy.context.region_data # view_loc = view3d_utils.region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0)) - # print("view_loc1", view_loc)#Dbg - - #method 2 + ## method 2 r3d = bpy.context.space_data.region_3d view_loc2 = r3d.view_matrix.inverted().translation - # print("view_loc2", view_loc2)#Dbg - # if view_loc != view_loc2: print('Might be an error when finding view coordinate') - return view_loc2 def location_to_region(worldcoords): @@ -76,6 +71,10 @@ def object_derived_get(ob, scene): return ob_matrix_pairs +# ----------------- +### Bmesh +# ----------------- + def link_vert(v,ordered_vert) : for e in v.link_edges : other_vert = e.other_vert(v) @@ -147,7 +146,11 @@ def gp_stroke_to_bmesh(strokes): return strokes_info -def simple_draw_gp_stroke(pts,frame,width = 2, mat_id = 0): +# ----------------- +### GP Drawing +# ----------------- + +def simple_draw_gp_stroke(pts, frame, width = 2, mat_id = 0): ''' draw basic stroke by passing list of point 3D coordinate the frame to draw on and optional width parameter (default = 2) @@ -169,11 +172,11 @@ def simple_draw_gp_stroke(pts,frame,width = 2, mat_id = 0): return stroke ## OLD - need update -def draw_gp_stroke(loop_info,frame,palette,width = 2) : +def draw_gp_stroke(loop_info, frame, palette, width = 2) : stroke = frame.strokes.new(palette) stroke.line_width = width - stroke.display_mode = '3DSPACE'# old->draw_mode + stroke.display_mode = '3DSPACE'# old -> draw_mode for i,info in enumerate(loop_info) : stroke.points.add() @@ -894,13 +897,6 @@ def draw_kmi(km, kmi, layout): ### linking utility # ----------------- -""" -def link_objects_in_blend(filepath, obj_name, link=True): - '''Link an object by name from a file, if link is False, append instead of linking''' - with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to): - data_to.objects = [o for o in data_from.objects if o == obj_name] # c.startswith(obj_name) - return data_to.objects - """ def link_objects_in_blend(filepath, obj_name_list, link=True): '''Link an object by name from a file, if link is False, append instead of linking''' if isinstance(obj_name_list, str): @@ -922,4 +918,37 @@ def check_objects_in_blend(filepath, avoid_camera=True): l = [o for o in data_from.objects if not any(x in o.lower() for x in ('camera', 'draw_cam', 'obj_cam'))] else: l = [o for o in data_from.objects] - return l \ No newline at end of file + return l + + +# ----------------- +### props handling +# ----------------- + +def iterate_selector(zone, attr, state, info_attr = None, active_access='active'): + '''Iterate with given attribute''' + item_number = len(zone) + if item_number <= 1: + return + + if getattr(zone, attr) == None: + print('no', attr, 'in', zone) + return + + if state: # swap + info = None + bottom = None + new_index = getattr(zone, attr) + state + setattr(zone, attr, new_index % item_number) + + if new_index == item_number: + bottom = 1 # bottom reached, cycle to first + elif new_index < 0: + bottom = -1 # up reached, cycle to last + + if info_attr: + active_item = getattr(zone, active_access) # active by default + if active_item: + info = getattr(active_item, info_attr) + + return info, bottom