From 660c3c4d76503b04fca199f5d5d12782a070b9cc Mon Sep 17 00:00:00 2001 From: pullusb Date: Wed, 24 Jul 2024 17:33:31 +0200 Subject: [PATCH] interactive mode --- __init__.py | 2 +- interpolate_strokes/operators.py | 148 ++++++++++++++++++++----------- ui.py | 6 +- 3 files changed, 100 insertions(+), 56 deletions(-) diff --git a/__init__.py b/__init__.py index 1e040d4..b70ab80 100755 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "GP Interpolate", "author": "Christophe Seux, Samuel Bernou", - "version": (0, 8, 4), + "version": (0, 8, 5), "blender": (4, 0, 2), "location": "Sidebar > Gpencil Tab > Interpolate", "description": "Interpolate Grease pencil strokes over 3D", diff --git a/interpolate_strokes/operators.py b/interpolate_strokes/operators.py index 88f39bb..fe0ae78 100644 --- a/interpolate_strokes/operators.py +++ b/interpolate_strokes/operators.py @@ -21,6 +21,8 @@ class GP_OT_interpolate_stroke_base(bpy.types.Operator): bl_description = 'Interpolate Stroke based on user bound triangle' bl_options = {'REGISTER', 'UNDO'} + interactive : bpy.props.BoolProperty(name='Interactive', default=False, options={'SKIP_SAVE'}) + next : bpy.props.BoolProperty(name='Next', default=True, options={'SKIP_SAVE'}) @classmethod @@ -38,22 +40,23 @@ class GP_OT_interpolate_stroke_base(bpy.types.Operator): else: return f"Interpolate Stroke Backward" - def apply_and_store(self): - '''individual item in self.store_list: (prop, attr, [new_val])''' - for item in self.store_list: + def apply_and_store(self, attrs): + '''individual item in attrs: (prop, attr, [new_val])''' + for item in attrs: prop, attr = item[:2] - self.store.append( (prop, attr, getattr(prop, attr)) ) + self.stored_attrs.append( (prop, attr, getattr(prop, attr)) ) if len(item) >= 3: setattr(prop, attr, item[2]) def restore(self): - for prop, attr, old_val in self.store: + for prop, attr, old_val in self.stored_attrs: setattr(prop, attr, old_val) def exit(self, context, status='INFO', text=None, cancelled=False): context.area.header_text_set(None) wm = context.window_manager - wm.progress_end() # Pgs + if self.report_progress: + wm.progress_end() # Pgs if self.timer: wm.event_timer_remove(self.timer) self.restore() @@ -65,8 +68,8 @@ class GP_OT_interpolate_stroke_base(bpy.types.Operator): ## Remove Plane and it's collection after use if self.plane is not None: bpy.data.objects.remove(self.plane) - if self.toolcol is not None: - bpy.data.collections.remove(self.toolcol) + if self.tool_col is not None: + bpy.data.collections.remove(self.tool_col) cancel_state = '(Stopped!) ' if cancelled else '' mess = f'{cancel_state}{self.loop_count} interpolated frame(s) ({time()-self.start:.3f}s)' @@ -84,33 +87,36 @@ class GP_OT_interpolate_stroke_base(bpy.types.Operator): def get_stroke_to_interpolate(self, context): ## Get strokes to interpolate tgt_strokes = [s for s in self.gp.data.layers.active.active_frame.strokes if s.select] - + ## If nothing selected in sculpt/paint, Select all before triggering if not tgt_strokes and context.mode in ('SCULPT_GPENCIL', 'PAINT_GPENCIL'): for s in self.gp.data.layers.active.active_frame.strokes: s.select = True tgt_strokes = self.gp.data.layers.active.active_frame.strokes - if not tgt_strokes: - return self.exit(context, status='ERROR', text='No stroke selected!') + if tgt_strokes: + return tgt_strokes + + return self.exit(context, status='ERROR', text='No stroke selected!') - return tgt_strokes ## For now, operators have their own invoke def invoke(self, context, event): self.debug = False - self.store_list = [] - self.store = [] - self.loop_count = 0 + self.stored_attrs = [] # context manager store/restore + self.loop_count = 0 # frames list iterator self.start = time() - self.scan_time = None - self.plane = None - self.toolcol = None + self.scan_time = None # to print time at exit in debug mode + self.plane = None # 3D Plane for bone interpolation + self.tool_col = None # collection containing 3D plane self.gp = context.object self.settings = context.scene.gp_interpo_settings self.frames_to_jump = None self.cancelled = False self.timer = None + self.timer_event = 'TIMER' + self.report_progress = (self.settings.use_animation and not self.interactive) + self.interpolated_keys = {context.scene.frame_current} ## Remove interpolation_plane collection ! (unseen, but can be hit) if interp_plane := bpy.data.objects.get('interpolation_plane'): @@ -118,11 +124,6 @@ class GP_OT_interpolate_stroke_base(bpy.types.Operator): if interp_col := bpy.data.collections.get('interpolation_tool'): bpy.data.collections.remove(interp_col) - ## Determine on what key/keys to jump - self.frames_to_jump = following_keys(forward=self.next, animation=self.settings.use_animation) - if not len(self.frames_to_jump): - return self.exit(context, status='WARNING', text='No keyframe available in this direction') - ## Change active layer if strokes are selected only on this layer layers = [l for l in self.gp.data.layers if (not l.lock and l.active_frame) @@ -137,8 +138,23 @@ class GP_OT_interpolate_stroke_base(bpy.types.Operator): ## Set active layer self.gp.data.layers.active = layers[0] - # TODO: Expose timer (in preferences ?) to let user more time to see result between frames - self.timer = context.window_manager.event_timer_add(0.05, window=context.window) + + # if self.interactive: + # ## TODO: Allow even if 0 keys are available + # ## disable timer event detection + # self.timer_event = None + # # Add available keys in other direction then sort + # self.frames_to_jump += following_keys(forward=not self.next, animation=self.settings.use_animation or self.interactive) + # self.frames_to_jump.sort() + + if not self.interactive: + ## Determine on what key/keys to jump + self.frames_to_jump = following_keys(forward=self.next, animation=self.settings.use_animation or self.interactive) + if not len(self.frames_to_jump): + return self.exit(context, status='WARNING', text='No keyframe available in this direction') + + # TODO: Expose timer (in preferences ?) to let user more time to see result between frames + self.timer = context.window_manager.event_timer_add(0.04, window=context.window) ## Converted to modal from "operator_single" @@ -185,7 +201,7 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): if state := super().invoke(context, event): return state - scn = bpy.context.scene + scn = context.scene origin = scn.camera.matrix_world.to_translation() @@ -209,13 +225,13 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): ## Ensure collection and plane exists # get/create collection - self.toolcol = bpy.data.collections.get('interpolation_tool') - if not self.toolcol: - self.toolcol = bpy.data.collections.new('interpolation_tool') + self.tool_col = bpy.data.collections.get('interpolation_tool') + if not self.tool_col: + self.tool_col = bpy.data.collections.new('interpolation_tool') - if self.toolcol.name not in bpy.context.scene.collection.children: - bpy.context.scene.collection.children.link(self.toolcol) - self.toolcol.hide_viewport = True # needed ? + if self.tool_col.name not in bpy.context.scene.collection.children: + bpy.context.scene.collection.children.link(self.tool_col) + self.tool_col.hide_viewport = True # needed ? # get/create meshplane self.plane = bpy.data.objects.get('interpolation_plane') @@ -223,8 +239,8 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): self.plane = create_plane(name='interpolation_plane') self.plane.select_set(False) - if self.plane.name not in self.toolcol.objects: - self.toolcol.objects.link(self.plane) + if self.plane.name not in self.tool_col.objects: + self.tool_col.objects.link(self.plane) target_obj = self.plane elif self.settings.method == 'GEOMETRY': @@ -243,14 +259,14 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): target_obj = override ## Prepare context manager - self.store_list = [ + attrs = [ # (context.view_layer.objects, 'active', self.gp), (context.tool_settings, 'use_keyframe_insert_auto', True), # (bpy.context.scene.render, 'simplify_subdivision', 0), ] ## Set everything in SETUP list - self.apply_and_store() + self.apply_and_store(attrs) if self.settings.method == 'BONE': ## replace plane @@ -307,27 +323,52 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): # Jump frame and paste - context.window_manager.progress_begin(self.frames_to_jump[0], self.frames_to_jump[-1]) # Pgs + if self.report_progress: + context.window_manager.progress_begin(self.frames_to_jump[0], self.frames_to_jump[-1]) # Pgs context.window_manager.modal_handler_add(self) context.area.header_text_set('Starting interpolation | Esc: Cancel') return {'RUNNING_MODAL'} def modal(self, context, event): - frame_num = len(self.frames_to_jump) - percentage = (self.loop_count) / (frame_num) * 100 - context.area.header_text_set(f'Interpolation {percentage:.0f}% {self.loop_count + 1}/{frame_num} | Esc: Cancel') + scn = context.scene - if event.type in {'RIGHTMOUSE', 'ESC'}: - context.area.header_text_set(f'Cancelling') + if self.interactive: + frame = None + self.loop_count = 0 # Reset to keep inifinite loop + prev_frame_num = following_keys(forward=False) + prev_frame_num = prev_frame_num[0] if prev_frame_num else None + next_frame_num = following_keys(forward=True) + next_frame_num = next_frame_num[0] if next_frame_num else None + context.area.header_text_set(f'Interpolate: {prev_frame_num} < {context.scene.frame_current} > {next_frame_num} | Esc: Finish') + + if event.type == 'LEFT_ARROW' and event.value == 'PRESS': + frame = prev_frame_num + if event.type == 'RIGHT_ARROW' and event.value == 'PRESS': + frame = next_frame_num + if frame is None: + self.report({'WARNING'}, 'No frame to jump to in this direction!') + + else: + frame_num = len(self.frames_to_jump) + percentage = (self.loop_count) / (frame_num) * 100 + context.area.header_text_set(f'Interpolation {percentage:.0f}% {self.loop_count + 1}/{frame_num} | Esc: Cancel') + + if event.type in {'RIGHTMOUSE', 'ESC', 'RET'}: return self.exit(context, status='WARNING', text='Cancelling', cancelled=True) ## -- LOOPTIMER - if event.type == 'TIMER': - f = self.frames_to_jump[self.loop_count] - bpy.context.window_manager.progress_update(f) # Pgs - scn = bpy.context.scene - scn.frame_set(f) + if event.type == self.timer_event or (self.interactive and frame is not None): + if not self.interactive: + frame = self.frames_to_jump[self.loop_count] + scn.frame_set(frame) + if frame in self.interpolated_keys: + self.report({'INFO'}, f'SKIP {frame} (already interpolated)') + return {'RUNNING_MODAL'} + print(f'-> {frame}') + if self.report_progress: + context.window_manager.progress_update(frame) # Pgs + origin = scn.camera.matrix_world.to_translation() plane_co, plane_no = get_gp_draw_plane(self.gp) bpy.ops.gpencil.select_all(action='DESELECT') @@ -385,12 +426,15 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): bpy.ops.gpencil.select_all(action='SELECT') for stroke in other_strokes: stroke.select = False - - self.loop_count += 1 - if self.loop_count >= len(self.frames_to_jump): - return self.exit(context) - bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + if self.interactive: + self.interpolated_keys.add(frame) + else: + self.loop_count += 1 + if self.loop_count >= len(self.frames_to_jump): + return self.exit(context) + + # bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) return {'RUNNING_MODAL'} diff --git a/ui.py b/ui.py index aafdc94..beb2ad6 100755 --- a/ui.py +++ b/ui.py @@ -40,9 +40,9 @@ class GP_PT_interpolate(bpy.types.Panel): direction_button_row.operator(ops_id, text=prev_text, icon=prev_icon).next = False direction_button_row.operator(ops_id, text=next_text, icon=next_icon).next = True - ## Button for interactive mode - # interactive_mode_row = row.row() - # interactive_mode_row.operator(ops_id, text='', icon='ACTION_TWEAK') # SNAP_INCREMENT, ACTION_TWEAK, CON_ACTION, CENTER_ONLY + ## Button for interactive mode (icons: SNAP_INCREMENT, ACTION_TWEAK, CON_ACTION, CENTER_ONLY) + interactive_mode_row = row.row() + interactive_mode_row.operator(ops_id, text='', icon='ACTION_TWEAK').interactive = True col.prop(settings, 'use_animation', text='Animation')