import bpy, re from . import fn ## step 1 : Create the curve and/or follow path constraint, snap to ground def create_follow_path_constraint(ob, curve, follow_curve=True): prefs = fn.get_addon_prefs() root_name = prefs.tgt_bone root = ob.pose.bones.get(root_name) if not root: return ('ERROR', f'posebone {root_name} not found in armature {ob.name} check addon preferences to change name') # Clear bone follow path constraint exiting_fp_constraints = [c for c in root.constraints if c.type == 'FOLLOW_PATH'] for c in exiting_fp_constraints: root.constraints.remove(c) # loc = ob.matrix_world @ root.matrix.to_translation() if root.name == ('world', 'root') and root.location != (0,0,0): old_loc = root.location root.location = (0,0,0) print(f'root moved from {old_loc} to (0,0,0) to counter follow curve offset') const = root.constraints.new('FOLLOW_PATH') const.target = curve # axis only in this case, should be in addon to prefs ## determine which axis to use... maybe found orientation in world space from matrix_basis ? root_world_base_direction = root.bone.matrix_local @ fn.get_direction_vector_from_enum(bpy.context.scene.anim_cycle_settings.forward_axis) const.forward_axis = fn.orentation_track_from_vector(root_world_base_direction) # 'TRACK_NEGATIVE_Y' # bpy.context.scene.anim_cycle_settings.forward_axis # 'FORWARD_X' print('const.forward_axis: ', const.forward_axis) const.use_curve_follow = True return curve, const def snap_curve(): obj = bpy.context.object curve = const = None if obj.type == 'ARMATURE': curve, const = fn.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 = fn.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 # 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) ## If object mode is Curve subdivide it (TODO if nurbs needs conversion) #-# subdivide the curve (if curve is not nurbs) # bpy.ops.object.mode_set(mode='EDIT') # bpy.ops.curve.select_all(action='SELECT') # 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) bpy.context.scene.anim_cycle_settings.path_to_follow = nc # return 0, nc class UAC_OT_create_curve_path(bpy.types.Operator): bl_idname = "anim.create_curve_path" bl_label = "Create Curve" bl_description = "Create curve and add follow path constraint" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return context.object and context.object.type == 'ARMATURE' def execute(self, context): # use root (or other specified bone) to find where to put the curve prefs = fn.get_addon_prefs() ob = context.object settings = context.scene.anim_cycle_settings bpy.ops.object.mode_set(mode='OBJECT', toggle=False) root_name = fn.get_root_name(context=context) root = ob.pose.bones.get(root_name) if not root: self.report({'ERROR'}, f'posebone {root_name} not found in armature {ob.name} check addon preferences to change name') return {"CANCELLED"} ## create curve at bone position loc = ob.matrix_world @ root.matrix.to_translation() root_axis_vec = fn.get_direction_vector_from_enum(settings.forward_axis) # get real world direction of the root world_forward = (root.matrix @ root_axis_vec) - root.matrix.to_translation() curve = fn.generate_curve(location=loc, direction=world_forward.normalized() * 10, name='curve_path', context=context) settings.path_to_follow = curve create_follow_path_constraint(ob, curve) # refresh evaluation so constraint shows up correctly bpy.context.scene.frame_set(bpy.context.scene.frame_current) return {"FINISHED"} class UAC_OT_create_follow_path(bpy.types.Operator): bl_idname = "anim.create_follow_path" bl_label = "Create Follow Path Constraint" bl_description = "Create follow path targeting curve chosen in dedicated field" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return context.object and context.object.type == 'ARMATURE' \ and context.scene.anim_cycle_settings.path_to_follow def execute(self, context): ob = context.object curve = context.scene.anim_cycle_settings.path_to_follow err, const = create_follow_path_constraint(ob, curve) if isinstance(err, str): self.report({'ERROR'}, err) return {"CANCELLED"} return {"FINISHED"} class UAC_OT_snap_curve_to_ground(bpy.types.Operator): bl_idname = "anim.snap_curve_to_ground" bl_label = "snap_curve_to_ground" bl_description = "Snap Curve" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return context.object and context.object.type == 'ARMATURE' def execute(self, context): err = snap_curve() if err: self.report({err[0]}, err[1]) if err[0] == 'ERROR': return {"CANCELLED"} return {"FINISHED"} class UAC_OT_edit_curve(bpy.types.Operator): bl_idname = "uac.edit_curve" bl_label = "Edit Curve" bl_description = "Edit curve used as constraint for foot" bl_options = {"REGISTER", "INTERNAL", "UNDO"} @classmethod def poll(cls, context): return context.object and context.object.type == 'ARMATURE' def execute(self, context): b = context.active_pose_bone curve = None # test with selected bone if b and b.constraints: curve = next((c.target for c in b.constraints if c.type == 'FOLLOW_PATH' and c.target), None) # get from 'root' bone if not curve: curve, _const = fn.get_follow_curve_from_armature(context.object) if isinstance(curve, str): self.report({curve}, _const) if curve == 'ERROR': return {"CANCELLED"} # set mode to object set curve as active and go Edit bpy.ops.object.mode_set(mode='OBJECT', toggle=False) curve.select_set(True) context.view_layer.objects.active = curve bpy.ops.object.mode_set(mode='EDIT', toggle=False) # EDIT_CURVE b.id_data.select_set(False) return {"FINISHED"} class UAC_OT_go_to_object(bpy.types.Operator): bl_idname = "uac.go_to_object" bl_label = "Go To Object" bl_description = "Go to object in pose mode" bl_options = {"REGISTER", "INTERNAL"} obj_name : bpy.props.StringProperty(options={'SKIP_SAVE'}) def execute(self, context): obj = context.scene.objects.get(self.obj_name) if not obj: self.report({'ERROR'}, f'Could not find object {self.obj_name} in scene objects') return {"CANCELLED"} bpy.ops.object.mode_set(mode='OBJECT', toggle=False) ## deselect all armatures (or all objects) for ob in context.scene.objects: if ob.type == 'ARMATURE': ob.select_set(False) obj.select_set(True) context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='POSE', toggle=False) self.report({'INFO'}, f'Back to pose mode {obj.name}') return {"FINISHED"} class UAC_OT_object_from_curve(bpy.types.Operator): bl_idname = "uac.object_from_curve" bl_label = "Back To Armature" bl_description = "Go in armature pose mode from current curve" bl_options = {"REGISTER", "INTERNAL", "UNDO"} @classmethod def poll(cls, context): return context.object and context.object.type == 'CURVE' def invoke(self, context, event): self.armatures = [] curve = context.object for o in context.scene.objects: if o.type != 'ARMATURE': continue for pb in o.pose.bones: for c in pb.constraints: if c.type == 'FOLLOW_PATH' and c.target and c.target == curve: self.armatures.append((o, pb)) break if not self.armatures: self.report({'ERROR'}, 'No armature using this curve found') return {"CANCELLED"} curve.select_set(False) if len(self.armatures) > 1: return context.window_manager.invoke_props_popup(self, event) # execute on change # set pose mode on only object available obj, pb = self.armatures[0] bpy.ops.object.mode_set(mode='OBJECT', toggle=False) for ob in context.scene.objects: if ob.type == 'ARMATURE': ob.select_set(False) obj.select_set(True) context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='POSE', toggle=False) self.report({'INFO'}, f'Back to pose mode {obj.name} (constraint on {pb.name})') return self.execute(context) def draw(self, context): layout = self.layout for arm, pb in self.armatures: layout.operator('uac.go_to_object', text=f'{arm.name} ({pb.name})', icon='OUTLINER_OB_ARMATURE').obj_name = arm.name def execute(self, context): return {"FINISHED"} classes=( UAC_OT_create_curve_path, UAC_OT_create_follow_path, UAC_OT_snap_curve_to_ground, UAC_OT_edit_curve, UAC_OT_go_to_object, UAC_OT_object_from_curve, # use set_choice_id is used to set an index in object_from_curve pop up menu ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) # if __name__ == "__main__": # register()