diff --git a/OP_realign.py b/OP_realign.py index 1fb68c0..4c44705 100644 --- a/OP_realign.py +++ b/OP_realign.py @@ -5,7 +5,7 @@ from math import pi import numpy as np from time import time -def batch_reproject(obj, project_type='VIEW', all_strokes=True, restore_frame=False): +def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False): '''Reproject - ops method :all_stroke: affect hided, locked layers ''' @@ -30,7 +30,7 @@ def batch_reproject(obj, project_type='VIEW', all_strokes=True, restore_frame=Fa bpy.context.scene.frame_set(f.frame_number) # switch to edit to reproject through ops bpy.ops.gpencil.select_all(action='SELECT') - bpy.ops.gpencil.reproject(type=project_type) # default is VIEW + bpy.ops.gpencil.reproject(type=proj_type) # default is VIEW bpy.ops.gpencil.select_all(action='DESELECT') # restore @@ -46,7 +46,7 @@ def batch_reproject(obj, project_type='VIEW', all_strokes=True, restore_frame=Fa bpy.context.scene.frame_current = oframe -def align_global(reproject=True, ref=None): +def align_global(reproject=True, ref=None, all_strokes=True): if not ref: ref = bpy.context.scene.camera @@ -101,10 +101,10 @@ def align_global(reproject=True, ref=None): o.matrix_basis = new_mat if reproject: - batch_reproject(o, project_type='FRONT') + batch_reproject(o, proj_type='FRONT', all_strokes=all_strokes) -def align_all_frames(reproject=True, ref=None): +def align_all_frames(reproject=True, ref=None, all_strokes=True): print('aligning all frames...') @@ -128,9 +128,9 @@ def align_all_frames(reproject=True, ref=None): rot_keys = list(set(rot_keys)) # TODO # TOTHINK - # for now the rotation of the object is adjusted at every check.... - # might be better to check camera rotation of the current frame only stored as copy. - # else the object rotate following the camera... + # for now the rotation of the object is adjusted at every frame.... + # might be better to check camera rotation of the current frame only (stored as copy). + # else the object rotate following the cameraview vector (not constant)... mat_90 = Matrix.Rotation(-pi/2, 4, 'X') @@ -191,7 +191,7 @@ def align_all_frames(reproject=True, ref=None): if reproject: - batch_reproject(o, project_type='FRONT') + batch_reproject(o, proj_type='FRONT', all_strokes=all_strokes) return @@ -199,21 +199,60 @@ def align_all_frames(reproject=True, ref=None): class GPTB_OT_realign(bpy.types.Operator): bl_idname = "gp.realign" bl_label = "Realign GP" - bl_description = "Realign the grease pencil on camera" + bl_description = "Realign the grease pencil front axis with active camera" bl_options = {"REGISTER"} @classmethod def poll(cls, context): return context.object and context.object.type == 'GPENCIL' - reproject : bpy.props.BoolProperty(name='Reproject', default=True) + reproject : bpy.props.BoolProperty( + name='Reproject', default=True, + description='Reproject stroke on the new alignment') + all_strokes : bpy.props.BoolProperty( + name='All Strokes', default=False, + description='Hided and locked layer will also be reprojected') + + ## add option to bake strokes if rotation anim is not constant ? might generate too many Keyframes + + def invoke(self, context, event): + if context.object.data.use_multiedit: + self.report({'ERROR'}, 'Does not work in Multi-edit') + return {"CANCELLED"} + + self.alert = '' + o = context.object + if o.animation_data and o.animation_data.action: + act = o.animation_data.action + for chan in ('rotation_euler', 'rotation_quaternion'): + if act.fcurves.find(chan): + self.alert = 'Animated Rotation (CONSTANT interpolation)' + interpos = [p for fcu in act.fcurves if fcu.data_path == chan for p in fcu.keyframe_points if p.interpolation != 'CONSTANT'] + if interpos: + self.alert = f'Animated Rotation ! ({len(interpos)} key not constant)' + break + + return context.window_manager.invoke_props_dialog(self, width=450) + + def draw(self, context): + layout = self.layout + if self.alert: + layout.label(text=self.alert, icon='INFO') + layout.label(text='(rotations key will be overwritten to face camera)') + + layout.prop(self, "reproject") + layout.label(text='Realign the GP front axis with active camera') + if self.reproject: + if not context.region_data.view_perspective == 'CAMERA': + layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR') + layout.prop(self, "all_strokes") + def execute(self, context): t0 = time() oframe = context.scene.frame_current - - o = bpy.context.object + o = bpy.context.object if o.animation_data and o.animation_data.action: if o.animation_data.action.fcurves.find('rotation_euler') or o.animation_data.action.fcurves.find('rotation_quaternion'): align_all_frames(reproject=self.reproject) @@ -225,6 +264,7 @@ class GPTB_OT_realign(bpy.types.Operator): align_global(reproject=self.reproject) context.scene.frame_current = oframe + print(f'\nGlobal Realign ({time()-t0:.2f}s)') return {"FINISHED"} @@ -232,39 +272,57 @@ class GPTB_OT_realign(bpy.types.Operator): class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator): bl_idname = "gp.batch_reproject_all_frames" - bl_label = "Reproject All Frame" - bl_description = "Reproject all frame of active object." + bl_label = "Reproject All Frames" + bl_description = "Reproject all frames of active object." bl_options = {"REGISTER"} @classmethod def poll(cls, context): return context.object and context.object.type == 'GPENCIL' - reproject : bpy.props.BoolProperty(name='Reproject', default=True) + # reproject : bpy.props.BoolProperty(name='Reproject', default=True) - # TODO witch side ? + all_strokes : bpy.props.BoolProperty( + name='All Strokes', default=False, + description='Hided and locked layer will also be reprojected') + + type: bpy.props.EnumProperty(name='Type', + items=(('FRONT', "Front", ""), + ('SIDE', "Side", ""), + ('TOP', "Top", ""), + ('VIEW', "View", ""), + ('SURFACE', "Surface", ""), + ('CURSOR', "Cursor", ""), + ), + default='FRONT') - # def invoke(self, context, event): - # return context.window_manager.invoke_props_dialog(self) - # return self.execute(context) + def invoke(self, context, event): + if context.object.data.use_multiedit: + self.report({'ERROR'}, 'Does not work in Multi-edit') + return {"CANCELLED"} + return context.window_manager.invoke_props_dialog(self) def draw(self, context): - self.layout.prop(self, "toggle", text="Toggle to redraw") - for i in range(20): - self.layout.label(str(i)) + layout = self.layout + if not context.region_data.view_perspective == 'CAMERA': + layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR') + layout.prop(self, "all_strokes") + layout.prop(self, "type") def execute(self, context): t0 = time() - batch_reproject(bpy.context.object, all_strokes=True, restore_frame=True) # all_strokes=True, restore_frame=False) - self.report({'INFO'}, f'\nReprojected in ({time()-t0:.2f}s)' ) + + batch_reproject(context.object, proj_type=self.type, all_strokes=self.all_strokes, restore_frame=True) + + self.report({'INFO'}, f'Reprojected in ({time()-t0:.2f}s)' ) return {"FINISHED"} classes = ( -GPTB_OT_realign, -GPTB_OT_batch_reproject_all_frames +# GPTB_OT_realign, +GPTB_OT_batch_reproject_all_frames, ) def register(): diff --git a/README.md b/README.md index 46cb4ea..670fb48 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,10 @@ Panel in sidebar : 3D view > sidebar 'N' > Gpencil ## Changelog: +1.0.9: + +- feat: Reproject all frames operator + 1.0.8: - feat: Keyframe jump filter added in UI to change general behavior. Keymap own jump filter can override this new global settings if specified diff --git a/UI_tools.py b/UI_tools.py index ef96a5e..ad610a6 100644 --- a/UI_tools.py +++ b/UI_tools.py @@ -160,15 +160,17 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel): box = layout.box() box.label(text='Missing base material setup', icon='INFO') box.operator('gp.load_default_palette') - + else: layout.label(text='No GP object selected') layout.prop(context.scene.gptoolprops, 'edit_lines_opacity') + # layout.operator('gp.realign') + layout.operator('gp.batch_reproject_all_frames') # text=Batch Reproject ## Create empty frame on layer (ops stored under GP_colorize... might be best to separate in another panel ) - layout.operator('gp.create_empty_frames', icon = 'DECORATE_KEYFRAME') + layout.operator('gp.create_empty_frames', icon='DECORATE_KEYFRAME') ## File checker row = layout.row(align=True) diff --git a/__init__.py b/__init__.py index ad7f8f8..d15f721 100644 --- a/__init__.py +++ b/__init__.py @@ -15,7 +15,7 @@ bl_info = { "name": "GP toolbox", "description": "Set of tools for Grease Pencil in animation production", "author": "Samuel Bernou", -"version": (1, 0, 8), +"version": (1, 0, 9), "blender": (2, 91, 0), "location": "sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "warning": "", @@ -44,6 +44,7 @@ from . import OP_palettes from . import OP_file_checker from . import OP_render from . import OP_copy_paste +from . import OP_realign from . import keymaps from .OP_pseudo_tint import GPT_OT_auto_tint_gp_layers @@ -439,6 +440,7 @@ def register(): OP_cursor_snap_canvas.register() OP_render.register() OP_copy_paste.register() + OP_realign.register() UI_tools.register() keymaps.register() bpy.types.Scene.gptoolprops = bpy.props.PointerProperty(type = GP_PG_ToolsSettings) @@ -460,6 +462,7 @@ def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) UI_tools.unregister() + OP_realign.unregister() OP_copy_paste.unregister() OP_render.unregister() OP_cursor_snap_canvas.unregister()