diff --git a/CHANGELOG.md b/CHANGELOG.md index 444ba6f..912ca87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +1.5.0 + +- changed: no need to have a cycle on fcurve to bake keys anymore +- changed: snap curve does not create a curve copy +- added: allow to directly snap selected curve (`ctrl + Click` to keep shrinkwarp modfifier, need to apply to affect object) +- fixed: error when going in curve edit from object mode + 1.4.4 - changed: default start to 100 diff --git a/OP_expand_cycle_step.py b/OP_expand_cycle_step.py index cf39d86..898957c 100644 --- a/OP_expand_cycle_step.py +++ b/OP_expand_cycle_step.py @@ -36,22 +36,18 @@ def bake_cycle(on_selection=True, end=None): last = max(all_keys) # int(max(all_keys)) offset = last - first - for fcu in act.fcurves: - - ## if a curve is not cycled don't touch - if not [m for m in fcu.modifiers if m.type == 'CYCLES']: - ct_no_cycle += 1 - continue - - if debug: print(fcu.data_path, 'has cycle') + for fcu in fn.get_only_pose_keyable_fcurves(obj, action=act): + #-# old -- filter only on fcurve that have a cycle modifier (maybe as an option) + # if not [m for m in fcu.modifiers if m.type == 'CYCLES']: + # ct_no_cycle += 1 + # continue + #-# only on location : # if not fcu.data_path.endswith('.location'): # continue - # prop = fcu.data_path.split('.')[-1] - b_name = fcu.data_path.split('"')[1] - if debug: print(b_name, 'has cycle') + pb = obj.pose.bones.get(b_name) if not pb: print(f'{b_name} is invalid') diff --git a/OP_setup_curve_path.py b/OP_setup_curve_path.py index 0a2bae3..0e02812 100644 --- a/OP_setup_curve_path.py +++ b/OP_setup_curve_path.py @@ -104,7 +104,7 @@ class AW_OT_remove_follow_path(bpy.types.Operator): class AW_OT_snap_curve_to_ground(bpy.types.Operator): bl_idname = "autowalk.snap_curve_to_ground" bl_label = "Snap Curve" - bl_description = "snap curve to ground determine in field" + bl_description = "Snap curve to ground determine in field" bl_options = {"REGISTER", "UNDO"} @classmethod @@ -119,6 +119,34 @@ class AW_OT_snap_curve_to_ground(bpy.types.Operator): return {"CANCELLED"} return {"FINISHED"} +class AW_OT_snap_selected_curve(bpy.types.Operator): + bl_idname = "autowalk.snap_selected_curve" + bl_label = "Snap Selected Curve" + bl_description = "Snap selected curve to ground\ + \nCtrl + Click : Not apply Shrinkwarp modifier (Apply manually)" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'CURVE' + + def invoke(self, context, event): + self.apply = not event.ctrl # don't apply if ctrl is pressd + return self.execute(context) + + def execute(self, context): + ob = context.object + # bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + ground = fn.get_gnd() + if not ground: + self.report({'ERROR'}, 'Need to specify ground (in curve options) or name an object in scene "Ground"') + return {"CANCELLED"} + fn.shrinkwrap_on_object(ob, ground, apply=self.apply) + if self.apply: + self.report({'INFO'}, 'ShrinkWrap Modifier need to be applyed manually') + + return {"FINISHED"} + class AW_OT_edit_curve(bpy.types.Operator): bl_idname = "autowalk.edit_curve" bl_label = "Edit Curve" @@ -130,6 +158,7 @@ class AW_OT_edit_curve(bpy.types.Operator): return context.object and context.object.type == 'ARMATURE' def execute(self, context): + ob = context.object b = context.active_pose_bone curve = None @@ -139,7 +168,7 @@ class AW_OT_edit_curve(bpy.types.Operator): # get from 'root' bone if not curve: - curve, _const = fn.get_follow_curve_from_armature(context.object) + curve, _const = fn.get_follow_curve_from_armature(ob) if isinstance(curve, str): self.report({curve}, _const) if curve == 'ERROR': @@ -150,7 +179,9 @@ class AW_OT_edit_curve(bpy.types.Operator): # curve context.mode -> EDIT_CURVE # Deselect armature object - b.id_data.select_set(False) + + # b.id_data.select_set(False) + ob.select_set(False) return {"FINISHED"} class AW_OT_go_to_object(bpy.types.Operator): @@ -235,6 +266,7 @@ AW_OT_create_curve_path, AW_OT_create_follow_path, AW_OT_remove_follow_path, AW_OT_snap_curve_to_ground, +AW_OT_snap_selected_curve, AW_OT_edit_curve, AW_OT_go_to_object, AW_OT_object_from_curve, # use set_choice_id is used to set an index in object_from_curve pop up menu diff --git a/__init__.py b/__init__.py index 95fdb55..aded596 100644 --- a/__init__.py +++ b/__init__.py @@ -4,7 +4,7 @@ bl_info = { "name": "Auto Walk", "description": "Develop a walk/run cycles along a curve and pin feets", "author": "Samuel Bernou", - "version": (1, 4, 4), + "version": (1, 5, 0), "blender": (3, 0, 0), "location": "View3D", "warning": "", diff --git a/fn.py b/fn.py index e56d4b3..ca56f02 100644 --- a/fn.py +++ b/fn.py @@ -389,66 +389,98 @@ def create_follow_path_constraint(ob, curve, follow_curve=True): const.use_curve_follow = True return curve, const -def snap_curve(): +def shrinkwrap_on_object(source, target, apply=True): + # shrinkwrap or cast on ground + mod = source.modifiers.new('Shrinkwrap', 'SHRINKWRAP') + # mod.wrap_method = 'TARGET_PROJECT' + mod.wrap_method = 'PROJECT' + mod.wrap_mode = 'ON_SURFACE' + mod.use_project_z = True + mod.use_negative_direction = True + mod.use_positive_direction = True + mod.target = target + + if apply: + # Apply and decimate + switch = False + if bpy.context.mode == 'EDIT_CURVE': + switch = True + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.modifier_apply({'object': source}, modifier="Shrinkwrap", report=False) + if switch: + bpy.ops.object.mode_set(mode='EDIT') + + +def snap_curve(create_copy=False): obj = bpy.context.object + gnd = get_gnd() + if not gnd: + return curve = const = None if obj.type == 'ARMATURE': curve, const = get_follow_curve_from_armature(obj) to_follow = bpy.context.scene.anim_cycle_settings.path_to_follow - if not curve and not to_follow: - return ('ERROR', f'No curve pointed by "Path" filed') - - # get curve from field - if not curve: - curve, const = create_follow_path_constraint(obj, to_follow) - if isinstance(curve, str): - return (curve, const) # those are error message - - # if obj.type == 'CURVE': - # return ('ERROR', f'Select the armature related to curve {obj.name}') - # else: - # return ('ERROR', 'Not an armature object') - - gnd = get_gnd() - if not gnd: - return + curve_act = None anim_frame = None - - # if it's on a snap curve, fetch original - if '_snap' in curve.name: - org_name = re.sub(r'_snap\.?\d{0,3}$', '', curve.name) - org_curve = bpy.context.scene.objects.get(org_name) - if org_curve: - const.target = org_curve + if create_copy: + # get curve from field + if not curve and not to_follow: + return ('ERROR', f'No curve pointed by "Path" filed') + + if not curve: + curve, const = create_follow_path_constraint(obj, to_follow) + if isinstance(curve, str): + return (curve, const) # those are error message + + # if obj.type == 'CURVE': + # return ('ERROR', f'Select the armature related to curve {obj.name}') + # else: + # return ('ERROR', 'Not an armature object') - # keep action - if curve.data.animation_data and curve.data.animation_data.action: - curve_act = curve.data.animation_data.action - anim_frame = curve.data.path_duration - # delete old snap - bpy.data.objects.remove(curve) - # assign old curve as main one - curve = org_curve + + # if it's on a snap curve, fetch original + if '_snap' in curve.name: + org_name = re.sub(r'_snap\.?\d{0,3}$', '', curve.name) + org_curve = bpy.context.scene.objects.get(org_name) - nc = curve.copy() - name = re.sub(r'\.\d{3}$', '', curve.name) + '_snap' - const.target = nc - nc.name = name - nc.data = curve.data.copy() - nc.data.name = name + '_data' - if curve_act: - nc.data.animation_data_create() - nc.data.animation_data.action = curve_act - if anim_frame: - nc.data.path_duration = anim_frame + if org_curve: + const.target = org_curve - curve.users_collection[0].objects.link(nc) + # keep action + if curve.data.animation_data and curve.data.animation_data.action: + curve_act = curve.data.animation_data.action + + anim_frame = curve.data.path_duration + # delete old snap + bpy.data.objects.remove(curve) + # assign old curve as main one + curve = org_curve + + nc = curve.copy() + name = re.sub(r'\.\d{3}$', '', curve.name) + '_snap' + const.target = nc + nc.name = name + nc.data = curve.data.copy() + nc.data.name = name + '_data' + + if curve_act: + nc.data.animation_data_create() + nc.data.animation_data.action = curve_act + if anim_frame: + nc.data.path_duration = anim_frame + + curve.users_collection[0].objects.link(nc) + + else: + if not curve: + return ('ERROR', 'Path not found') + nc = curve ## If object mode is Curve subdivide it (TODO if nurbs needs conversion) #-# subdivide the curve (if curve is not nurbs) @@ -458,18 +490,7 @@ def snap_curve(): # bpy.ops.curve.subdivide(number_cuts=4) # bpy.ops.object.mode_set(mode='OBJECT') - # shrinkwrap or cast on ground - mod = nc.modifiers.new('Shrinkwrap', 'SHRINKWRAP') - # mod.wrap_method = 'TARGET_PROJECT' - mod.wrap_method = 'PROJECT' - mod.wrap_mode = 'ON_SURFACE' - mod.use_project_z = True - mod.use_negative_direction = True - mod.use_positive_direction = True - mod.target = gnd - - # Apply and decimate - bpy.ops.object.modifier_apply({'object': nc}, modifier="Shrinkwrap", report=False) + shrinkwrap_on_object(nc, gnd) bpy.context.scene.anim_cycle_settings.path_to_follow = nc # return 0, nc @@ -646,10 +667,11 @@ def remove_all_cycles_modifier(ob=None): print(f'Remove cyclic modifiers on {ct} fcurve(s)') return ct -def create_cycle_modifiers(ob=None): - ob = ob or bpy.context.object +def get_only_pose_keyable_fcurves(ob, action=None): + '''Can action providing another action (must be for the same object)''' - # skip bones that are on protected layers ? + act = action or ob.animation_data.action + ## skip bones that are on protected layers ? # protected = [i for i, l in enumerate(ob.data.layers_protected) if l] # for b in ob.data.bones: # if b.use_deform: # don't affect deform bones @@ -657,24 +679,34 @@ def create_cycle_modifiers(ob=None): ## b_layers = [i for i, l in enumerate(b.layers) if l] name_list = [b.name for b in ob.data.bones] # if not b.use_deform (too limiting) - + fcus = [] re_prefix = re.compile(r'^(mch|def|org|vis|fld|ctp)[\._-]', flags=re.I) - ct = 0 - for fc in ob.animation_data.action.fcurves: - if [m for m in fc.modifiers if m.type == 'CYCLES']: - # skip already existing modifier - continue - if not '"' in fc.data_path: - continue + for fc in act.fcurves: # skip offset if fc.data_path.endswith('.offset') and 'constraint' in fc.data_path: continue + # skip fcus that are not bones + if not '"' in fc.data_path: + continue + b_name = fc.data_path.split('"')[1] + if b_name not in name_list: + continue + if re_prefix.match(b_name): continue + fcus.append(fc) + + return fcus - if b_name not in name_list: +def create_cycle_modifiers(ob=None): + ob = ob or bpy.context.object + ct = 0 + keyable_fcurves = get_only_pose_keyable_fcurves(ob) + for fc in keyable_fcurves: + if [m for m in fc.modifiers if m.type == 'CYCLES']: + # skip if already existing modifier continue # print(f'Adding cycle modifier {fc.data_path}') _m = fc.modifiers.new(type='CYCLES') diff --git a/panels.py b/panels.py index b83807b..64c3a6f 100644 --- a/panels.py +++ b/panels.py @@ -66,7 +66,13 @@ class AW_PT_walk_cycle_anim_panel(bpy.types.Panel): box.prop_search(settings, "gnd", context.scene, "objects") row = box.row() - row.operator('autowalk.snap_curve_to_ground', text='Snap curve to ground', icon='SNAP_ON') + if ob and ob.type == 'ARMATURE': + row.operator('autowalk.snap_curve_to_ground', text='Snap Curve To Ground', icon='SNAP_ON') + elif ob and ob.type == 'CURVE': + row.operator('autowalk.snap_selected_curve', text='Snap Selected Curve To Ground', icon='SNAP_ON') + else: + row.label(text='Select curve or armature to snap', icon='INFO') + row.active = bool(settings.gnd)