diff --git a/__init__.py b/__init__.py index eaa8960..201d097 100755 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "gp interpolate", "author": "Christophe Seux, Samuel Bernou", - "version": (0, 7, 0), + "version": (0, 7, 1), "blender": (3, 6, 0), "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 2fd5de6..34bed73 100644 --- a/interpolate_strokes/operators.py +++ b/interpolate_strokes/operators.py @@ -48,7 +48,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): next : bpy.props.BoolProperty(name='Next', default=True, options={'SKIP_SAVE'}) def apply_and_store(self): - self.store = [] + # self.store = [] # item = (prop, attr, [new_val]) for item in self.store_list: prop, attr = item[:2] @@ -64,6 +64,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): self.debug = False self.status = 'START' self.store_list = [] + self.store = [] self.loop_count = 0 self.start = time() self.scan_time = None @@ -71,17 +72,24 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): self.toolcol = None self.gp = context.object self.settings = context.scene.gp_interpo_settings - + self.frames_to_jump = None self.cancelled = False - self._timer = context.window_manager.event_timer_add(0.01, window=context.window) context.window_manager.modal_handler_add(self) + self._timer = context.window_manager.event_timer_add(0.01, window=context.window) + context.area.header_text_set('Starting interpolation | Esc: Cancel') return {'RUNNING_MODAL'} - def exit_modal(self): - bpy.context.window_manager.progress_end() # Pgs + def exit_modal(self, context, status='INFO', text=None): + context.area.header_text_set(None) + wm = context.window_manager + wm.progress_end() # Pgs + wm.event_timer_remove(self._timer) self.restore() if self.debug: + ## show as solid ? + # if self.plane is not None: + # self.plane.display_type = 'SOLID' if self.scan_time is not None: print(f"Paste'n'place time {time()-self.start - self.scan_time}s") else: @@ -93,208 +101,227 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): bpy.data.collections.remove(self.toolcol) cancel_state = '(Stopped!) ' if self.cancelled else '' - self.report({'INFO'}, f'{cancel_state}{self.loop_count} interpolated frame(s) ({time()-self.start:.3f}s)') + mess = f'{cancel_state}{self.loop_count} interpolated frame(s) ({time()-self.start:.3f}s)' + if text: + print(mess) + self.report({status}, text) + else: + ## report standard info + if self.loop_count > 1: + self.report({'INFO'}, mess) def modal(self, context, event): if event.type in {'RIGHTMOUSE', 'ESC'}: print('Cancelling') + self.status = 'CANCELLED' self.cancelled = True - self.exit_modal() + context.area.header_text_set(f'Cancelling') + self.exit_modal(context) return {'CANCELLED'} - if event.type == 'TIMER': - if self.status == 'START': - - scn = bpy.context.scene + if self.frames_to_jump: + 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') + # (frame: {self.frames_to_jump[self.loop_count]}) - # auto_key_status = context.tool_settings.use_keyframe_insert_auto - # context.tool_settings.use_keyframe_insert_auto = True + if self.status == 'START': + scn = bpy.context.scene + ## Determine on what key/keys to jump + self.frames_to_jump = following_keys(forward=self.next, all_keys=self.settings.use_animation) + if not len(self.frames_to_jump): + self.exit_modal(context, status='WARNING', text='No keyframe available in this direction') + return {'CANCELLED'} + # print('self.frames_to_jump: ', self.frames_to_jump) + + self.gp = context.object - ## Determine on what key/keys to jump - self.frames_to_jump = following_keys(forward=self.next, all_keys=self.settings.use_animation) - if not len(self.frames_to_jump): - self.report({'WARNING'}, 'No keyframe available in this direction') - return {'CANCELLED'} - # print('self.frames_to_jump: ', self.frames_to_jump) - - self.gp = context.object + # matrix = np.array(self.gp.matrix_world, dtype='float64') + # origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') + matrix = self.gp.matrix_world + origin = scn.camera.matrix_world.to_translation() + + col = self.settings.target_collection + if not col: + col = scn.collection - # matrix = np.array(self.gp.matrix_world, dtype='float64') - # origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') - matrix = self.gp.matrix_world - origin = scn.camera.matrix_world.to_translation() - - col = self.settings.target_collection - if not col: - col = scn.collection + # print('----') + if not self.gp.data.layers.active: + self.exit_modal(context, status='ERROR', text='No active layer') + return {'CANCELLED'} + if not self.gp.data.layers.active.active_frame: + self.exit_modal(context, status='ERROR', text='No active frame') + return {'CANCELLED'} - # print('----') + 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 - 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: + self.exit_modal(context, status='ERROR', text='No stroke selected !') + return {'CANCELLED'} - if not tgt_strokes: - self.report({'ERROR'}, 'No stroke selected !') + included_cols = [c.name for c in self.gp.users_collection] + target_obj = None + + if self.settings.method == 'BONE': + if not self.settings.target_rig or not self.settings.target_bone: + self.exit_modal(context, status='ERROR', text='No Bone selected') return {'CANCELLED'} - included_cols = [c.name for c in self.gp.users_collection] - target_obj = None + included_cols.append('interpolation_tool') + + ## 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') + + 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.settings.method == 'BONE': - if not self.settings.target_rig or not self.settings.target_bone: - self.report({'ERROR'}, 'No Bone selected') - return {'CANCELLED'} + # get/create meshplane + self.plane = bpy.data.objects.get('interpolation_plane') + if not self.plane: + self.plane = create_plane(name='interpolation_plane') + self.plane.select_set(False) - included_cols.append('interpolation_tool') + if self.plane.name not in self.toolcol.objects: + self.toolcol.objects.link(self.plane) + target_obj = self.plane - ## 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') + elif self.settings.method == 'GEOMETRY': + if col != context.scene.collection: + included_cols.append(col.name) + ## Maybe include a plane just behind geo ? probably bad idea + + elif self.settings.method == 'OBJECT': + if not self.settings.target_object: + self.exit_modal(context, status='ERROR', text='No Object selected') + return {'CANCELLED'} + col = scn.collection # Reset collection filter + target_obj = self.settings.target_object + if target_obj.library: + ## Look if an override exists in scene to use instead of default object + if (override := next((o for o in scn.objects if o.name == target_obj.name and o.override_library), None)): + target_obj = override - 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 ? - - # get/create meshplane - self.plane = bpy.data.objects.get('interpolation_plane') - if not self.plane: - self.plane = create_plane(name='interpolation_plane') - self.plane.select_set(False) + ## Prepare context manager + self.store_list = [ + # (context.view_layer.objects, 'active', self.gp), + (context.tool_settings, 'use_keyframe_insert_auto', True), + # (bpy.context.scene.render, 'simplify_subdivision', 0), + ] + + # TODO: collection filter for GEOMETRY mode optimization - if self.plane.name not in self.toolcol.objects: - self.toolcol.objects.link(self.plane) - target_obj = self.plane + ## Hide optimizations (safe for Bone Mode only, if no error) + # if self.settings.method == 'BONE': + # ## TEST: Add collections containing rig (cannot be excluded) + # # rig_parent_cols = [c.name for c in scn.collection.children_recursive if self.settings.target_rig.name in c.all_objects] + # # included_cols += rig_parent_cols + # for vlc in context.view_layer.layer_collection.children: + # self.store_list.append( + # # (vlc, 'exclude', vlc.name not in included_cols), # If excluded rig does not update ! + # (vlc, 'hide_viewport', vlc.name not in included_cols), # viewport viz + # ) + + # print(f'Preparation {time()-start:.4f}s') + ## Set everything in SETUP list + self.apply_and_store() - elif self.settings.method == 'GEOMETRY': - if col != context.scene.collection: - included_cols.append(col.name) - ## Maybe include a plane just behind geo ? probably bad idea - - elif self.settings.method == 'OBJECT': - if not self.settings.target_object: - self.report({'ERROR'}, 'No Object selected') - return {'CANCELLED'} - col = scn.collection # Reset collection filter - target_obj = self.settings.target_object - if target_obj.library: - ## Look if an override exists in scene to use instead of default object - if (override := next((o for o in scn.objects if o.name == target_obj.name and o.override_library), None)): - target_obj = override + if self.settings.method == 'BONE': + ## replace plane + _bone_plane = plane_on_bone(self.settings.target_rig.pose.bones.get(self.settings.target_bone), + arm=self.settings.target_rig, + set_rotation=self.settings.use_bone_rotation, + mesh=True) - ## Prepare context manager - self.store_list = [ - # (context.view_layer.objects, 'active', self.gp), - (context.tool_settings, 'use_keyframe_insert_auto', True), - # (bpy.context.scene.render, 'simplify_subdivision', 0), - ] - - # TODO: for now, the collection filter is not used at all in GEOMETRY mode - # it can be used to hide collection for faster animation mode + ## Set collection visibility + intercol = bpy.data.collections.get('interpolation_tool') + vl_col = bpy.context.view_layer.layer_collection.children.get(intercol.name) + intercol.hide_viewport = vl_col.exclude = vl_col.hide_viewport = False - if self.settings.method == 'BONE': - ## TEST: Add collections containing rig (cannot be excluded) - # rig_parent_cols = [c.name for c in scn.collection.children_recursive if self.settings.target_rig.name in c.all_objects] - # included_cols += rig_parent_cols - for vlc in context.view_layer.layer_collection.children: - self.store_list.append( - # (vlc, 'exclude', vlc.name not in included_cols), # If excluded rig does not update ! - (vlc, 'hide_viewport', vlc.name not in included_cols), # viewport viz - ) - - # print(f'Preparation {time()-start:.4f}s') - ## Set everything in SETUP list - self.apply_and_store() + # Override collection + col = intercol + + dg = bpy.context.evaluated_depsgraph_get() + self.strokes_data = [] - if self.settings.method == 'BONE': - ## replace plane - _bone_plane = plane_on_bone(self.settings.target_rig.pose.bones.get(self.settings.target_bone), - arm=self.settings.target_rig, - set_rotation=self.settings.use_bone_rotation, - mesh=True) + for si, stroke in enumerate(tgt_strokes): + nb_points = len(stroke.points) - ## Set collection visibility - intercol = bpy.data.collections.get('interpolation_tool') - vl_col = bpy.context.view_layer.layer_collection.children.get(intercol.name) - intercol.hide_viewport = vl_col.exclude = vl_col.hide_viewport = False + local_co = np.empty(nb_points * 3, dtype='float64') + stroke.points.foreach_get('co', local_co) + # local_co_3d = local_co.reshape((nb_points, 3)) + world_co_3d = matrix_transform(local_co.reshape((nb_points, 3)), matrix) - # Override collection - col = intercol - - dg = bpy.context.evaluated_depsgraph_get() - self.strokes_data = [] + stroke_data = [] + for i, point in enumerate(stroke.points): + point_co_world = world_co_3d[i] + if target_obj: + object_hit, hit_location, tri, tri_indices = obj_ray_cast(target_obj, Vector(point_co_world), origin, dg) + else: + # scene raycast + object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg) - for si, stroke in enumerate(tgt_strokes): - nb_points = len(stroke.points) + ## Increasing search range + if not object_hit: # or object_hit not in col.all_objects[:]: + found = False + for iteration in range(1, 6): + for square_co in search_square(point_co_world, factor=self.settings.search_range * iteration): - local_co = np.empty(nb_points * 3, dtype='float64') - stroke.points.foreach_get('co', local_co) - # local_co_3d = local_co.reshape((nb_points, 3)) - world_co_3d = matrix_transform(local_co.reshape((nb_points, 3)), matrix) + if target_obj: + object_hit, hit_location, tri, tri_indices = obj_ray_cast(target_obj, Vector(square_co), origin, dg) + else: + # scene raycast + object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg) - stroke_data = [] - for i, point in enumerate(stroke.points): - point_co_world = world_co_3d[i] - if target_obj: - object_hit, hit_location, tri, tri_indices = obj_ray_cast(target_obj, Vector(point_co_world), origin, dg) - else: - # scene raycast - object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg) - - ## Increasing search range - if not object_hit: # or object_hit not in col.all_objects[:]: - found = False - for iteration in range(1, 6): - for square_co in search_square(point_co_world, factor=self.settings.search_range * iteration): - - if target_obj: - object_hit, hit_location, tri, tri_indices = obj_ray_cast(target_obj, Vector(square_co), origin, dg) - else: - # scene raycast - object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg) - - if object_hit: # and object_hit in col.all_objects[:]: - hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri)) - found = True - # print(f'{si}:{i} iteration {iteration}') # Dbg - break - - if found: + if object_hit: # and object_hit in col.all_objects[:]: + hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri)) + found = True + # print(f'{si}:{i} iteration {iteration}') # Dbg break - if not found: - ## /!\ ERROR ! No surface found! - # For debugging, select only problematic stroke (and point) - for sid, s in enumerate(tgt_strokes): - s.select = sid == si - for ip, p in enumerate(stroke.points): - p.select = ip == i - self.report({'ERROR'}, f'Stroke {si} point {i} could not find underlying geometry') - return {'CANCELLED'} - - stroke_data.append((stroke, point_co_world, object_hit, hit_location, tri, tri_indices)) + if found: + break - self.strokes_data.append(stroke_data) + if not found: + ## /!\ ERROR ! No surface found! + # For debugging, select only problematic stroke (and point) + for sid, s in enumerate(tgt_strokes): + s.select = sid == si + for ip, p in enumerate(stroke.points): + p.select = ip == i + self.exit_modal(context, status='ERROR', text=f'Stroke {si} point {i} could not find underlying geometry') + return {'CANCELLED'} + + stroke_data.append((stroke, point_co_world, object_hit, hit_location, tri, tri_indices)) - if self.debug: - self.scan_time = time()-self.start - print(f'Scan time {self.scan_time:.4f}s') - - # Copy stroke selection - bpy.ops.gpencil.copy() + self.strokes_data.append(stroke_data) - # Jump frame and paste - bpy.context.window_manager.progress_begin(self.frames_to_jump[0], self.frames_to_jump[-1]) # Pgs - self.status = 'LOOP' + if self.debug: + self.scan_time = time()-self.start + print(f'Scan time {self.scan_time:.4f}s') + + # Copy stroke selection + bpy.ops.gpencil.copy() - elif self.status == 'LOOP': + # Jump frame and paste + bpy.context.window_manager.progress_begin(self.frames_to_jump[0], self.frames_to_jump[-1]) # Pgs + self.status = 'LOOP' + + + ## -- LOOPTIMER + if event.type == 'TIMER': + + if self.status == 'LOOP': f = self.frames_to_jump[self.loop_count] bpy.context.window_manager.progress_update(f) # Pgs scn = bpy.context.scene @@ -385,8 +412,11 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): self.loop_count += 1 if self.loop_count >= len(self.frames_to_jump): - self.exit_modal() + self.exit_modal(context) return {'FINISHED'} + + bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + # context.area.tag_redraw() return {'RUNNING_MODAL'} diff --git a/interpolate_strokes/properties.py b/interpolate_strokes/properties.py index 4cb87a0..2ac2356 100644 --- a/interpolate_strokes/properties.py +++ b/interpolate_strokes/properties.py @@ -45,7 +45,7 @@ class GP_PG_interpolate_settings(PropertyGroup): items= ( ('FRAME', 'Frame', 'prev/next scene frame depending on the padding options', 0), ('GPKEY', 'GP Key', 'prev/next Grease pencil key', 1) , - ('RIGKEY', 'Rig Key', 'prev/next armatures keys in targeted collection', 2), + ('RIGKEY', 'Rig Key', 'prev/next armatures keys in targeted collection (camera keys are included)', 2), ), default='FRAME', description='Select how the previous or next frame should be chosen' @@ -59,8 +59,8 @@ class GP_PG_interpolate_settings(PropertyGroup): target_collection : PointerProperty( name='Collection', type=bpy.types.Collection, - description='Target collection to check armature keyframes from', - # placeholder='Collection' + description='Target collection to get armature keyframes\ + \nNothing = every armature in sccene', ) target_rig : PointerProperty( diff --git a/ui.py b/ui.py index c7cd0b7..7754049 100755 --- a/ui.py +++ b/ui.py @@ -6,9 +6,9 @@ class GP_PT_interpolate(bpy.types.Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' - @classmethod - def poll(cls, context): - return context.object # and context.object.type == 'GPENCIL' + # @classmethod + # def poll(cls, context): + # return context.object def draw(self, context): settings = bpy.context.scene.gp_interpo_settings @@ -22,10 +22,20 @@ class GP_PT_interpolate(bpy.types.Panel): prev_icon, next_icon = 'FRAME_PREV', 'FRAME_NEXT' else: prev_icon, next_icon = 'PREV_KEYFRAME', 'NEXT_KEYFRAME' + + prev_text = next_text = '' + scn = context.scene + if settings.use_animation: + prev_icon, next_icon = 'PLAY_REVERSE', 'PLAY' + ## Show animation range: + prev_text = f'{scn.frame_preview_start if scn.use_preview_range else scn.frame_start} < {scn.frame_current}' + next_text = f'{scn.frame_current} > {scn.frame_preview_end if scn.use_preview_range else scn.frame_end}' + + row = col.row(align=True) row.scale_x = 3 - row.operator("gp.interpolate_stroke", text="", icon=prev_icon).next = False - row.operator("gp.interpolate_stroke", text="", icon=next_icon).next = True + row.operator("gp.interpolate_stroke", text=prev_text, icon=prev_icon).next = False + row.operator("gp.interpolate_stroke", text=next_text, icon=next_icon).next = True col.prop(settings, 'use_animation', text='Animation') col.prop(settings, 'method', text='Method') @@ -39,7 +49,6 @@ class GP_PT_interpolate(bpy.types.Panel): col.prop(settings, 'use_bone_rotation', text='Use Bone Rotation') elif settings.method == 'GEOMETRY': - col.prop(settings, 'target_collection', text='Collection') col.prop(settings, 'search_range') col.prop(settings, 'remove_occluded') @@ -53,7 +62,9 @@ class GP_PT_interpolate(bpy.types.Panel): if settings.mode == 'FRAME': col.prop(settings, 'padding') - + + if settings.mode == 'RIGKEY': + col.prop(settings, 'target_collection', text='Collection') ## Parent still full WIP # col = layout.column() # col.label(text='Layer Parent') diff --git a/utils.py b/utils.py index 78e1ffd..67493f9 100644 --- a/utils.py +++ b/utils.py @@ -191,6 +191,8 @@ def plane_on_bone(bone, arm=None, cam=None, set_rotation=True, mesh=True): plane = bpy.data.objects.get('interpolation_plane') if not plane: plane = create_plane(name='interpolation_plane') + # Display type as Wire for a discrete XP + plane.display_type = 'WIRE' if plane.name not in col.objects: col.objects.link(plane) @@ -394,18 +396,18 @@ def following_keys(forward=True, all_keys=False) -> list:# -> list[int] | list | direction = 1 if forward else -1 cur_frame = bpy.context.scene.frame_current settings = bpy.context.scene.gp_interpo_settings - + + scn = bpy.context.scene + if forward: + limit = scn.frame_preview_end if scn.use_preview_range else scn.frame_end + else: + limit = scn.frame_preview_start if scn.use_preview_range else scn.frame_start + + frames = [] if settings.mode == 'FRAME': jump = settings.padding * direction if all_keys: - scn = bpy.context.scene - if forward: - limit = scn.frame_preview_end if scn.use_preview_range else scn.frame_end - else: - limit = scn.frame_preview_start if scn.use_preview_range else scn.frame_start - limit += direction # offset by one for limit to be in range - return list(range(cur_frame + jump , limit, jump)) else: @@ -419,10 +421,17 @@ def following_keys(forward=True, all_keys=False) -> list:# -> list[int] | list | col = settings.target_collection if not col: col = bpy.context.scene.collection - for arm in [o for o in col.all_objects if o.type == 'ARMATURE']: - if not arm.animation_data or not arm.animation_data.action: + + objs = [o for o in col.all_objects if o.type == 'ARMATURE'] + # Add camera moves detection + objs += [bpy.context.scene.camera] + + for obj in objs: + print(obj.name) + if not obj.animation_data or not obj.animation_data.action: continue - frames = [k.co.x for fc in arm.animation_data.action.fcurves for k in fc.keyframe_points] + frames += [k.co.x for fc in obj.animation_data.action.fcurves for k in fc.keyframe_points] + if not frames: return [] @@ -433,9 +442,9 @@ def following_keys(forward=True, all_keys=False) -> list:# -> list[int] | list | if all_keys: frames = list(set(frames)) if forward: - frame_list = [int(f) for f in frames if f > cur_frame] + frame_list = [int(f) for f in frames if f > cur_frame and f <= limit] else: - frame_list = [int(f) for f in frames if f < cur_frame] + frame_list = [int(f) for f in frames if f < cur_frame and f >= limit] return frame_list if forward: