diff --git a/__init__.py b/__init__.py index 5e24ae2..d775141 100755 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "gp interpolate", "author": "Christophe Seux, Samuel Bernou", - "version": (0, 2, 0), + "version": (0, 2, 1), "blender": (3, 6, 0), "location": "Sidebar > Gpencil Tab > Interpolate", "description": "Interpolate Grease pencil strokes over 3D", diff --git a/interpolate_strokes/__init__.py b/interpolate_strokes/__init__.py index 8e29173..40a5b45 100644 --- a/interpolate_strokes/__init__.py +++ b/interpolate_strokes/__init__.py @@ -1,8 +1,11 @@ -from gp_interpolate.interpolate_strokes import operators, properties +from gp_interpolate.interpolate_strokes import (operators, + properties, + interpolate_simple) modules = ( properties, operators, + interpolate_simple, ) if "bpy" in locals(): diff --git a/interpolate_strokes/interpolate_simple.py b/interpolate_strokes/interpolate_simple.py new file mode 100755 index 0000000..458e9f7 --- /dev/null +++ b/interpolate_strokes/interpolate_simple.py @@ -0,0 +1,227 @@ +import bpy +import numpy as np +from time import perf_counter, time +from mathutils import Vector, Matrix + +from gp_interpolate.utils import (matrix_transform, + plane_on_bone, + ray_cast_point, + intersect_with_tesselated_plane, + triangle_normal, + search_square, + get_gp_draw_plane, + create_plane, + following_keys, + attr_set) + + +from mathutils.geometry import (barycentric_transform, + intersect_point_tri, + intersect_point_line, + intersect_line_plane, + tessellate_polygon) + +## /!\ Old code kept for testing +## use pseudo plane coordinate instead of rayvast on real mesh plane + + +class GP_OT_interpolate_stroke_simple(bpy.types.Operator): + bl_idname = "gp.interpolate_stroke_simple" + bl_label = "Interpolate Stroke Simple" + bl_description = 'Interpolate Stroke Simple' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if context.active_object and context.object.type == 'GPENCIL'\ + and context.mode in ('EDIT_GPENCIL', 'SCULPT_GPENCIL', 'PAINT_GPENCIL'): + return True + cls.poll_message_set("Need a Grease pencil object in Edit or Sculpt mode") + return False + + @classmethod + def description(cls, context, properties): + if properties.next: + return f"Interpolate Stroke Forward" + else: + return f"Interpolate Stroke Backward" + + next : bpy.props.BoolProperty(name='Next', default=True, options={'SKIP_SAVE'}) + + def execute(self, context): + settings = context.scene.gp_interpo_settings + + auto_key_status = context.tool_settings.use_keyframe_insert_auto + context.tool_settings.use_keyframe_insert_auto = True + + ## Determine on what key to jump + frames_to_jump = following_keys(forward=self.next) + if not frames_to_jump: + self.report({'WARNING'}, 'No keyframe available in this direction') + return {'CANCELLED'} + + frames_to_jump = frames_to_jump[0] + + gp = context.object + + scn = bpy.context.scene + dg = bpy.context.evaluated_depsgraph_get() + matrix = np.array(gp.matrix_world, dtype='float64')#.inverted() + col = settings.target_collection + if not col: + col = scn.collection + + origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') + # print('----') + + tgt_strokes = [s for s in 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 gp.data.layers.active.active_frame.strokes: + s.select = True + tgt_strokes = gp.data.layers.active.active_frame.strokes + + if not tgt_strokes: + self.report({'ERROR'}, 'No stroke selected !') + return {'CANCELLED'} + + strokes_data = [] + if settings.method == 'BONE': + ## Follow Bone method (Full WIP) + if not settings.target_rig or not settings.target_bone: + self.report({'ERROR'}, 'No Bone selected') + return {'CANCELLED'} + + bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), + arm=settings.target_rig, + set_rotation=settings.use_bone_rotation) + + strokes_data = [] + for stroke in tgt_strokes: + nb_points = len(stroke.points) + 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) + + stroke_data = [] + + for i, point in enumerate(stroke.points): + point_co_world = world_co_3d[i] + hit_location, tri, tri_indices = intersect_with_tesselated_plane(point_co_world, origin, bone_plane) + ## Probably easier to just generate a single vast triangle and use it + + ## Store same as other method (without object hit) + stroke_data.append((stroke, point_co_world, hit_location, tri, tri_indices)) + + strokes_data.append(stroke_data) + + else: + ## Geometry method + for stroke in tgt_strokes: + nb_points = len(stroke.points) + + 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) + + stroke_data = [] + for i, point in enumerate(stroke.points): + point_co_world = world_co_3d[i] + + object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg) + if not object_hit or object_hit not in col.all_objects[:]: + for square_co in search_square(point_co_world, factor=settings.search_range): + object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, 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)) + + break + + stroke_data.append((stroke, point_co_world, object_hit, hit_location, tri, tri_indices)) + + strokes_data.append(stroke_data) + + # Copy stroke selection, jump frame and paste + + bpy.ops.gpencil.copy() + + scn.frame_set(frames_to_jump) + + plan_co, plane_no = get_gp_draw_plane(gp) + + bpy.ops.gpencil.paste() + + + + if settings.method == 'BONE': + matrix_inv = np.array(gp.matrix_world.inverted(), dtype='float64')#.inverted() + new_strokes = gp.data.layers.active.active_frame.strokes[-len(strokes_data):] + + bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), + arm=settings.target_rig, + set_rotation=settings.use_bone_rotation) + + for new_stroke, stroke_data in zip(new_strokes, strokes_data): + world_co_3d = [] # np.array(len()dtype='float64')#np. + for stroke, point_co, hit_location, tri_a, tri_indices in stroke_data: + tri_b = [bone_plane[i] for i in tri_indices] + # tri_b = matrix_transform(tri_b, settings.target_rig.matrix_world) + ## rotate tri_b by bone differential angle camera's aim axis ? + + new_loc = barycentric_transform(hit_location, *tri_a, *tri_b) + world_co_3d.append(new_loc) + + + # Reproject on plane + new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d] + new_local_co_3d = matrix_transform(new_world_co_3d, matrix_inv) + + nb_points = len(new_stroke.points) + new_stroke.points.foreach_set('co', new_local_co_3d.reshape(nb_points*3)) + new_stroke.points.update() + + else: + dg = bpy.context.evaluated_depsgraph_get() + + matrix_inv = np.array(gp.matrix_world.inverted(), dtype='float64')#.inverted() + new_strokes = gp.data.layers.active.active_frame.strokes[-len(strokes_data):] + + for new_stroke, stroke_data in zip(new_strokes, strokes_data): + world_co_3d = [] # np.array(len()dtype='float64')#np. + for stroke, point_co, object_hit, hit_location, tri_a, tri_indices in stroke_data: + eval_ob = object_hit.evaluated_get(dg) + tri_b = [eval_ob.data.vertices[i].co for i in tri_indices] + tri_b = matrix_transform(tri_b, eval_ob.matrix_world) + + new_loc = barycentric_transform(hit_location, *tri_a, *tri_b) + world_co_3d.append(new_loc) + + # Reproject on plane + new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d] + new_local_co_3d = matrix_transform(new_world_co_3d, matrix_inv) + + nb_points = len(new_stroke.points) + new_stroke.points.foreach_set('co', new_local_co_3d.reshape(nb_points*3)) + new_stroke.points.update() + + ## Reset autokey status + context.tool_settings.use_keyframe_insert_auto = auto_key_status + + return {'FINISHED'} + +classes = ( + GP_OT_interpolate_stroke_simple, +) + +def register(): + for c in classes: + bpy.utils.register_class(c) + + +def unregister(): + for c in reversed(classes): + bpy.utils.unregister_class(c) diff --git a/interpolate_strokes/operators.py b/interpolate_strokes/operators.py index 7b77866..e34c4ed 100644 --- a/interpolate_strokes/operators.py +++ b/interpolate_strokes/operators.py @@ -1,6 +1,6 @@ import bpy import numpy as np -from time import perf_counter +from time import perf_counter, time from mathutils import Vector, Matrix from gp_interpolate.utils import (matrix_transform, @@ -11,6 +11,7 @@ from gp_interpolate.utils import (matrix_transform, search_square, get_gp_draw_plane, create_plane, + following_keys, attr_set) from mathutils.geometry import (barycentric_transform, @@ -20,63 +21,6 @@ from mathutils.geometry import (barycentric_transform, tessellate_polygon) -def following_keys(forward=True, all_keys=True) -> list:# -> list[int] | list | None: - '''return a lsit of int or an empty list''' - direction = 1 if forward else -1 - cur_frame = bpy.context.scene.frame_current - settings = bpy.context.scene.gp_interpo_settings - - if settings.mode == 'FRAME': - 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, limit, settings.padding * direction)) - - else: - return [cur_frame + (settings.padding * direction)] - - elif settings.mode == 'GPKEY': - layers = bpy.context.object.data.layers - frames = [f.frame_number for l in layers for f in l.frames] - - elif settings.mode == 'RIGKEY': - 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: - continue - frames = [k.co.x for fc in arm.animation_data.action.fcurves for k in fc.keyframe_points] - - if not frames: - return [] - - # Sort frames (invert if looking backward) - frames.sort(reversed=not forward) - - if all_keys: - frames = list(set(frames)) - if forward: - frame_list = [int(f) for f in frames if f > cur_frame] - else: - frame_list = [int(f) for f in frames if f < cur_frame] - return frame_list - - if forward: - new = next((f for f in frames if f > cur_frame), None) - else: - new = next((f for f in frames if f < cur_frame), None) - if new is None: - return [] - return [int(new)] - - ## TODO: add bake animation to empty for later GP layer parenting class GP_OT_interpolate_stroke(bpy.types.Operator): @@ -118,12 +62,12 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): gp = context.object matrix = np.array(gp.matrix_world, dtype='float64')#.inverted() + origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') col = settings.target_collection if not col: col = scn.collection - origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') # print('----') tgt_strokes = [s for s in gp.data.layers.active.active_frame.strokes if s.select] @@ -139,7 +83,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): return {'CANCELLED'} included_cols = [c.name for c in gp.users_collection] - + start = time() if settings.method == 'BONE': ## Follow Bone method (WIP) if not settings.target_rig or not settings.target_bone: @@ -186,14 +130,17 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): (vlc, 'exclude', vlc.name not in included_cols), # (vlc, 'hide_viewport', vlc.name not in included_cols), # viewport viz ) + + print(f'Preparation {time()-start:.4f}s') with attr_set(store_list): if settings.method == 'BONE': ## replace plane - bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), + _bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), arm=settings.target_rig, set_rotation=settings.use_bone_rotation, mesh=True) + ## Set collection visibility intercol = bpy.data.collections.get('interpolation_tool') vl_col = bpy.context.view_layer.layer_collection.children.get(intercol.name) @@ -206,7 +153,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): dg = bpy.context.evaluated_depsgraph_get() strokes_data = [] - for stroke in tgt_strokes: + for si, stroke in enumerate(tgt_strokes): nb_points = len(stroke.points) local_co = np.empty(nb_points * 3, dtype='float64') @@ -216,29 +163,71 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): stroke_data = [] for i, point in enumerate(stroke.points): + # print(si, i) point_co_world = world_co_3d[i] object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg) + ## + # try: + # object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg) + # except Exception as e: + # print(f'Error on first {si}:{i}') + # self.report({'ERROR'}, f'Error on first {si}:{i}') + # for p in stroke.points: + # p.select = False + # stroke.points[i].select = True + # print(e) + # return {'CANCELLED'} + + ## with one extra search if not object_hit or object_hit not in col.all_objects[:]: for square_co in search_square(point_co_world, factor=settings.search_range): object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, 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)) - break + + ### with 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=settings.search_range * iteration): + + # object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, 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 + # context.scene.cursor.location = square_co + # break + + # if found: + # 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)) strokes_data.append(stroke_data) - # Copy stroke selection, jump frame and paste - + scan_time = time()-start + print(f'Scan time {scan_time:.4f}s') + + # Copy stroke selection bpy.ops.gpencil.copy() + # Jump frame and paste wm = bpy.context.window_manager # Pgs - wm.progress_begin(frames_to_jump[0], frames_to_jump[-1]) # Pgs + for f in frames_to_jump: wm.progress_update(f) # Pgs scn.frame_set(f) @@ -256,13 +245,22 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): new_strokes = gp.data.layers.active.active_frame.strokes[-len(strokes_data):] for new_stroke, stroke_data in zip(new_strokes, strokes_data): - world_co_3d = [] # np.array(len()dtype='float64')#np. + world_co_3d = [] for stroke, point_co, object_hit, hit_location, tri_a, tri_indices in stroke_data: eval_ob = object_hit.evaluated_get(dg) tri_b = [eval_ob.data.vertices[i].co for i in tri_indices] tri_b = matrix_transform(tri_b, eval_ob.matrix_world) - + new_loc = barycentric_transform(hit_location, *tri_a, *tri_b) + # try: + # new_loc = barycentric_transform(hit_location, *tri_a, *tri_b) + # except Exception as e: + # print(f'\nCould not apply barycentric tranform {eval_ob.name}') + # print(e) + # bpy.context.scene.cursor.location = hit_location + # self.report({'ERROR'}, f'Stroke {si} point {i} could not find underlying geometry') + # return {'CANCELLED'} + world_co_3d.append(new_loc) # Reproject on plane @@ -274,200 +272,15 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): new_stroke.points.update() wm.progress_end() # Pgs - ## Reset autokey status - # context.tool_settings.use_keyframe_insert_auto = auto_key_status # (Done in context manager) + ## TODO: Remove plane on the fly - + print(f"Paste'n'place time {time()-start - scan_time}s") + if len(frames_to_jump) > 1: + self.report({'INFO'}, f'{len(frames_to_jump)} interpolated frame(s) ({time()-start:.3f}s)') + print('Done') return {'FINISHED'} -""" -class GP_OT_interpolate_stroke(bpy.types.Operator): - bl_idname = "gp.interpolate_stroke" - bl_label = "Interpolate Stroke" - bl_description = 'Interpolate Stroke' - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - if context.active_object and context.object.type == 'GPENCIL'\ - and context.mode in ('EDIT_GPENCIL', 'SCULPT_GPENCIL', 'PAINT_GPENCIL'): - return True - cls.poll_message_set("Need a Grease pencil object in Edit or Sculpt mode") - return False - - @classmethod - def description(cls, context, properties): - if properties.next: - return f"Interpolate Stroke Forward" - else: - return f"Interpolate Stroke Backward" - - next : bpy.props.BoolProperty(name='Next', default=True, options={'SKIP_SAVE'}) - - def execute(self, context): - settings = context.scene.gp_interpo_settings - - auto_key_status = context.tool_settings.use_keyframe_insert_auto - context.tool_settings.use_keyframe_insert_auto = True - - ## Determine on what key to jump - frames_to_jump = following_keys(forward=self.next) - if frames_to_jump is None: - self.report({'WARNING'}, 'No keyframe available in this direction') - return {'CANCELLED'} - - gp = context.object - - scn = bpy.context.scene - dg = bpy.context.evaluated_depsgraph_get() - matrix = np.array(gp.matrix_world, dtype='float64')#.inverted() - col = settings.target_collection - if not col: - col = scn.collection - - origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') - # print('----') - - tgt_strokes = [s for s in 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 gp.data.layers.active.active_frame.strokes: - s.select = True - tgt_strokes = gp.data.layers.active.active_frame.strokes - - if not tgt_strokes: - self.report({'ERROR'}, 'No stroke selected !') - return {'CANCELLED'} - - strokes_data = [] - if settings.method == 'BONE': - ## Follow Bone method (Full WIP) - if not settings.target_rig or not settings.target_bone: - self.report({'ERROR'}, 'No Bone selected') - return {'CANCELLED'} - - bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), - arm=settings.target_rig, - set_rotation=settings.use_bone_rotation) - - strokes_data = [] - for stroke in tgt_strokes: - nb_points = len(stroke.points) - 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) - - stroke_data = [] - - for i, point in enumerate(stroke.points): - point_co_world = world_co_3d[i] - hit_location, tri, tri_indices = intersect_with_tesselated_plane(point_co_world, origin, bone_plane) - ## probably easier to just generate a single vast triangle and use it - - ## Store same as other method (without object hit) - stroke_data.append((stroke, point_co_world, hit_location, tri, tri_indices)) - - strokes_data.append(stroke_data) - - else: - ## Geometry method - for stroke in tgt_strokes: - nb_points = len(stroke.points) - - 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) - - stroke_data = [] - for i, point in enumerate(stroke.points): - point_co_world = world_co_3d[i] - - object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg) - if not object_hit or object_hit not in col.all_objects[:]: - for square_co in search_square(point_co_world, factor=settings.search_range): - object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, 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)) - - break - - stroke_data.append((stroke, point_co_world, object_hit, hit_location, tri, tri_indices)) - - strokes_data.append(stroke_data) - - # Copy stroke selection, jump frame and paste - - bpy.ops.gpencil.copy() - - scn.frame_set(frames_to_jump) - - plan_co, plane_no = get_gp_draw_plane(gp) - - bpy.ops.gpencil.paste() - - - - if settings.method == 'BONE': - matrix_inv = np.array(gp.matrix_world.inverted(), dtype='float64')#.inverted() - new_strokes = gp.data.layers.active.active_frame.strokes[-len(strokes_data):] - - bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), - arm=settings.target_rig, - set_rotation=settings.use_bone_rotation) - - for new_stroke, stroke_data in zip(new_strokes, strokes_data): - world_co_3d = [] # np.array(len()dtype='float64')#np. - for stroke, point_co, hit_location, tri_a, tri_indices in stroke_data: - tri_b = [bone_plane[i] for i in tri_indices] - # tri_b = matrix_transform(tri_b, settings.target_rig.matrix_world) - ## rotate tri_b by bone differential angle camera's aim axis ? - - new_loc = barycentric_transform(hit_location, *tri_a, *tri_b) - world_co_3d.append(new_loc) - - - # Reproject on plane - new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d] - new_local_co_3d = matrix_transform(new_world_co_3d, matrix_inv) - - nb_points = len(new_stroke.points) - new_stroke.points.foreach_set('co', new_local_co_3d.reshape(nb_points*3)) - new_stroke.points.update() - - else: - dg = bpy.context.evaluated_depsgraph_get() - - matrix_inv = np.array(gp.matrix_world.inverted(), dtype='float64')#.inverted() - new_strokes = gp.data.layers.active.active_frame.strokes[-len(strokes_data):] - - for new_stroke, stroke_data in zip(new_strokes, strokes_data): - world_co_3d = [] # np.array(len()dtype='float64')#np. - for stroke, point_co, object_hit, hit_location, tri_a, tri_indices in stroke_data: - eval_ob = object_hit.evaluated_get(dg) - tri_b = [eval_ob.data.vertices[i].co for i in tri_indices] - tri_b = matrix_transform(tri_b, eval_ob.matrix_world) - - new_loc = barycentric_transform(hit_location, *tri_a, *tri_b) - world_co_3d.append(new_loc) - - # Reproject on plane - new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d] - new_local_co_3d = matrix_transform(new_world_co_3d, matrix_inv) - - nb_points = len(new_stroke.points) - new_stroke.points.foreach_set('co', new_local_co_3d.reshape(nb_points*3)) - new_stroke.points.update() - - ## Reset autokey status - context.tool_settings.use_keyframe_insert_auto = auto_key_status - - return {'FINISHED'} -""" classes = ( GP_OT_interpolate_stroke, diff --git a/interpolate_strokes/properties.py b/interpolate_strokes/properties.py index 24c15b5..c88bb72 100644 --- a/interpolate_strokes/properties.py +++ b/interpolate_strokes/properties.py @@ -24,14 +24,14 @@ class GP_PG_interpolate_settings(PropertyGroup): description='Select method for interpolating strokes' ) - use_animation : BoolProperty( - name='Animatation', - default=True, - description='Apply the interpolation on the remaining range') + use_animation : BoolProperty(name='Animation', + default=False, + description='Apply the interpolation on the all keys forward or backward') search_range : FloatProperty( name="Search Range", - description="Search range size when points are out of mesh", + description="Search range size when points are out of mesh\ + \nThe value is as percentage of the camera width", default=0.05, precision=2, step=3, options={'HIDDEN'}) diff --git a/ui.py b/ui.py index 6b773f2..c99eef6 100755 --- a/ui.py +++ b/ui.py @@ -17,7 +17,7 @@ class GP_PT_interpolate(bpy.types.Panel): layout.use_property_split = True col = layout.column(align=False) - ## interpolation buttons + ## Interpolation buttons if settings.mode == 'FRAME': prev_icon, next_icon = 'FRAME_PREV', 'FRAME_NEXT' else: @@ -26,7 +26,13 @@ class GP_PT_interpolate(bpy.types.Panel): 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 - # col.separator() + + ## Old version to test (TODO: delete later) + # col.label(text='Test Old Ops') + # row = col.row(align=True) + # row.scale_x = 3 + # row.operator("gp.interpolate_stroke_simple", text="", icon=prev_icon).next = False + # row.operator("gp.interpolate_stroke_simple", text="", icon=next_icon).next = True col.prop(settings, 'method', text='Method') diff --git a/utils.py b/utils.py index c4d8d40..e26d7f1 100644 --- a/utils.py +++ b/utils.py @@ -133,6 +133,8 @@ def plane_on_bone(bone, arm=None, cam=None, set_rotation=True, mesh=True): else: ## Use mid bone to better follow movement mat.translation = arm.matrix_world @ ((bone.tail + bone.head) / 2) # Mid bone + + mat_scale = Matrix.Scale(10, 4) # maybe move above mesh condition if mesh: # get/create collection @@ -155,7 +157,6 @@ def plane_on_bone(bone, arm=None, cam=None, set_rotation=True, mesh=True): return plane plane = plane_coords() - mat_scale = Matrix.Scale(10, 4) # maybe move above mesh condition return matrix_transform(plane, mat @ mat_scale) def create_plane(name='Plane', collection=None): @@ -274,4 +275,63 @@ def get_gp_draw_plane(obj=None): plane_no.rotate(mat) - return plane_co, plane_no \ No newline at end of file + return plane_co, plane_no + + +## --- Animation + +def following_keys(forward=True, all_keys=False) -> list:# -> list[int] | list | None: + '''return a lsit of int or an empty list''' + direction = 1 if forward else -1 + cur_frame = bpy.context.scene.frame_current + settings = bpy.context.scene.gp_interpo_settings + + if settings.mode == 'FRAME': + 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, limit, settings.padding * direction)) + + else: + return [cur_frame + (settings.padding * direction)] + + elif settings.mode == 'GPKEY': + layers = bpy.context.object.data.layers + frames = [f.frame_number for l in layers for f in l.frames] + + elif settings.mode == 'RIGKEY': + 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: + continue + frames = [k.co.x for fc in arm.animation_data.action.fcurves for k in fc.keyframe_points] + + if not frames: + return [] + + # Sort frames (invert if looking backward) + frames.sort(reversed=not forward) + + if all_keys: + frames = list(set(frames)) + if forward: + frame_list = [int(f) for f in frames if f > cur_frame] + else: + frame_list = [int(f) for f in frames if f < cur_frame] + return frame_list + + if forward: + new = next((f for f in frames if f > cur_frame), None) + else: + new = next((f for f in frames if f < cur_frame), None) + if new is None: + return [] + return [int(new)]