├── .gitignore ├── panels ├── __init__.py └── action.py ├── properties ├── actionman_item.py ├── __init__.py └── actionman_properties.py ├── operators ├── __init__.py ├── move_action.py ├── clean_action.py ├── delete_constraints.py └── apply_action_changes.py ├── LICENSE.md ├── README.md ├── __init__.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .vscode/ -------------------------------------------------------------------------------- /panels/__init__.py: -------------------------------------------------------------------------------- 1 | from .action import ActionManActionPanel 2 | 3 | 4 | __all__ = ["ActionManActionPanel"] 5 | 6 | -------------------------------------------------------------------------------- /properties/actionman_item.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class ActionManItemProperty(bpy.types.PropertyGroup): 5 | action: bpy.props.PointerProperty(type=bpy.types.Action) 6 | -------------------------------------------------------------------------------- /properties/__init__.py: -------------------------------------------------------------------------------- 1 | from .actionman_item import ActionManItemProperty 2 | from .actionman_properties import ActionManProperties 3 | 4 | 5 | __all__ = ["ActionManItemProperty", "ActionManProperties"] 6 | -------------------------------------------------------------------------------- /operators/__init__.py: -------------------------------------------------------------------------------- 1 | from .clean_action import CleanAction 2 | from .apply_action_changes import ApplyActionChanges 3 | from .delete_constraints import DeleteAllConstraints, DeleteUselessConstraints 4 | from .move_action import ActionMoveOperator 5 | 6 | 7 | __all__ = [ 8 | "CleanAction", 9 | "ApplyActionChanges", 10 | "DeleteAllConstraints", 11 | "DeleteUselessConstraints", 12 | "ActionMoveOperator", 13 | ] 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Loïc Pinsard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Action Man 2 | Action Man is a simple tool allowing you to use blender actions to their full potential. 3 | This only works on armature objects, adapting the tool to work with other object would be really easy but I don't need that for my personal workflow so I haven't implemented it. 4 | 5 | # Features 6 | - Split an action in two actions that contain Translation and Rotation/Scale. 7 | If this is checked, ActionMan will put all the Translation action constraint at the top of the stack and the Rotation/Scale at the bottom of the stack. 8 | This makes the rigs a lot more stable and reliable as the order of the constraints have no impact on the end result anymore. 9 | - Clean action: Removes all the animation data for a channel if the keyframes all have equal values. 10 | - Create/Update Constraints: Creates an action constraint on every object (usually bones) affected by the action. 11 | All the settings above the `Create Constraints` will directly be used on the action constraint. 12 | This operator also renames automatically the constraints if the action has been renamed 13 | - Delete all the actions constraints related to an action that aren't usefull (ie: constraints that exist on a bone that isn't affected by the action). this is usefull in two cases: 14 | - You simplified an action and some bones aren't affectecte by it anymore. 15 | - You forgot to click the `clean action` button before clicking the `create constraints` button. 16 | 17 | # Upcoming features 18 | - [ ] Create mirror action 19 | -------------------------------------------------------------------------------- /operators/move_action.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import logging 3 | 4 | from ..utils import enforce_constraint_order 5 | 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class ActionMoveOperator(bpy.types.Operator): 11 | bl_idname = "actionman.action_move" 12 | bl_label = "Action Move" 13 | bl_options = {"INTERNAL"} 14 | 15 | direction: bpy.props.StringProperty() 16 | 17 | def invoke(self, context, event): 18 | self.armature = context.object.data 19 | self.index = self.armature.actionman_active_action_index 20 | self.action = self.armature.actionman_actions.values()[self.index].action 21 | 22 | if event.shift: 23 | self.move_max() 24 | else: 25 | self.move_once() 26 | 27 | return {"FINISHED"} 28 | 29 | def move_max(self): 30 | can_move = True 31 | while can_move: 32 | can_move = self.move_once() 33 | 34 | def move_once(self): 35 | 36 | if self.direction == "UP": 37 | new_index = self.index - 1 38 | if self.direction == "DOWN": 39 | new_index = self.index + 1 40 | 41 | if new_index < 0 or new_index >= len(self.armature.actionman_actions): 42 | return False 43 | 44 | other_action = self.armature.actionman_actions.values()[new_index].action 45 | 46 | self.armature.actionman_actions.move(self.index, new_index) 47 | self.armature.actionman_active_action_index = new_index 48 | self.index = new_index 49 | self.action.actionman.index = new_index 50 | 51 | enforce_constraint_order(self.armature, self.action) 52 | enforce_constraint_order(self.armature, other_action) 53 | 54 | return True 55 | -------------------------------------------------------------------------------- /operators/clean_action.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import logging 3 | 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def remove_flat_curves(action): 9 | """Remove the fcurves where the keyframes all have the same value.""" 10 | logger.info("Removing flat curves of action {}.".format(action.name)) 11 | for fcurve in action.fcurves: 12 | points = fcurve.keyframe_points 13 | first_value = None 14 | is_flat_curve = True 15 | for point in points: 16 | coordinates = point.co 17 | value = round(coordinates[1], 5) 18 | if first_value is None: # storing the value of the first point. 19 | first_value = value 20 | else: # comparing every point's value to the first value. 21 | if value != first_value: 22 | is_flat_curve = False 23 | break 24 | if is_flat_curve: 25 | logger.debug("Removing curve {} ".format(fcurve.data_path)) 26 | action.fcurves.remove(fcurve) 27 | 28 | 29 | def remove_empty_groups(action): 30 | """Remove the groups that have no channels.""" 31 | logger.info("Removing empty groups of action {}.".format(action.name)) 32 | for group in action.groups: 33 | if len(group.channels) == 0: 34 | logger.debug("Removing group {}.".format(group.name)) 35 | action.groups.remove(group) 36 | 37 | 38 | class CleanAction(bpy.types.Operator): 39 | """Removes all the useless fcurves and groups of the active action.""" 40 | 41 | bl_idname = "actionman.clean" 42 | bl_label = "Clean Action" 43 | bl_options = {"REGISTER", "UNDO"} 44 | 45 | @classmethod 46 | def poll(cls, context): 47 | """Make sure there's an active action.""" 48 | return context.object.animation_data.action is not None 49 | 50 | def execute(self, context): 51 | """Execute the operator.""" 52 | action = context.object.animation_data.action 53 | remove_flat_curves(action) 54 | remove_empty_groups(action) 55 | return {"FINISHED"} 56 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or modify 2 | # it under the terms of the GNU General Public License as published by 3 | # the Free Software Foundation; either version 3 of the License, or 4 | # (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, but 7 | # WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 9 | # General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | 14 | bl_info = { 15 | "name": "Action Man", 16 | "author": "Loïc Pinsard", 17 | "description": "Action Man lets you easily create and manage action constraint for a given action.", 18 | "blender": (2, 80, 0), 19 | "location": "", 20 | "warning": "", 21 | "category": "Rigging", 22 | } 23 | 24 | import bpy 25 | 26 | from .operators import ( 27 | CleanAction, 28 | ApplyActionChanges, 29 | DeleteUselessConstraints, 30 | DeleteAllConstraints, 31 | ActionMoveOperator, 32 | ) 33 | from .panels import ActionManActionPanel 34 | from .properties import ActionManItemProperty, ActionManProperties 35 | 36 | 37 | CLASSES_TO_REGISTER = ( 38 | # operators 39 | CleanAction, 40 | ApplyActionChanges, 41 | DeleteUselessConstraints, 42 | DeleteAllConstraints, 43 | ActionMoveOperator, 44 | # panels 45 | ActionManActionPanel, 46 | # properties 47 | ActionManItemProperty, 48 | ActionManProperties, 49 | ) 50 | 51 | 52 | def select_active_action(self, context): 53 | armature = context.active_object 54 | index = self.actionman_active_action_index 55 | action = self.actionman_actions.values()[index].action 56 | armature.animation_data.action = action 57 | 58 | 59 | def register(): 60 | """Register the addon.""" 61 | 62 | for cls in CLASSES_TO_REGISTER: 63 | bpy.utils.register_class(cls) 64 | 65 | bpy.types.Action.actionman = bpy.props.PointerProperty( 66 | type=ActionManProperties, name="Action Man Properties" 67 | ) 68 | bpy.types.Armature.actionman_actions = bpy.props.CollectionProperty( 69 | type=ActionManItemProperty 70 | ) 71 | bpy.types.Armature.actionman_active_action_index = bpy.props.IntProperty( 72 | update=select_active_action 73 | ) 74 | 75 | 76 | def unregister(): 77 | """Unregister the addon.""" 78 | 79 | for cls in CLASSES_TO_REGISTER: 80 | bpy.utils.unregister_class(cls) 81 | -------------------------------------------------------------------------------- /operators/delete_constraints.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import bpy 3 | 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class DeleteUselessConstraints(bpy.types.Operator): 9 | """Remove all the action constraints named like the active action that exist on an object that isn't influenced by the action.""" 10 | 11 | bl_idname = "actionman.delete_useless_constraints" 12 | bl_label = "Delete Useless Constraints" 13 | bl_options = {"REGISTER", "UNDO"} 14 | 15 | @classmethod 16 | def poll(cls, context): 17 | action = context.object.animation_data.action 18 | action_exists = action is not None 19 | is_managed = action.actionman.manage 20 | return action_exists and is_managed 21 | 22 | def execute(self, context): 23 | """Execute the operator.""" 24 | action = context.object.animation_data.action 25 | for obj in bpy.data.objects: 26 | if obj.type == "ARMATURE": 27 | for bone in obj.pose.bones: 28 | for constraint in bone.constraints: 29 | if constraint.type == "ACTION": 30 | if ( 31 | action.name == constraint.name 32 | and bone.name not in action.groups 33 | ): 34 | logger.info( 35 | "Removing Constraint `{}` on bone `{}`".format( 36 | constraint.name, bone.name 37 | ) 38 | ) 39 | bone.constraints.remove(constraint) 40 | 41 | return {"FINISHED"} 42 | 43 | 44 | class DeleteAllConstraints(bpy.types.Operator): 45 | """Remove all the action constraints named like the active action.""" 46 | 47 | bl_idname = "actionman.delete_all_constraints" 48 | bl_label = "Delete All Constraints" 49 | bl_options = {"REGISTER", "UNDO"} 50 | 51 | @classmethod 52 | def poll(cls, context): 53 | action = context.object.animation_data.action 54 | action_exists = action is not None 55 | is_face_action = action.actionman.manage 56 | return action_exists and is_face_action 57 | 58 | def execute(self, context): 59 | """Execute the operator.""" 60 | action = context.object.animation_data.action 61 | for obj in bpy.data.objects: 62 | if obj.type == "ARMATURE": 63 | for bone in obj.pose.bones: 64 | for constraint in bone.constraints: 65 | if constraint.type == "ACTION": 66 | if action.name == constraint.name: 67 | logger.info( 68 | "Removing Constraint `{}` on bone `{}`".format( 69 | constraint.name, bone.name 70 | ) 71 | ) 72 | bone.constraints.remove(constraint) 73 | 74 | return {"FINISHED"} 75 | -------------------------------------------------------------------------------- /panels/action.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import logging 3 | 4 | 5 | logging.getLogger(__name__) 6 | 7 | 8 | class ActionManActionPanel(bpy.types.Panel): 9 | bl_category = "Action Man" 10 | bl_label = "Action Man" 11 | bl_idname = "FR_PT_panel" 12 | bl_space_type = "DOPESHEET_EDITOR" 13 | bl_region_type = "UI" 14 | 15 | @classmethod 16 | def poll(cls, context): 17 | """Make sure there's an active action.""" 18 | active_object = context.object 19 | if not active_object: 20 | return False 21 | 22 | animation_data = active_object.animation_data 23 | if not animation_data: 24 | return False 25 | 26 | action = animation_data.action 27 | if not action: 28 | return False 29 | 30 | return True 31 | 32 | def draw(self, context): 33 | action = context.object.animation_data.action 34 | 35 | layout = self.layout 36 | 37 | row = layout.row() 38 | row.prop(action.actionman, "manage") 39 | 40 | if not action.actionman.manage: 41 | return 42 | 43 | settings_box = layout.box() 44 | row = settings_box.row() 45 | row.prop(action.actionman, "target") 46 | target = action.actionman.target 47 | 48 | if target is not None: 49 | if type(target.data) == bpy.types.Armature: 50 | row = settings_box.row() 51 | row.prop_search( 52 | action.actionman, "subtarget", target.data, "bones", text="Bone" 53 | ) 54 | 55 | row = settings_box.row() 56 | row.prop(action.actionman, "transform_channel") 57 | 58 | row = settings_box.row() 59 | row.prop(action.actionman, "target_space") 60 | 61 | row = settings_box.row() 62 | row.separator() 63 | 64 | row = settings_box.row() 65 | row.label(text="Activation Range:") 66 | 67 | split = settings_box.split() 68 | col = split.column(align=True) 69 | 70 | transform_channel = action.actionman.transform_channel.split("_")[0] 71 | col.prop( 72 | action.actionman, 73 | "activation_start_" + transform_channel.lower(), 74 | text="Start", 75 | ) 76 | col.prop( 77 | action.actionman, "activation_end_" + transform_channel.lower(), text="End" 78 | ) 79 | 80 | row = settings_box.row() 81 | row.separator() 82 | 83 | row = settings_box.row() 84 | row.prop(action.actionman, "split_transformations") 85 | 86 | row = layout.row() 87 | row.separator() 88 | 89 | row = layout.row() 90 | row.operator("actionman.clean") 91 | 92 | row = layout.row() 93 | row.operator("actionman.apply", text="Apply") 94 | 95 | row = layout.row() 96 | row.separator() 97 | 98 | row = layout.row() 99 | row.label(text="Delete Constraints:") 100 | row = layout.row(align=True) 101 | row.operator("actionman.delete_useless_constraints", text="Useless") 102 | row.operator("actionman.delete_all_constraints", text="All") 103 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import logging 3 | 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def enforce_constraint_order(armature, action): 9 | bones = get_affected_bones(armature, action) 10 | current_active_bone = armature.bones.active 11 | for bone in bones: 12 | constraints = bone.constraints 13 | 14 | action_constraints = [c.name for c in constraints if c.type == "ACTION"] 15 | translate_constraints = sorted( 16 | [c for c in action_constraints if "translate" in c] 17 | ) 18 | rotate_scale_constraints = sorted( 19 | [c for c in action_constraints if "rotate_scale" in c] 20 | ) 21 | unsplit_constraints = sorted( 22 | list( 23 | set(action_constraints) 24 | - set(translate_constraints) 25 | - set(rotate_scale_constraints) 26 | ) 27 | ) 28 | 29 | ordered_action_names = ( 30 | translate_constraints + rotate_scale_constraints + unsplit_constraints 31 | ) 32 | 33 | armature.bones.active = bone.bone 34 | reorder_constraints(bone, ordered_action_names) 35 | armature.bones.active = current_active_bone 36 | 37 | 38 | def get_affected_bones(armature, action): 39 | armature_ob = bpy.data.objects[armature.name] 40 | return [b for b in armature_ob.pose.bones if b.name in action.groups] 41 | 42 | 43 | def reorder_constraints(bone, ordered_action_names): 44 | def sort_constraint_as_actions(item): 45 | return ordered_action_names.index(item.name) 46 | 47 | constraints = [c for c in bone.constraints if c.type == "ACTION"] 48 | 49 | ordered_constraints = sorted(constraints, key=sort_constraint_as_actions) 50 | 51 | for expected_index, constraint in enumerate(ordered_constraints): 52 | 53 | ctx = bpy.context.copy() 54 | ctx["constraint"] = constraint 55 | 56 | current_index = bone.constraints.find(constraint.name) 57 | if current_index > expected_index: 58 | while current_index != expected_index: 59 | previous_index = current_index 60 | bpy.ops.constraint.move_up( 61 | ctx, constraint=constraint.name, owner="BONE" 62 | ) 63 | current_index = bone.constraints.find(constraint.name) 64 | if previous_index == current_index: 65 | raise Exception( 66 | "Failed to move the constraint {}. The bone {} is probably hidden".format( 67 | constraint.name, bone.name 68 | ) 69 | ) 70 | elif current_index < expected_index: 71 | while current_index != expected_index: 72 | previous_index = current_index 73 | bpy.ops.constraint.move_down( 74 | ctx, constraint=constraint.name, owner="BONE" 75 | ) 76 | current_index = bone.constraints.find(constraint.name) 77 | if previous_index == current_index: 78 | raise Exception( 79 | "Failed to move the constraint {}. The bone {} is probably hidden".format( 80 | constraint.name, bone.name 81 | ) 82 | ) 83 | -------------------------------------------------------------------------------- /properties/actionman_properties.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def on_update_manage(self, context): 5 | armature = context.active_object.data 6 | action = context.active_object.animation_data.action 7 | value = self.manage 8 | if value: 9 | existing_actions = [a.action for a in armature.actionman_actions] 10 | if self in existing_actions: 11 | return 12 | prop = armature.actionman_actions.add() 13 | prop.name = action.name 14 | prop.action = action 15 | else: 16 | index = armature.actionman_actions.find(action.name) 17 | if index > -1: 18 | armature.actionman_actions.remove(index) 19 | 20 | 21 | class ActionManProperties(bpy.types.PropertyGroup): 22 | manage: bpy.props.BoolProperty(name="Manage", update=on_update_manage) 23 | index: bpy.props.IntProperty("Index") 24 | name_backup: bpy.props.StringProperty(name="Name Backup") 25 | 26 | target: bpy.props.PointerProperty(name="Target", type=bpy.types.Object) 27 | subtarget: bpy.props.StringProperty(name="Sub Target") 28 | 29 | transform_channel: bpy.props.EnumProperty( 30 | name="Transform Channel", 31 | items=[ 32 | ("LOCATION_X", " X Location", " X Location", 1), 33 | ("LOCATION_Y", " Y Location", " Y Location", 2), 34 | ("LOCATION_Z", " Z Location", " Z Location", 3), 35 | ("ROTATION_X", "X Rotation", "X Rotation", 4), 36 | ("ROTATION_Y", "Y Rotation", "Y Rotation", 5), 37 | ("ROTATION_Z", "Z Rotation", "Z Rotation", 6), 38 | ("SCALE_X", "X Scale", "X Scale", 7), 39 | ("SCALE_Y", "Y Scale", "Y Scale", 8), 40 | ("SCALE_Z", "Z Scale", "Z Scale", 9), 41 | ], 42 | ) 43 | target_space: bpy.props.EnumProperty( 44 | name="Target Space", 45 | items=[ 46 | ("WORLD", "World Space", "World Space", 1), 47 | ("POSE", "Pose Space", "Pose Space", 2), 48 | ("LOCAL_WITH_PARENT", "Local With Parent", "Local With Parent", 3), 49 | ("LOCAL", "Local Space", "Local Space", 4), 50 | ], 51 | default="LOCAL", 52 | ) 53 | 54 | activation_start: bpy.props.FloatProperty("Activation Start", unit="LENGTH") 55 | activation_end: bpy.props.FloatProperty("Activation End", unit="LENGTH") 56 | 57 | activation_start_location: bpy.props.FloatProperty( 58 | "Activation Start Location", unit="LENGTH" 59 | ) 60 | activation_end_location: bpy.props.FloatProperty( 61 | "Activation End Location", unit="LENGTH" 62 | ) 63 | 64 | activation_start_rotation: bpy.props.FloatProperty( 65 | "Activation Start Rotation", unit="ROTATION" 66 | ) 67 | activation_end_rotation: bpy.props.FloatProperty( 68 | "Activation End Rotation", unit="ROTATION" 69 | ) 70 | 71 | activation_start_scale: bpy.props.FloatProperty("Activation Start Scale") 72 | activation_end_scale: bpy.props.FloatProperty("Activation End Scale") 73 | split_transformations: bpy.props.BoolProperty( 74 | name="Split Transformations", default=True 75 | ) 76 | translate_subaction: bpy.props.PointerProperty( 77 | name="Translate Subaction", type=bpy.types.Action 78 | ) 79 | rotate_scale_subaction: bpy.props.PointerProperty( 80 | name="Rotate/Scale Subaction", type=bpy.types.Action 81 | ) 82 | -------------------------------------------------------------------------------- /operators/apply_action_changes.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from math import pi 3 | import bpy 4 | 5 | from ..utils import enforce_constraint_order 6 | 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class ApplyActionChanges(bpy.types.Operator): 12 | """Removes all the useless fcurves and groups of the active action.""" 13 | 14 | bl_idname = "actionman.apply" 15 | bl_label = "Create Action Constraints" 16 | bl_options = {"REGISTER", "UNDO", "INTERNAL"} 17 | 18 | @classmethod 19 | def poll(cls, context): 20 | action = context.object.animation_data.action 21 | action_exists = action is not None 22 | is_managed = action.actionman.manage 23 | return action_exists and is_managed 24 | 25 | def execute(self, context): 26 | """Execute the operator.""" 27 | self.action = context.object.animation_data.action 28 | 29 | if self.action.name != self.action.actionman.name_backup: 30 | self.rename_action_references() 31 | 32 | if self.action.actionman.split_transformations: 33 | self.split_transformations() 34 | 35 | self.create_or_update_constraints(context) 36 | 37 | armature = bpy.context.object.data 38 | enforce_constraint_order(armature, self.action) 39 | 40 | return {"FINISHED"} 41 | 42 | def split_transformations(self): 43 | translate_action = self.create_sub_action("translate") 44 | for fcurve in translate_action.fcurves: 45 | if not "location" in fcurve.data_path: 46 | translate_action.fcurves.remove(fcurve) 47 | 48 | rotate_scale_action = self.create_sub_action("rotate_scale") 49 | for fcurve in rotate_scale_action.fcurves: 50 | if "location" in fcurve.data_path: 51 | rotate_scale_action.fcurves.remove(fcurve) 52 | 53 | def create_sub_action(self, action_component): 54 | action_name = action_component + "_subaction" 55 | action = getattr(self.action.actionman, action_name, None) 56 | 57 | if action: 58 | bpy.data.actions.remove(action, do_unlink=True) 59 | 60 | action = self.action.copy() 61 | self.clear_action(action) 62 | 63 | setattr(self.action.actionman, action_name, action) 64 | action.name = f"{self.action.name}.{action_component}" 65 | 66 | return action 67 | 68 | def clear_action(self, action): 69 | """resets the action's actionman attributs to their default.""" 70 | manage = False 71 | index = 0 72 | name_backup = "" 73 | 74 | target = None 75 | subtarget = "" 76 | 77 | transform_channel = "LOCATION_X" 78 | target_space = "LOCAL" 79 | 80 | activation_start = 0 81 | activation_end = 0 82 | 83 | activation_start_location = 0 84 | activation_end_location = 0 85 | activation_start_rotation = 0 86 | activation_end_rotation = 0 87 | activation_start_scale = 0 88 | activation_end_scale = 0 89 | 90 | split_transformations = False 91 | translate_subaction = None 92 | rotate_scale_subaction = None 93 | 94 | def create_or_update_constraints(self, context): 95 | for group in self.action.groups: # each group corresponds to a bone 96 | obj = context.object 97 | bone = obj.pose.bones[group.name] 98 | 99 | self.remove_unwanted_constraints(bone) 100 | if self.action.actionman.split_transformations: 101 | translate_constraint = self.create_or_get_action_constraint( 102 | bone, self.action.name + ".translate" 103 | ) 104 | self.set_constraint_settings( 105 | translate_constraint, self.action.actionman.translate_subaction 106 | ) 107 | rotate_scale_constraint = self.create_or_get_action_constraint( 108 | bone, self.action.name + ".rotate_scale" 109 | ) 110 | self.set_constraint_settings( 111 | rotate_scale_constraint, 112 | self.action.actionman.rotate_scale_subaction, 113 | ) 114 | else: 115 | constraint = self.create_or_get_action_constraint( 116 | bone, self.action.name 117 | ) 118 | self.set_constraint_settings(constraint, self.action) 119 | 120 | self.create_or_update_limit_constraints() 121 | 122 | def rename_action_references(self): 123 | """rename the actions in the constraints as well as the armature.actionman_actions.""" 124 | for obj in bpy.data.objects: 125 | if obj.type == "ARMATURE": 126 | armature = obj.data 127 | for prop in armature.actionman_actions: 128 | if prop.action == self.action: 129 | prop.name = self.action.name 130 | for bone in obj.pose.bones: 131 | for constraint in bone.constraints: 132 | if constraint.type == "ACTION": 133 | if constraint.name == self.action.actionman.name_backup: 134 | constraint.name = self.action.name 135 | self.action.actionman.name_backup = self.action.name 136 | 137 | def create_or_get_action_constraint(self, bone, constraint_name): 138 | if constraint_name in [c.name for c in bone.constraints]: 139 | logger.info( 140 | "Action Constraint `{}` already exists on `{}`, updating its values only.".format( 141 | constraint_name, bone.name 142 | ) 143 | ) 144 | constraint = bone.constraints[constraint_name] 145 | else: 146 | logger.info("Adding action constraint on {}".format(bone)) 147 | constraint = bone.constraints.new("ACTION") 148 | constraint.name = constraint_name 149 | constraint.show_expanded = False 150 | return constraint 151 | 152 | def set_constraint_settings(self, constraint, action): 153 | target = self.action.actionman.target 154 | constraint.target = target 155 | if self.action.actionman.subtarget: 156 | constraint.subtarget = self.action.actionman.subtarget 157 | 158 | constraint.transform_channel = self.action.actionman.transform_channel 159 | constraint.target_space = self.action.actionman.target_space 160 | 161 | constraint.action = action 162 | 163 | constraint.frame_start = int(self.action.frame_range[0]) 164 | constraint.frame_end = int(self.action.frame_range[1]) 165 | 166 | transform = self.action.actionman.transform_channel.split("_")[0] 167 | 168 | value_min = getattr( 169 | self.action.actionman, "activation_start_" + transform.lower() 170 | ) 171 | value_max = getattr( 172 | self.action.actionman, "activation_end_" + transform.lower() 173 | ) 174 | 175 | if transform == "ROTATION": 176 | value_min = value_min * (180 / pi) 177 | value_max = value_max * (180 / pi) 178 | 179 | constraint.min = value_min 180 | constraint.max = value_max 181 | 182 | def remove_unwanted_constraints(self, bone): 183 | """Remove the action constraints that shouldn't exist. 184 | if the action is split, remove the main action from the constraint. 185 | if it's not split, remove the components. 186 | """ 187 | if self.action.actionman.split_transformations: 188 | # remove the potential unsplit action from the constraints 189 | if self.action.name in [c.name for c in bone.constraints]: 190 | bone.constraints.remove(bone.constraints[self.action.name]) 191 | else: 192 | # remove the potential split actions from the constraint 193 | translate_action = self.action.actionman.translate_subaction 194 | if translate_action: 195 | if translate_action.name in [c.name for c in bone.constraints]: 196 | bone.constraints.remove(bone.constraints[translate_action.name]) 197 | rotate_scale_action = self.action.actionman.rotate_scale_subaction 198 | if rotate_scale_action: 199 | if rotate_scale_action.name in [c.name for c in bone.constraints]: 200 | bone.constraints.remove(bone.constraints[rotate_scale_action.name]) 201 | 202 | def create_or_update_limit_constraints(self): 203 | target = self.action.actionman.target 204 | subtarget = self.action.actionman.subtarget 205 | actions = [ 206 | a 207 | for a in bpy.data.actions 208 | if a.actionman.target == target and a.actionman.subtarget == subtarget 209 | ] 210 | 211 | if target: 212 | if subtarget: 213 | subtarget = target.pose.bones[subtarget] 214 | controller = subtarget 215 | else: 216 | logger.warning( 217 | "No Controller specified, skipping limit constraint creation" 218 | ) 219 | return 220 | 221 | data = {"LOCATION": {}, "ROTATION": {}, "SCALE": {}} 222 | for action in actions: 223 | if not action.actionman.manage: 224 | continue 225 | if ( 226 | action.actionman.target == target 227 | and action.actionman.subtarget == subtarget.name 228 | ): 229 | transform_channel = action.actionman.transform_channel 230 | transform, axis = transform_channel.split("_") 231 | end = getattr(action.actionman, "activation_end_" + transform.lower()) 232 | if transform == "ROTATION": 233 | end = end * (180 / pi) 234 | 235 | if transform == "SCALE": 236 | if end < 1: 237 | data[transform]["min_" + axis] = end 238 | 239 | if end > 1: 240 | data[transform]["max_" + axis] = end 241 | else: 242 | if end < 0: 243 | data[transform]["min_" + axis] = end 244 | 245 | if end > 0: 246 | data[transform]["max_" + axis] = end 247 | 248 | for transform in ["LOCATION", "ROTATION", "SCALE"]: 249 | if data[transform]: 250 | constraint = self.create_or_get_limit_constraint(controller, transform) 251 | for axis in "XYZ": 252 | if transform == "SCALE": 253 | start = data[transform].get("min_" + axis, 1) 254 | end = data[transform].get("max_" + axis, 1) 255 | else: 256 | start = data[transform].get("min_" + axis, 0) 257 | end = data[transform].get("max_" + axis, 0) 258 | if transform == "ROTATION": 259 | setattr(constraint, "use_limit_" + axis.lower(), True) 260 | start = start * (180 / pi) 261 | end = end * (180 / pi) 262 | else: 263 | setattr(constraint, "use_min_" + axis.lower(), True) 264 | setattr(constraint, "use_max_" + axis.lower(), True) 265 | 266 | if start: 267 | setattr(constraint, "min_" + axis.lower(), start) 268 | if end: 269 | setattr(constraint, "max_" + axis.lower(), end) 270 | 271 | def create_or_get_limit_constraint(self, controller, transform_type): 272 | constraint_name = "Limit " + transform_type.title() 273 | constraint_type = "LIMIT_" + transform_type.upper() 274 | 275 | existing_constraints_names = [c.name for c in controller.constraints] 276 | if constraint_name in existing_constraints_names: 277 | constraint = controller.constraints[constraint_name] 278 | else: 279 | constraint = controller.constraints.new(constraint_type) 280 | constraint.owner_space = "LOCAL" 281 | constraint.use_transform_limit = True 282 | constraint.show_expanded = False 283 | 284 | return constraint 285 | 286 | --------------------------------------------------------------------------------