diff --git a/CHANGELOG.md b/CHANGELOG.md index fa52acb..40f0023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +2.5.0 + +- added: Animation manager new button `Frame Select Step` (sort of a checker deselect, but in GP dopesheet) + 2.4.0 - changed: Batch reproject consider camera movement and is almost 8x faster diff --git a/OP_helpers.py b/OP_helpers.py index a72dc97..c1e7238 100644 --- a/OP_helpers.py +++ b/OP_helpers.py @@ -1,15 +1,17 @@ -from time import ctime import bpy import mathutils -from mathutils import Vector#, Matrix -from pathlib import Path import math + +from time import ctime +from mathutils import Vector #, Matrix +from pathlib import Path from math import radians +from bpy.types import Operator + from .view3d_utils import View3D from . import utils - -class GPTB_OT_copy_text(bpy.types.Operator): +class GPTB_OT_copy_text(Operator): bl_idname = "wm.copytext" bl_label = "Copy To Clipboard" bl_description = "Insert passed text to clipboard" @@ -23,7 +25,7 @@ class GPTB_OT_copy_text(bpy.types.Operator): self.report({'INFO'}, mess) return {"FINISHED"} -class GPTB_OT_flipx_view(bpy.types.Operator): +class GPTB_OT_flipx_view(Operator): bl_idname = "view3d.camera_mirror_flipx" bl_label = "Cam Mirror Flipx" bl_description = "Invert X scale on camera to flip image horizontally" @@ -38,7 +40,7 @@ class GPTB_OT_flipx_view(bpy.types.Operator): context.scene.camera.scale.x *= -1 return {"FINISHED"} -class GPTB_OT_view_camera_frame_fit(bpy.types.Operator): +class GPTB_OT_view_camera_frame_fit(Operator): bl_idname = "view3d.view_camera_frame_fit" bl_label = "View Fit" bl_description = "Fit the camera in view (view 1:1)" @@ -67,7 +69,7 @@ class GPTB_OT_view_camera_frame_fit(bpy.types.Operator): return {"FINISHED"} -class GPTB_OT_rename_data_from_obj(bpy.types.Operator): +class GPTB_OT_rename_data_from_obj(Operator): bl_idname = "gp.rename_data_from_obj" bl_label = "Rename GP From Object" bl_description = "Rename the GP datablock with the same name as the object" @@ -148,7 +150,7 @@ def get_gp_alignement_vector(context): elif orient == 'CURSOR': return Vector((0,0,1))#.rotate(context.scene.cursor.matrix) -class GPTB_OT_draw_cam(bpy.types.Operator): +class GPTB_OT_draw_cam(Operator): bl_idname = "gp.draw_cam_switch" bl_label = "Draw Cam Switch" bl_description = "switch between main camera and draw (manipulate) camera" @@ -276,7 +278,7 @@ class GPTB_OT_draw_cam(bpy.types.Operator): return {"FINISHED"} -class GPTB_OT_set_view_as_cam(bpy.types.Operator): +class GPTB_OT_set_view_as_cam(Operator): bl_idname = "gp.set_view_as_cam" bl_label = "Cam At View" bl_description = "Place the active camera at current viewpoint, parent to active object. (need to be out of camera)" @@ -318,7 +320,7 @@ class GPTB_OT_set_view_as_cam(bpy.types.Operator): return {"FINISHED"} -class GPTB_OT_reset_cam_rot(bpy.types.Operator): +class GPTB_OT_reset_cam_rot(Operator): bl_idname = "gp.reset_cam_rot" bl_label = "Reset Rotation" bl_description = "Reset rotation of the draw manipulation camera" @@ -396,7 +398,7 @@ class GPTB_OT_reset_cam_rot(bpy.types.Operator): context.space_data.region_3d.view_camera_offset = new_cam_offset return {"FINISHED"} -class GPTB_OT_toggle_mute_animation(bpy.types.Operator): +class GPTB_OT_toggle_mute_animation(Operator): bl_idname = "gp.toggle_mute_animation" bl_label = "Toggle Animation Mute" bl_description = "Enable/Disable animation evaluation\n(shift+clic to affect selection only)" @@ -451,7 +453,7 @@ class GPTB_OT_toggle_mute_animation(bpy.types.Operator): return {'FINISHED'} -class GPTB_OT_toggle_hide_gp_modifier(bpy.types.Operator): +class GPTB_OT_toggle_hide_gp_modifier(Operator): bl_idname = "gp.toggle_hide_gp_modifier" bl_label = "Toggle Modifier Hide" bl_description = "Show/Hide viewport on GP objects modifier\ @@ -481,7 +483,7 @@ class GPTB_OT_toggle_hide_gp_modifier(bpy.types.Operator): return {'FINISHED'} -class GPTB_OT_list_disabled_anims(bpy.types.Operator): +class GPTB_OT_list_disabled_anims(Operator): bl_idname = "gp.list_disabled_anims" bl_label = "List Disabled Anims" bl_description = "List disabled animations channels in scene. (shit+clic to list only on seleciton)" @@ -546,7 +548,7 @@ class GPTB_OT_list_disabled_anims(bpy.types.Operator): ## TODO presets are still not used... need to make a custom preset save/remove/quickload manager to be efficient (UIlist ?) -class GPTB_OT_overlay_presets(bpy.types.Operator): +class GPTB_OT_overlay_presets(Operator): bl_idname = "gp.overlay_presets" bl_label = "Overlay presets" bl_description = "Overlay save/load presets for showing only whats needed" @@ -601,7 +603,7 @@ class GPTB_OT_overlay_presets(bpy.types.Operator): return {'FINISHED'} -class GPTB_OT_clear_active_frame(bpy.types.Operator): +class GPTB_OT_clear_active_frame(Operator): bl_idname = "gp.clear_active_frame" bl_label = "Clear Active Frame" bl_description = "Delete all strokes in active frames" @@ -633,7 +635,7 @@ class GPTB_OT_clear_active_frame(bpy.types.Operator): return {'FINISHED'} -class GPTB_OT_check_canvas_alignement(bpy.types.Operator): +class GPTB_OT_check_canvas_alignement(Operator): bl_idname = "gp.check_canvas_alignement" bl_label = "Check Canvas Alignement" bl_description = "Check if view is aligned to canvas\nWarn if the drawing angle to surface is too high\nThere can be some error margin" @@ -663,7 +665,116 @@ class GPTB_OT_check_canvas_alignement(bpy.types.Operator): # self.report({ret}, message) return {'FINISHED'} -class GPTB_OT_open_addon_prefs(bpy.types.Operator): +class GPTB_OT_step_select_frames(Operator): + bl_idname = "gptb.step_select_frames" + bl_label = "Step Select Frame" + bl_description = "Select frames by a step frame value" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'GPENCIL' + + start : bpy.props.IntProperty(name='Start Frame', + description='Start frame of the step animation', + default=100) + + step : bpy.props.IntProperty(name='Step', + description='Step of the frame, value of 2 select one frame on two', + default=2, + min=2) + + strict : bpy.props.BoolProperty(name='Strict', + description='Strictly select step frame from start to scene end range\ + \nElse reset step when a gap exsits already', + default=False) + + # TODO: add option to start at cursor (True default) + + def invoke(self, context, execute): + ## list frame to keep + return context.window_manager.invoke_props_dialog(self, width=450) + # return self.execute(context) + + def draw(self, context): + layout = self.layout + col = layout.column() + col.prop(self, 'step') + + col.prop(self, 'strict') + if self.strict: + col.prop(self, 'start') + + ## helper (need more work) + # col.separator() + # range_string = f"{', '.join(numbers[:3])} ... {', '.join(numbers[-3:])}" + # col.label(text=f'Will keep {range_string}') + if not self.strict: + col.label(text=f'Each gap will be considered a new step start', icon='INFO') + + def execute(self, context): + numbers = [i for i in range(self.start, context.scene.frame_end + 1, self.step)] + self.to_select = numbers + + ## Negative switch : list frames to remove + self.to_select = [i for i in range(self.start, context.scene.frame_end + 1) if i not in numbers] + + gp = context.object.data + + ## Get frame summary (reset start after each existing gaps) + key_summary = list(set([f.frame_number for l in gp.layers for f in l.frames])) + key_summary.sort() + print('key summary: ', key_summary) + + start = key_summary[0] + if self.strict: + to_select = self.to_select + else: + to_select = [] + prev = None + for k in key_summary: + print(k, prev) + if prev is not None and k != prev + 1: + ## this is a gap ! new start + prev = start = k + # print('new start', start) + continue + + new_range = [i for i in range(start, key_summary[-1] + 1, self.step)] + # print('new_range: ', new_range) + if k not in new_range: + to_select.append(k) + + prev = k + + ## deselect all + for l in gp.layers: + for f in l.frames: + f.select = False + + print('To select:', to_select) + gct = 0 + for i in to_select: + ct = 0 + for l in gp.layers: + frame = next((f for f in l.frames if f.frame_number == i), None) + if not frame: + continue + + ## Select instead of remove + frame.select = True + ## Optionnally remove frames ? + # l.frames.remove(frame) + + ct += 1 + + # print(f'{i}: Selected {ct} frame(s)') + gct += ct + + self.report({'INFO'}, f'Selected {gct} frames') + return {"FINISHED"} + +class GPTB_OT_open_addon_prefs(Operator): bl_idname = "gptb.open_addon_prefs" bl_label = "Open Addon Prefs" bl_description = "Open user preferences window in addon tab and prefill the search with addon name" @@ -686,6 +797,7 @@ GPTB_OT_toggle_hide_gp_modifier, GPTB_OT_list_disabled_anims, GPTB_OT_clear_active_frame, GPTB_OT_check_canvas_alignement, +GPTB_OT_step_select_frames, GPTB_OT_open_addon_prefs, ) diff --git a/UI_tools.py b/UI_tools.py index e4017fa..3d94929 100644 --- a/UI_tools.py +++ b/UI_tools.py @@ -242,6 +242,9 @@ class GPTB_PT_anim_manager(Panel): row.operator('gp.toggle_hide_gp_modifier', text='ON', icon=on_icon).show = True row.operator('gp.toggle_hide_gp_modifier', text='OFF', icon=off_icon).show = False + ## Step Select Frames + col.operator('gptb.step_select_frames') + ## Follow curve path col = col.column() row = col.row(align=True) diff --git a/__init__.py b/__init__.py index ad82d75..805f01a 100755 --- a/__init__.py +++ b/__init__.py @@ -4,7 +4,7 @@ bl_info = { "name": "GP toolbox", "description": "Tool set for Grease Pencil in animation production", "author": "Samuel Bernou, Christophe Seux", -"version": (2, 4, 0), +"version": (2, 5, 0), "blender": (3, 0, 0), "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "warning": "",