Monday, December 1, 2025

action and action constraint tool doodle for Blender 2.79

this is a tool i wrote for Blender 2.79 to help with creating actions and action constraints.  there may be bugs so please modify/use at your own risk.  to use the addon each of the files need to be saved in a folder and put in Blender add-on paths.  Happy Sketching!



the files below are
__init__.py
creating.py
editing.py
utils.py
ui.py

which can be saved in folder example (naActionTools) 

 

#__init__.py

import bpy 
import imp

bl_info = {
    "name": "actions/action constraint tool",
    "author": "Nathaniel Anozie",
    "version": (0, 1),
    "blender": (2, 79, 0),
    "location": "3D View",
    "description": "actions/action constraint tool",
    "category": "Object",
}

from . import ui
imp.reload(ui)

from . import creating
imp.reload(creating)

from . import utils
imp.reload(utils)


def register():
    ui.register()
    
def unregister():
    ui.unregister()
    
if __name__ == "__main__":
    register()
#creating.py
import bpy
import logging
import re
import math

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG) #without this info logs wouldnt show in console


from . import utils


class PoseMirror(object):
    """responsible for mirroring actions and action constraints
    """
    """
    import imp
    testTool = imp.load_source("tool","/Users/Nathaniel/Documents/src_blender/python/riggingTools/faceTools/addJointBlendshape.py") #change to path to python file
    
    #print(testTool.PoseMirror("Armature")._getSourceActions())
    
    testTool.PoseMirror("Armature").mirrorActions()
    testTool.PoseMirror("Armature").mirrorActionConstraints()
    
    """     
    def __init__(self, armature):
        """
        @param armature str name for armature object
        """
        self._armature = armature
        
    def mirrorActionConstraints(self, bones=[]): #
        """mirror all action constraints from left side to right - assumes all destination bones/actions exist
        @param bones (list of str) optional bone names to mirror action constraints for. if not specified looks at all left side bones
        """
        armature = self._armature
        
        #find all action constraints on a bone that ends in .L - for simplicity require action constraint to end in .L    
        allSourceBones = []
        if bones:
            allSourceBones = [bone for bone in bones if bone.endswith('.L')]
        else:    
            allSourceBones = [b.name for b in bpy.data.objects[armature].pose.bones if b.name.endswith('.L')]
        
        
        #allSourceBones = [b.name for b in bpy.data.objects[armature].pose.bones if b.name.endswith('.L')]
        
        if not allSourceBones:
            logger.info("doing nothing - requires a source bone ending in .L to exist")
            return
    
        #for each action constraint find the .L bone and .R bone
        for boneSource in allSourceBones:
            #if bone doesnt have a destination skip it
            boneDestination = boneSource.replace(".L",".R")
            if not boneDestination in bpy.data.objects[armature].pose.bones:
                continue
            
            #if source bone doesnt have action constraint skip it 
            sourceBoneActionConstraints = [cnt.name for cnt in bpy.data.objects[armature].pose.bones[boneSource].constraints if cnt.type == "ACTION"]
            if not sourceBoneActionConstraints:
                continue
            
            ###
            #if destination bone has any action constraints remove them
            #so we dont double up constraints
            destinationBoneExistingConstraints = [cnt.name for cnt in bpy.data.objects[armature].pose.bones[boneDestination].constraints if cnt.type == "ACTION"]
            for cntName in destinationBoneExistingConstraints:
                cntObj = bpy.data.objects[armature].pose.bones[boneDestination].constraints[cntName] 
                bpy.data.objects[armature].pose.bones[boneDestination].constraints.remove(cntObj)
            ###
            
            
            for sourceActionConstraint in sourceBoneActionConstraints: 
                
                logger.debug("sourceActionConstraint:"+sourceActionConstraint)
                
                #make the same constraint on destination bone
                #copy constraint data from source to destination
                cntObjSource = bpy.data.objects[armature].pose.bones[boneSource].constraints[sourceActionConstraint]

                #make the destination constraint name - might want to enforce source constraint ends in proper source suffix
                cntDestinationName = ""
                cntDestinationName = cntObjSource.name.replace(".L", ".R")
                """
                if cntObjSource.name.endswith(".L"):
                    cntDestinationName = cntObjSource.name.replace(".L", ".R")
                else:
                    cntDestinationName = cntObjSource.name+".R"
                """
                
                #if destination action constraint already exists skip it
                """
                allActionConstraints = self._getAllActionConstraints()
                if cntDestinationName in allActionConstraints:
                    logger.debug("skipping: {} because it already exists".format(cntDestinationName))
                    continue
                """
                
                #skip if cant find destination action
                sourceActionName = cntObjSource.action.name
                if sourceActionName.endswith(".L"):
                    destinationAction = sourceActionName.replace(".L", ".R")
                elif sourceActionName.endswith(".C"):
                    destinationAction = sourceActionName           
                #else:
                #    destinationAction = sourceActionName+"_mir"
                    
                if not destinationAction in bpy.data.actions:
                    logger.warning("could not find destination action {action} for source bone {bone} skipping. supports destination suffix .R or _mir".format(action=destinationAction, bone=boneSource))
                    continue
                

                    
                cntObjDestination = bpy.data.objects[armature].pose.bones[boneDestination].constraints.new("ACTION")
                cntObjDestination.name = cntDestinationName
                
                cntObjDestination.target =          cntObjSource.target
                cntObjDestination.subtarget =       cntObjSource.subtarget.replace(".L",".R")#todo check it exists
                cntObjDestination.target_space =    cntObjSource.target_space
                srcTransformChannel = cntObjSource.transform_channel
                cntObjDestination.transform_channel=srcTransformChannel
                cntObjDestination.min =             cntObjSource.min
                srcMax = cntObjSource.max
                if "LOCATION_X" in srcTransformChannel or "ROTATION_Z" in srcTransformChannel:
                    srcMax = -1*srcMax #to support mirroring of action constraint
                cntObjDestination.max =             srcMax
                cntObjDestination.frame_start =     cntObjSource.frame_start
                cntObjDestination.frame_end =       cntObjSource.frame_end
                
                #logger.debug("destination action {}".format(destinationAction))
                cntObjDestination.action =          bpy.data.actions[destinationAction]
        
        utils.turnOffActions(armature)

    def mirrorActions(self, actionNames=[], replace=False):
        """mirror given source action names - if none provide it mirrors all source side actions
        @param actionNames (list of str) - optional list of action names ex: ["testAction.L"]
        @param replace (bool) - whether to replace the action on opposite side if it exists - defaults to False
        """
        actionsSource = []
        if actionNames:
            actionsSource = actionNames
        else:
            #tries to mirror all source actions
            actionsSource = self._getSourceActions() or [] #should be str names for action
            
        for action in actionsSource:
            
            if self._isMirrorActionExists(action):
                if not replace:
                    #skip mirroring actions that already have a mirror
                    continue
                else:
                    #overrite mirror action by first deleting it
                    destinationActionName = self._getMirrorAction(action)
                    if destinationActionName in bpy.data.actions:
                        bpy.data.actions.remove(bpy.data.actions[destinationActionName])
                    
            self.mirrorSingleAction(action)
    
        utils.turnOffActions(self._armature)

    def _getMirrorAction(self, actionName):
        """return str for mirrored action name if it exists - None otherwise
        """
        if actionName.endswith(".L"):
            destinationActionName = actionName.replace(".L",".R") 
        else:
            destinationActionName = actionName+"_mir"
    
        if destinationActionName in bpy.data.actions:
            return destinationActionName
            
        return None
        
    def _isMirrorActionExists(self, actionName):
        """return true if mirror of action already exists
        """
        if self._getMirrorAction(actionName):
            return True
        return False
    
    def mirrorSingleAction(self, sourceAction):
        """mirrors action for all bones in action - going from source left to destination right
        @param sourceAction str name for source action to mirror
        """
        armature = self._armature
        if not sourceAction in bpy.data.actions:
            raise RuntimeError("requires source action to exist in blend file")
            
        sourceData = bpy.data.actions[sourceAction]
        if sourceAction.endswith(".L"):
            destinationActionName = sourceAction.replace(".L",".R") #todo: find better name - like using .R suffix
        else:
            destinationActionName = sourceAction+"_mir"
            
        obj = bpy.data.objects[armature]
            
        #create a new action on object. for now ignoring additional way 
        #to find action name to prevent trailing digits on action name.
        obj.animation_data_create() #so we have .action attribute
        mirAction = bpy.data.actions.new(name=destinationActionName)
        obj.animation_data.action = mirAction
        
        
        #might have .R side bones animated when starting mirror so skip these fcurves
        sourceBoneFcurves = [fcrv for fcrv in sourceData.fcurves if ".R" not in fcrv.data_path] #going from source left to destination right
        
        #create fcurves on action
        for fc in sourceBoneFcurves:
            logging.debug("in:{}".format(fc.data_path))
            logging.debug("in:{}".format(fc.array_index))
            #create new fcurve
            mirrorDataPath = fc.data_path.replace(".L",".R")
            mirrorArrayIndex = fc.array_index
            logging.debug("mirrorDataPath: "+mirrorDataPath)
            logging.debug("mirrorArrayIndex: "+str(fc.array_index))
            fc_scene = bpy.data.actions[mirAction.name].fcurves.new(mirrorDataPath, fc.array_index)
            logging.debug("out:{}".format(fc_scene.data_path))
            logging.debug("out:{}".format(fc_scene.array_index))
            #add keyframe points for fcurve
            for pt in fc.keyframe_points:
                ptco = list(pt.co) 
                logging.debug(ptco)
                ##flip rotate y and z. quaternion y and z. flip translate x
                #todo: see if the tangent handles need to be flipped as well
                shouldFlip = False
                if ("location" in mirrorDataPath and mirrorArrayIndex == 0) \
                    or ("rotation_euler" in mirrorDataPath and mirrorArrayIndex in [1,2]) \
                    or ("rotation_quaternion" in mirrorDataPath and mirrorArrayIndex in [2,3]):
                    shouldFlip = True
                if shouldFlip:
                    ptco[1] = -1*ptco[1]
                ##   
                kp = fc_scene.keyframe_points.insert(ptco[0], ptco[1])
                #need to edit tangents here
                kp.handle_left = pt.handle_left
                kp.handle_right = pt.handle_right
                kp.handle_left_type = pt.handle_left_type
                kp.handle_right_type = pt.handle_right_type
                kp.interpolation = pt.interpolation
    
            #handle addiditional fcurve attributes
            fc_scene.extrapolation = fc.extrapolation
            fc_scene.color_mode = fc.color_mode
            
        #go to current frame to make action creation stick
        curFrame = bpy.context.scene.frame_current
        bpy.context.scene.frame_set(curFrame)

    def _getSourceActions(self):
        """get all actions that use a source side bone
        """
        armature = self._armature
        
        result = []
        
        for act in bpy.data.actions:
            for fcrv in act.fcurves:
                if ".L" in fcrv.data_path:
                    result.append(act.name)
                    #dont need to check any more fcurves for this action
                    break
                    
        return result

    def _getAllActionConstraints(self):
        """get all action constraints
        """
        armature = self._armature
        result = []
        
        for bone in bpy.data.objects[armature].pose.bones:
            for cnt in bone.constraints:
                if cnt.type == "ACTION":
                    result.append(cnt.name)
        return result




#todo: have it support pose where theres no animation  
#todo: support if driver bone deleted in edit mode - i think its animation is still left over

class AnimationDriver(object):
    """a class for driving a pose in an animation by a driver bone
    
    assumes driver bone is animated at identical frames to pose and has a naming suffix see kDriverBoneSuffix
    
    when done animator should be able to pose left side lip corner.  hooks up lip corner up/dn/in/out to shapes
    for lip corner want up/dn/in/out shapes created from a single animation
    """
    """
    import imp
    testTool = imp.load_source("tool","/Users/Nathaniel/Documents/src_blender/python/riggingTools/faceTools/addJointBlendshape.py") #change to path to python file
    
    testTool.AnimationDriver("Armature", "Bone.001_macroAnim","ArmatureAction").doIt()
    
    #mp = testTool.AnimationDriver("Armature", "lipCorner_anim.L","LipCornerAction")
    #mp.doIt()
    
    """  
    kDriverBoneSuffix = "macroAnim"
    
    def __init__(self, armature, driverBone, actionName):
        """
        @param armature     str data object name of armature        
        @param driverBone   str name for driver bone of macro pose to be animated. it should have keyframes for its driving range at frames identical to pose
        (for simplicity assume a *_macroAnim.L or *_macroAnim.R suffix for driver bones)
        @param actionName   str name for action that has both pose and driver bone at different values for the different poses
        """
        self._armature = armature 
        #assert driverBone has proper naming
        if self.kDriverBoneSuffix not in driverBone:
            raise RuntimeError("requires driver bone name to have {} in its name".format(self.kDriverBoneSuffix))
            
        if driverBone not in bpy.data.objects[armature].pose.bones:
            raise RuntimeError("cannot find driver bone - please check its naming")
            
        self._driverBone = driverBone
        self._actionName = actionName
        
    def doIt(self):
        """
        """
        #turn on animation for action
        self._setAnimation(on=True)
        
        #get fcurve for driver bone
        #loop keyframe points of that fcurve
        #save frame of pose (assumes driver and pose have identical frames)
        #get axis and on point from driver bone (save driver value example axis 'z' value 3)
        
        driverInfo = self.getAllOnPointInfoForDriver() or []
        for dinfo in driverInfo:
            #for a single channel could have multiple frames
            #logger.debug(dinfo)
            for frameInfo in dinfo:
                self._setAnimation(on=True)
                
                logger.debug(frameInfo)
                frame = frameInfo.get("frame")
                onPoint = frameInfo.get("onPoint")
                channel = frameInfo.get("other")[0]
                axis = ["x","y","z"][frameInfo.get("other")[1]]
                logger.debug("frame:{frame} onPoint:{onPoint} channel:{channel} axis:{axis}".format(frame=frame, onPoint=onPoint, channel=channel, axis=axis))
                #ex: DEBUG:tool:frame:44.0 onPoint:-1.032 channel:location axis:z
                
                #go to frame
                self._goToFrame(frame)
                
                #turn off animation for action keeping pose
                self._setAnimation(on=False)
                
                #get list of all driver bones (for simplicity assume a certain suffix for driver bones)
                #zero out all driver bone ts/rs/sc - this is so it doesnt get driven by the pose we will create - since its in same armature
                #i think above will exclude driver bone(s) so it doesnt get driven
                self._setDriverBonesToDefault()
                
                #createAnimatablePose with the info  (needs a channel parameter to differentiate translates from rotates/scale)
                #todo: support rotation/scale drivers
                #todo: dont loose initial driver animation
                createAnimatablePose(self._armature, self._driverBone, self._actionName, onPoint=(onPoint, axis), offPoint=(0, axis), channel=channel )
                #ex: createAnimatablePose(armature, driverBone, "lipUp_Action.L", onPoint=(2,'z'), offPoint=(0,'z') )
                
        #cleanup
        self._goToFrame(0)
        self._setAnimation(on=False)
        
        """
        #step through each frame of animationAction - turnoff animation keeping pose - create the animatable pose
        self.setAnimation(on=True) #turn on animation for animationAction - shouldnt need to do first time arround
        self.goToFrame(frame=fr)
        self.setAnimation(on=False)
        createAnimatablePose(armature, driverBone, "lipUp_Action.L", onPoint=(2,'z'), offPoint=(0,'z')) #armature, driver bone, name for action, when blendshape on using driver bone
        """
    
    def _setAnimation(self, on=True):
        """set animation on or off - by turning off have a static posed armature on the frame it was turned off
        @param on   bool whether to turn on current action or temporarily turn it off - doesnt delete the animation
        """
        if on:
            bpy.data.objects[self._armature].animation_data.action = bpy.data.actions[self._actionName]
        else:
            bpy.data.objects[self._armature].animation_data.action = None
        
    def _getFcurvesForDriver(self):
        """get all fcurves for driver bone
        
        a few fcurves for a single bone
        suppose bone ty fcurve is (0,0) (10,2) (20,0) (30,-2)
        and     bone tz fcurve is (0,0) (10,0) (20,5) (30,0)
        """
        allFcurves = bpy.data.objects[self._armature].animation_data.action.fcurves
        return [ fcrv for fcrv in allFcurves if self._driverBone == re.search( r'.*\["(.*)"\].*', fcrv.data_path ).group(1) ] #ex data path: 'pose.bones["Bone"].location'
    
    def getAllOnPointInfoForDriver(self):
        """get the info needed from the driver that can be used for driving poses
        a few fcurves for a single bone
        suppose bone ty fcurve is (0,0) (10,2) (20,0) (30,-2)
        and     bone tz fcurve is (0,0) (10,0) (20,5) (30,0)  
        
        get a list like:
        [{"frame":10,"axis":'y',"onPoint":2,"channel":"t"},{"frame":30,"axis":'y',"onPoint":-2,"channel":"t"}]
        
        for each fcurve - need to include channel either t, r, s (only supporting euler rotation for drivers)
        
        @param fcurves  list of fcurve objects for driver bone
        ex item: >>> type(bpy.data.objects['Armature'].animation_data.action.fcurves[1])
        

        start at first fcurve
        ty fcurve
        start at first keyframe point of fcurve - any thing non default/ non zero? no
        got to next keyframe point of fcurve - any thing non zero? yes - so save the frame 10 and the value 2 - save this dict {"frame":10,"axis":'y',"onPoint":2} to list
        go to next keyframe point - any thin non default? no 
        go to next keyframe point - ... save {"frame":30,"axis":'y',"onPoint":-2}
        no more keyframe points so stop        
        """
        result = []
        fcurves = self._getFcurvesForDriver()
        #assert fcurves
        for fcrv in fcurves:
            fcrvInfo = []
            logger.debug(fcrv.data_path)
            channel = fcrv.data_path.split('.')[-1] #ex: "location"
            for kpoint in fcrv.keyframe_points:
                #logger.debug("channel:{}".format(channel))
                #skip default keyframe points
                if (kpoint.co[1] == 0 and channel != "scale") or (kpoint.co[1] == 1 and channel == "scale"):
                    continue
                
                onValue = kpoint.co[1]
                #if rotation convert radians to degrees
                if channel == "rotation_euler":
                    onValue = onValue*(180/math.pi)
                
                driver_info = dict(frame=kpoint.co[0], onPoint= round(onValue,3), other=(channel, fcrv.array_index)  ) #other ex: ("location",1) for ty
                fcrvInfo.append(driver_info)
            if fcrvInfo:
                result.append(fcrvInfo)        
    
        logger.debug(result)
        #ex:
        """
        [[{'other': ('location', 1), 'onPoint': 1.044, 'frame': 14.0}, {'other': ('location', 1), 'onPoint': -1.239, 'frame': 26.0}]]
        """
        
        return result
        
    
    def _getAllDriverBones(self):
        """get list of all driver bones in armature (for simplicity assumes a certain suffix for driver bones)
        """
        return [bone.name for bone in bpy.data.objects[self._armature].pose.bones if self.kDriverBoneSuffix in bone.name  ]
    
    def _setDriverBonesToDefault(self):
        """zero out all driver bone ts/rs/sc - this is so it doesnt get driven by the pose we will create - since its in same armature
        """
        allDriverBones = self._getAllDriverBones() or []
        for dbone in allDriverBones:
            logger.debug(dbone)
            logger.info("setting driver bone {} to default".format(dbone))
            bpy.data.objects[self._armature].pose.bones[dbone].location=(0.0,0.0,0.0)
            bpy.data.objects[self._armature].pose.bones[dbone].rotation_euler=(0.0,0.0,0.0)
            bpy.data.objects[self._armature].pose.bones[dbone].scale=(1.0,1.0,1.0)
                
    def _goToFrame(self, frame=0):
        """go to given frame number
        @param frame    float frame to go to
        """
        bpy.context.scene.frame_set(frame)




class PoseCreate(object):
    """class that does meat for creating a pose based on actions.
    its recommended to not use this directly but instead use a PoseCreateApplication
    """
    def __init__(self,  action, 
                        armature,
                        animbone,
                        onFrame=10, 
                        offFrame=0,
                        onPoint=(10,'z'),
                        offPoint=(0,'z'),
                        channel="location"
                ):
        """
        @param action: (str) name of action
        @param armature: (str) name of armature its object name
        @param animbone: (str) name of bone to be animated to turn on pose         
        @param onFrame: (double) frame that joint blendshape is on in action (default 10)
        @param offFrame: (double) frame that joint blendshape is off in action (default 0)
        @param onPoint: tuple (double,str) ex:  (10,z) it says when bone is at 10 in given channel in z axis blendshape should be on. 
        for rotation it requires degrees input
        @param offPoint: tuple (double,str) ex: (0,z) it says when bone is at 0 in z axis blendshape should be off
        @param channel:  (str) channel ex: location or rotation_euler      
        """
        #essential variables
        self._action = action
        self._armature = armature
        self._animbone = animbone
        self._actionConstraintDict = {} #keys bone affected - value is action constraint name - it will be created
        
        #optional variables
        self.__onFrame = onFrame
        self.__offFrame = offFrame
        self.__onPoint = onPoint
        self.__offPoint = offPoint
        self.__channel = channel
        
    def doIt(self):
        #assign action to affected bones
        self._assignActionToAffectedBones()
        
        #set proper action frame inputs
        status = self._setActionInputs()

        utils.turnOffActions(self._armature) #example go to first frame and turn off actions.
        return True

    def _assignActionToAffectedBones(self):
        """
        create an action constraint for each affected bone
        """
        affectedbones = self._getActionBones() or []
        logger.info("affectedbones:")
        logger.info(affectedbones)
        armature = self._armature
        action = self._action
        
        #create action constraint for each bone
        for bone in affectedbones:
            act = bpy.data.objects[armature].pose.bones[bone].constraints.new('ACTION')
            act.name = action+'_'+'cnt'
            self._actionConstraintDict[bone] = act.name
            logger.info("creating action constraint on bone {0} constraint name {1}".format(bone, act.name))

    def _setActionInputs(self):
        """sets the inputs (action,frame data) to all created action constraints
        """
        actionConstraintDict = self._actionConstraintDict
        armature = self._armature
        action = self._action
        animbone = self._animbone
        onPoint = self.__onPoint
        offPoint = self.__offPoint
        onFrame = self.__onFrame
        offFrame = self.__offFrame
        
        animaxis = onPoint[1].upper() #ex: Z
        channelType = "LOCATION"
        if self.__channel == "rotation_euler":
            channelType = "ROTATION"
        elif self.__channel == "scale":
            channelType = "SCALE"
        #no support for quaternion - i dont think its available for action constraints
        
        if not self._actionConstraintDict:
            logger.info("no pose found. doing nothing")
            return False

        assert armature
        assert action
                
        for bone,cnt in actionConstraintDict.items():
            logger.info("cnt: {0} bone: {1}".format(cnt,bone))
            cntObj = bpy.data.objects[armature].pose.bones[bone].constraints[cnt]
            cntObj.target = bpy.data.objects[armature]
            cntObj.subtarget = animbone #bone animator animates to achieve pose
            cntObj.target_space = 'LOCAL'
            cntObj.transform_channel = channelType+"_"+animaxis #'LOCATION_'+animaxis
            cntObj.min = offPoint[0] #for animator
            cntObj.max = onPoint[0]
            cntObj.frame_start = offFrame #for action
            cntObj.frame_end = onFrame
            logger.info("action name")
            logger.info(action)
            cntObj.action = bpy.data.actions[action]
            
        return True    

    def _getActionBones(self):
        #get bones involved in action
        #@return (list of str) of bone names
        
        result = []
        action = self._action
        fcurves = bpy.data.actions[action].fcurves
        
        for fcurve in fcurves:
            #find the bone name from data path that looks like: 'pose.bones["lip_up_tail.L"].location'
            result.append( re.search( r'.*\["(.*)"\].*', fcurve.data_path ).group(1) )
        
        return list(set(result))
        
    """    
    def _getActionBones(self):
        #get bones involved in action
        #@return (list of str) of bone names
        
        result = []
        action = self._action
        result = [actgrp.name for actgrp in bpy.data.actions[action].groups]
        return result
     """


#action creation class
class ActionCreator(object):
    """for creating an action out of a static pose.
    todo: be able to create actions out of animated poses - one action for each keyframe
    todo: able to not have to keyframe all location channels but just ones changed in pose
    """
    kDefaultTranslates = [0.0,0.0,0.0]
    kDefaultEuler = [0.0,0.0,0.0]
    kDefaultQuats = [1.0,0.0,0.0,0.0]
    
    def __init__(self, name, armature='', isolateBones=[]):
        """
        @param name (str) name to use for action
        @param armature (str) data object name for armature
        @param isolateBones (list of str) optional list of bone names to only consider - if not specified it looks for posed bones in entire armature
        """
        assert armature
        self.armature = armature
        self.name = name
        self._action = None
        self._bonesToDataPathDict = {}  #bones used in action and what attribute trs,rots,or scale should be keyframed
                                        #ex: {'Bone': {'location':(0,2,1),'rotation':(90,0,0)}, 'Bone.001': {'location':(10,0,0)} }
        self._isolateBones = isolateBones
        
    def doIt(self):
        #figure out bones to use in action
        self._getBonesForAction()
        #print(self._bonesToDataPathDict)
        
        #creates action/fcurves/ and keys the bones used in action in a progression from default to pose
        self._setKeyframes()
        
        #to register the action
        bpy.ops.object.mode_set(mode="OBJECT") #todo: make cleaner
        
        return self._action
        
    def _getBonesForAction(self):
        """figure out bones to use in action (also include data path)
        (todo: support modes not starting in pose)
        """
        self._bones = []
        armatureObj = bpy.data.objects[self.armature]

        #go through all bones of armature and save any bone whose trs,rots,scales are different from default
        #also save the transform for the pose. (todo: support poses that are animated - assumes pose is static)
        for bone in armatureObj.pose.bones:
            #if isolating bones skip any bones not in isolate list
            isoBones = self._isolateBones or []
            if isoBones and (bone.name not in isoBones):
                continue
                
            #if bone translate not equal default 
            if not (tuple(bone.location) == tuple(self.kDefaultTranslates)):
                self._bonesToDataPathDict.setdefault(bone.name,{}).setdefault('location',[]).extend( list(bone.location) )
                #ex: {'Bone.001': {'location': [0.0, 0.76, 0.0]}, 'Bone': {'location': [0.0, 0.38, 0.0]}}

            if bone.rotation_mode == 'QUATERNION':
                if not (tuple(bone.rotation_quaternion) == tuple(self.kDefaultQuats)):
                    self._bonesToDataPathDict.setdefault(bone.name,{}).setdefault('rotation_quaternion',[]).extend( list(bone.rotation_quaternion) )
            else:
                if not (tuple(bone.rotation_euler) == tuple(self.kDefaultEuler)):
                    self._bonesToDataPathDict.setdefault(bone.name,{}).setdefault('rotation_euler',[]).extend(list(bone.rotation_euler))               
                
    def _setKeyframes(self):
        """key the bones used in action in a progression from default to pose
        """
        boneDict = self._bonesToDataPathDict #ex: {'Bone': ['location'], 'Bone.001': ['location']}
        #print("boneDict:")
        #print(boneDict)
        startFrame = 0 
        endFrame = 10 #todo: support different frame ranges
        armatureObj = bpy.data.objects[self.armature]
        actionNamePrefix = self.name
        
        armatureObj.animation_data_clear() #todo: see if this is needed
        #create a new action on object. for now ignoring additional way 
        #to find action name to prevent trailing digits on action name.
        armatureObj.animation_data_create() #so we have .action attribute
        self._action = bpy.data.actions.new(name=actionNamePrefix) #name= actionNamePrefix+'Action'
        armatureObj.animation_data.action = self._action
        
        #make fcurves and add keyframes
        for bone, attrDict in boneDict.items():
            for attr, transform in attrDict.items():
                print("bone:{0} attr:{1}".format(bone,attr))
                #create fcurve for all location channels
                print("creating fcurve")
                fcurveArrayIndexRange = [j for j in range(0,len(transform))] #todo: make more specific - so not using all axis
                for i in fcurveArrayIndexRange:
                    fcrv = armatureObj.animation_data.action.fcurves.new('pose.bones["{bone}"].{attr}'.format(bone=bone, attr=attr),i)
                    print("creating keyframes for action")
                    #handle first frame
                    valueFirst = 0 #the default is at 0 for loc scal roteuler
                    if attr == "rotation_quaternion" and i == 0:
                        valueFirst = 1 #quaternion default for first index should be 1
                    valueLast = transform[i]
                    fcrv.keyframe_points.insert(startFrame,valueFirst) #frame, value
                    #handle last frame
                    fcrv.keyframe_points.insert(endFrame,valueLast)
        


#editing.py
import re
import logging

import bpy
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)


#public method to add to addon
def completeMissingConstraintsBySourceBone(armatureName='', sourceBoneName='', actionName=''):
    """
    Args:
        sourceBoneName (str): name of source bone that has constraints we want to apply to other bones
    """
    #if was going to use selection
    #armatureName = bpy.context.object.name
    #actionName = bpy.data.objects[armatureName].animation_data.action.name

    bonesMissingActionConstraints = getBonesInActionMissingSourceConstraint(armatureName=armatureName, actionName=actionName, sourceBoneName=sourceBoneName)

    if not bonesMissingActionConstraints:
        return False

    #copy source constraints to bones missing action constraints
    cc = ConstraintCopier(armatureName=armatureName, constraintType="ACTION")
    cc.copyConstraint(sourceBoneName=sourceBoneName, destinationBoneNames=bonesMissingActionConstraints)

    return True

#could move these to utils
def getBonesInAction(actionName=''):
    """get list of all bone names with at least one animation curve in given action.
    #ex:
    print(getBonesInAction('testSimple.C'))

    Returns:
        List(str): for names of bones with at least one animation curve in given action
    """
    if not actionName in bpy.data.actions:
        logger.warning("could not find action. cannot find bones in action")
        return []
    dataPaths = [fcurve.data_path for fcurve in bpy.data.actions[actionName].fcurves]
    bonesInAction = [re.match('.*\["(.*)"\].*', dataPath).groups()[0] for dataPath in dataPaths] #'pose.bones["Bone"].location'
    result = list(set(bonesInAction))

    return result

def getBonesInActionMissingSourceConstraint(armatureName='', actionName='', sourceBoneName=''):
    """get all bones in action missing a constraint that source bone has

    #ex:
    #print(getBonesInActionMissingSourceConstraint(armatureName='Armature', actionName='testSimple.C', sourceBoneName='Bone.002'))

    Args:
        armatureName (str): armature data object name
        actionName (str): action name
        sourceBoneName (str): source bone name
    Returns:
        List(str): for names of bones in action that are missing at least one source bone constraint
    """
    result = []
    bonesInAction = getBonesInAction(actionName=actionName) or []
    if not bonesInAction:
        return []

    sourceBone = bpy.data.objects[armatureName].pose.bones[sourceBoneName]
    sourceConstraintNames = [cnt.name for cnt in sourceBone.constraints]

    for bone in bonesInAction:
        #if bone missing constraint save it
        boneObj = bpy.data.objects[armatureName].pose.bones[bone]
        bonesConstraints = [cnt.name for cnt in boneObj.constraints]
        if set(sourceConstraintNames) != set(bonesConstraints):
            result.append(bone)

    result = list(set(result)) #remove duplicates
    return result


#end move to utils

def activateAction(armatureName='', actionName=''):
    """activate given action
    """
    if (not armatureName) or (not actionName):
        logger.warning("couldnt find action or armature. skipping activating action.")
        return False
    #go to first frame in case an action was already on
    bpy.context.scene.frame_set(0)

    #clear current pose before turning on action
    bpy.ops.pose.select_all(action='DESELECT')
    bpy.ops.pose.select_all(True)
    bpy.ops.pose.transforms_clear()
    bpy.data.objects[armatureName].animation_data.action = bpy.data.actions[actionName]
    bpy.ops.pose.select_all(action='DESELECT')

    return True

class ActionFinder(object):
    """an api to help find action constraints or actions.
    it requires some inputs to be set first.

    example usage:
    import sys
    sys.path.append('/Users/Nathaniel/Documents/src_blender/python/riggingTools/faceTools/naActionTools')
    import editing

    actFinder = editing.ActionFinder()
    actFinder.setArmature("Armature")
    actFinder.setAnimBone("browAllAnim.L")  #animator control
    actFinder.setTransformChannel("LOCATION_Y") #channel and axis control movement turns on pose
    actFinder.setIsPositive(True)   #set whether moving control in positive direction turns on pose
    actFinder.doIt() #needs to be called before querying action constraint
    print("action constraint found:")
    print(actFinder.getActionConstraint())
    print("action found:")
    print(actFinder.getAction())
    """
    def __init__(self):
        self._action = '' #this is the action name that can be isolated
        self._actionConstraint = '' #this is action constraint name computed from inputs

        #for computing variables
        self._armature = ''
        self._animBone = ''
        self._transformChannel = '' #ex: 'LOCATION_Y'  ex: 'ROTATION_Z'
        self._isPositive = True

    def doIt(self):
        #figure out action constraint to find
        self._computeActionConstraintForAnimBone()

        #figure out action to find
        self._computeAction()

    def setArmature(self, value):
        self._armature = value
    def setAnimBone(self, value):
        self._animBone = value
    def setTransformChannel(self, value):
        self._transformChannel = value
    def setIsPositive(self, value):
        self._isPositive = value


    def getAction(self):
        return self._action

    def getActionConstraint(self):
        return self._actionConstraint

    def _computeActionConstraintForAnimBone(self):
        #loop all action constraints
        #find ones with driver bone and axis and direction
        #return first action constraint found with match
        armature = self._armature
        animBone = self._animBone
        transformChannel = self._transformChannel
        isPositive = self._isPositive

        #assert inputs
        if (not armature) or (not animBone) or (not transformChannel):
            logger.warning("missing inputs. skipping computing of action constraint")
            return


        #find action constraint from pose bones
        for bone in bpy.data.objects[armature].pose.bones:
            for constraint in bone.constraints:
                if constraint.type != 'ACTION':
                    continue
                if constraint.subtarget != animBone:
                    continue
                if constraint.transform_channel != transformChannel:
                    continue
                #check direction
                isConstraintPositive = True if constraint.max >= 0 else False
                if isPositive != isConstraintPositive:
                    continue
                #we found our constraint
                self._actionConstraint = constraint.name
                #we dont need to check any others
                return

    def _computeAction(self):
        """use found action constraint and finds its action name
        """
        armature = self._armature

        for bone in bpy.data.objects[armature].pose.bones:
            for constraint in bone.constraints:
                if constraint.name == self._actionConstraint:
                    self._action = constraint.action.name
                    #we dont need to check any other actions
                    return


class ConstraintCopier(object):
    """responsible for copying specified constraint type from source bone to destination bone
    #example usage
    cc = ConstraintCopier(armatureName="Armature", constraintType="ACTION")
    cc.copyConstraint(sourceBoneName="boneA", destinationBoneNames=["bone.001"])
    """
    def __init__(self, armatureName='', constraintType='ACTION'):
        """
        Args:
            armatureName (str): armature data object name
            constraintType (str): constraint type name ex: 'ACTION'
        """
        self._armatureName = armatureName
        self._constraintType = constraintType

    #public api
    def setConstraintType(self, constraintType):
        if not constraintType:
            logger.warning("not valid constraint type")
            return
        self._constraintType = constraintType

    def setArmature(self, armatureName):
        if not armatureName in bpy.data.objects:
            logger.warning("not valid armature data object name")
            return
        self._armatureName = armatureName       

    def copyConstraint(self, sourceBoneName='', destinationBoneNames=[]):
        """copy constraints from a source bone to destination bone
        Args:
            sourceBoneName (str): source bone name
            destinationBoneName (List[str]): list of destination bone names
        """
        armatureName = self._armatureName
        constraintType = self._constraintType

        for dBone in destinationBoneNames:
            if dBone not in bpy.data.objects[armatureName].pose.bones:
                logger.warning("could not find destination bone {}".format(dBone))
                return

        if not armatureName in bpy.data.objects or not sourceBoneName in bpy.data.objects[armatureName].pose.bones:
            logger.warning("could not find inputs. exiting copying constraints")
            return

        sourceBone = bpy.data.objects[armatureName].pose.bones[sourceBoneName]
        srcActionConstraints = [cnt for cnt in sourceBone.constraints if cnt.type == constraintType]
        if not srcActionConstraints:
            logger.warning("could not find any constraints to copy on source bone {}. doing nothing".format(sourceBoneName))
            return

        #loop through all destination bones copying source bone constraint to destination bone
        for destinationBoneName in destinationBoneNames:
            destinationBone = bpy.data.objects[armatureName].pose.bones[destinationBoneName]
            for cnt in srcActionConstraints:
                #skip creating constraint if constraint already exists on destination bone
                if cnt.name in [destCnt.name for destCnt in destinationBone.constraints]:
                    new_cnt = destinationBone.constraints[cnt.name]
                else:
                    new_cnt = destinationBone.constraints.new(cnt.type)
                #destination constraint name same as source

                #copy attributes of constraint
                for prop in dir(cnt):
                    #dont copy target
                    if prop == "target":
                        setattr(new_cnt, prop, bpy.data.objects[armatureName])
                        continue
                    try:
                        setattr(new_cnt, prop, getattr(cnt, prop))
                    except:
                        pass       


class ActionBoneSelector(object):
    """has api to be able to make bone selections related to bones in action
    """
    def __init__(self):
        self._obj = bpy.context.active_object
        self._action = self._obj.animation_data.action

    def selectLeftSideBones(self):
        """select all left side bones in action
        """
        bonesInActionLeftSide = self.getLeftSideBones() or []
        if not bonesInActionLeftSide:
            return False

        #select them
        self._selectBones(bonesInActionLeftSide)

        return True

    def selectRightSideBones(self):
        """select all right side bones for all left side bones in action
        """
        bonesInActionLeftSide = self.getLeftSideBones() or []
        if not bonesInActionLeftSide:
            return False

        #get all right side bones
        bonesRightSide = [bone.replace('.L', '.R') for bone in bonesInActionLeftSide]
        #print('right side', bonesRightSide)

        #select them
        self._selectBones(bonesRightSide)

        return True

    def getLeftSideBones(self):
        """
        Returns:
            (list[str]): names of all bones on left side of armature
        """
        obj = self._obj
        action = self._action

        dataPaths = [fcurve.data_path for fcurve in action.fcurves]
        #print(dataPaths)
        bonesInAction = [re.match('.*\["(.*)"\].*', dataPath).groups()[0] for dataPath in dataPaths] #'pose.bones["Bone"].location'
        bonesInAction = list(set(bonesInAction)) #remove duplicates
        bonesInActionLeftSide = [b for b in bonesInAction if b.endswith('.L')]

        return bonesInActionLeftSide     

    def _selectBones(self, bones=[]):
        if not bones:
            return
        obj = self._obj
        #deselect all pose bones
        bpy.ops.pose.select_all(action='DESELECT')
        for b in bones:
            obj.pose.bones[b].bone.select = True
        ########        

class MirrorPose(object):
    """has tools for mirroring a single pose of armature. not no keyframes are set
    """
    def __init__(self):
        pass

    def doIt(self):
        """primary method to be called by client tools
        """
        #select left side bones of action first.
        boneSelector = ActionBoneSelector()
        boneSelector.selectLeftSideBones()
        self.mirrorPoseOfSelected()

    def mirrorPoseOfSelected(self):
        """mirror pose for selected bones. ex select .L side bones first.
        """
        #todo: do nothing if no bones selected
        bpy.ops.pose.copy()
        bpy.ops.pose.paste(flipped=True)


"""
import bpy
import imp
cntTool = imp.load_source("tool", "/Users/Nathaniel/Documents/src_blender/python/riggingTools/faceTools/naActionTools/editing.py")
cc = cntTool.ConstraintCopier(armatureName="Armature", constraintType="ACTION")
cc.copyConstraint(sourceBoneName="Bone.002", destinationBoneNames=["Bone"])

"""
    

#inspired by
#blender dot stack exchange dot com ‚How to copy constraints from one bone to another post

#utils.py
import bpy
import logging

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG) #without this info logs wouldnt show in console

def getTransformChannelData(armature, bone):
	"""
	example
	getTransformChannelData("Armature", "lipCornerAnim.L") #returns ("LOCATION_Y", "True")
	"""
	attrsData = [("location.x", "LOCATION_X"), 
				("location.y", "LOCATION_Y"), 
				("location.z", "LOCATION_Z"), 
				("rotation_euler.x", "ROTATION_X"), #not supporting quaterion
				("rotation_euler.y", "ROTATION_Y"),
				("rotation_euler.z", "ROTATION_Z")] #todo add scale support.
	poseBone = bpy.data.objects[armature].pose.bones[bone] #no error checking

	for attrDat in attrsData:
		attr, transformChannel = attrDat
		val = eval("poseBone.{0}".format(attr))
		direction = "True" if val >= 0 else "False"
		#if its not default return it
		if (val > 0.0) or (val < 0.0):
			return (transformChannel, direction)


	return None

def turnOffActions(armature):
	"""go to 0 frame and turn off active action.
	"""
	bpy.context.scene.frame_set(0)
	#unlink action. make action not active, without deleting it.
	bpy.data.objects[armature].animation_data.action = None
#ui.py
import bpy

from bpy.props import(
    StringProperty,
    PointerProperty,
    EnumProperty
    )

from bpy.types import(
    Operator,
    Panel,
    PropertyGroup
    )

from . import creating
from . import editing
from . import utils
import importlib
importlib.reload(creating)
importlib.reload(editing)
importlib.reload(utils)


class ActionPanel(Panel):
	bl_label = "Action Panel"
	bl_space_type = "VIEW_3D"
	bl_region_type = "UI"

	def draw(self, context):
		layout = self.layout
		layout.label(text="Action Creating/Editing Tool")

		
		layout.label(text="-"*80)
		layout.label(text="Creating Section:")
		layout.label(text="first pose static mesh to posed shape with no keyframes")
		#add text fields
		layout.prop(context.scene.action_prop, "actionName", text="actionName")
		layout.prop(context.scene.action_prop, "armatureName", text="armatureName")
		layout.prop(context.scene.action_prop, "animBone", text="animBone")
		layout.prop(context.scene.action_prop, "onPointValue", text="onPointValue")
		layout.prop(context.scene.action_prop, "onPointAxis", text="onPointAxis")
		layout.prop(context.scene.action_prop, "onFrame", text="onFrame")

		#button
		layout.operator("obj.do_actioncreate") #uses bl_idname

		layout.label(text="-"*80)
		layout.label(text="Mirroring Section:")
		layout.prop(context.scene.action_prop, "actionName", text="actionName")
		layout.prop(context.scene.action_prop, "armatureName", text="armatureName")
		layout.operator("obj.do_actionmirror")

		layout.label(text="-"*80)
		layout.label(text="Editing Section:")
		layout.operator("obj.do_editingloadselected")
		layout.prop(context.scene.action_prop, "armatureName", text="armatureName")
		layout.prop(context.scene.action_prop, "animBone", text="animBone")
		layout.prop(context.scene.action_prop, "transformChannel", text="transformChannel")
		layout.prop(context.scene.action_prop, "isPositive", text="isPositive")
		layout.operator("obj.do_editselectaction")

		layout.label(text="Adding Missing Constraints:")
		layout.prop(context.scene.action_prop, "actionName", text="actionName")
		layout.prop(context.scene.action_prop, "armatureName", text="armatureName")
		layout.prop(context.scene.action_prop, "sourceBoneNameForConstraints", text="sourceBone")
		layout.operator("obj.do_editcompleteconstraint")
		

		layout.label(text="Tools for editing pose of action:")
		layout.operator("obj.do_editmirrorpose")
		layout.operator("obj.do_editselectrightsidebones")

class ActionProperties(PropertyGroup):
	actionName = StringProperty(
		name="actionName",
		description="action name ex: lipUUUAction.C"
		)
	armatureName = StringProperty(
		name="armatureName",
		description="armature name"
		)
	animBone = StringProperty(
		name="animBone",
		description="animator control bone used to turn on pose ex: lipUUUAnim.C"
		)
	onPointValue = StringProperty(
		name="onPointValue",
		default="1",
		description="what value in channel box of animator control turns on pose ex: 1"
		)
	onPointAxis = StringProperty(
		name="onPointAxis",
		default="y",
		description="axis for animator control to turn on pose ex: y"
		)
	onFrame = StringProperty(
		name="onFrame",
		default="10",
		description="what frame in the action has the mesh posed. ex: 10"
		)

	#editing
	transformChannel = EnumProperty(
		name="transformChannel",
		description="transform channel for animator control driving action",
		items=[ ("LOCATION_X", "LOCATION_X", ""),
				("LOCATION_Y", "LOCATION_Y", ""),
				("LOCATION_Z", "LOCATION_Z", ""),
				("ROTATION_X", "ROTATION_X", ""),
				("ROTATION_Y", "ROTATION_Y", ""),
				("ROTATION_Z", "ROTATION_Z", "")
		],
		default="LOCATION_Y"
		)
	isPositive = EnumProperty(
		name="isPositive",
		description="is direction positive to drive action",
		items=[ ("True", "True", ""),
				("False", "False", "")
		],
		default="True" 
		)

	
	sourceBoneNameForConstraints = StringProperty(
		name="sourceBone",
		description="source bone name that has constraints we wish to apply to other bones ex: jaw.C"
		)

class ActionCreateOperator(Operator):
	"""create action. first pose static mesh to posed shape with no keyframes, fill in values then click create
	"""
	bl_idname = "obj.do_actioncreate"
	bl_label = "Action Create"
	bl_options = {"REGISTER"}

	def execute(self, context):
		self.report({'INFO'}, "Creating Action")

		actionName = context.scene.action_prop.actionName #"lipUUUAction.C"
		armatureName = context.scene.action_prop.armatureName #"Armature"
		onPointValue = int(context.scene.action_prop.onPointValue) #1
		onPointAxis = context.scene.action_prop.onPointAxis #'y'
		onPoint = (onPointValue, onPointAxis) #(1,'y') #animator control movement for pose
		animBone = context.scene.action_prop.animBone
		onFrame = int(context.scene.action_prop.onFrame)

		ac = creating.ActionCreator(actionName, armature=armatureName)
		ac.doIt()

		pc = creating.PoseCreate(actionName,
		                armatureName,
		                animbone=animBone,#"lipUUUAnim.C",
		                onFrame=onFrame,
		                offFrame=0,
		                onPoint=onPoint,
		                offPoint=(0,onPointAxis))
		                
		pc.doIt()

		return {'FINISHED'}


class ActionMirrorOperator(Operator):
	"""mirror given action and action constraints
	"""
	bl_idname = "obj.do_actionmirror"
	bl_label = "Action Mirror"
	bl_options = {"REGISTER"}

	def execute(self, context):
		self.report({'INFO'}, "Mirroring Action")

		actionName = context.scene.action_prop.actionName #"lipUUUAction.C"
		armatureName = context.scene.action_prop.armatureName #"Armature"
		
		creating.PoseMirror(armatureName).mirrorActions(actionNames=[actionName], replace=True)
		creating.PoseMirror(armatureName).mirrorActionConstraints()

		return {'FINISHED'}

class ActionEditingSelectOperator(Operator):
	"""select a computed action for quicker editing of existing actions.
	"""
	bl_idname = "obj.do_editselectaction"
	bl_label = "Load Action"
	bl_options = {"REGISTER"}

	def execute(self, context):
		self.report({'INFO'}, "Editing Action")

		actionName = context.scene.action_prop.actionName #"lipUUUAction.C"
		armatureName = context.scene.action_prop.armatureName #"Armature"
		animBone = context.scene.action_prop.animBone #"browAllAnim.L"
		transformChannel = context.scene.action_prop.transformChannel #"LOCATION_Y"
		isPositive = True if context.scene.action_prop.isPositive == "True" else False #True

		actFinder = editing.ActionFinder()
		actFinder.setArmature(armatureName)
		actFinder.setAnimBone(animBone)  #animator control
		actFinder.setTransformChannel(transformChannel) #channel and axis control movement turns on pose
		actFinder.setIsPositive(isPositive)   #set whether moving control in positive direction turns on pose
		actFinder.doIt() #needs to be called before querying action constraint
		#print("action constraint found:")
		#print(actFinder.getActionConstraint())
		#print("action found:")
		#print(actFinder.getAction())
		actionName = actFinder.getAction()

		if not actionName:
			self.report({'INFO'}, "Couldnt find action for given inputs. doing nothing")
		else:
			#turn on computed action
			editing.activateAction(armatureName=armatureName, actionName=actionName)

		return {'FINISHED'}

class ActionEditingLoadSelectedOperator(Operator):
	"""load selected bone. first pose bone to get direction.
	"""
	bl_idname = "obj.do_editingloadselected"
	bl_label = "Load Selected Bone"

	def execute(self, context):
		self.report({'INFO'}, "Load Selected Bone")

		if not context.selected_pose_bones:
			self.report({'INFO'}, "requires a single selected pose bone. doing nothing")
			return {'FINISHED'}

		#compute values to use from selection
		armatureName = context.selected_objects[0].name
		animBone = context.selected_pose_bones[0].name
		transformData = utils.getTransformChannelData(armatureName, animBone)
		if not transformData:
			self.report({'INFO'}, "requires selected bone to be nudged in a single axis to determine transform channel and direction. doing nothing")
			return {'FINISHED'}

		transformChannel = ""
		isPositive = "True"
		if transformData:
			transformChannel, isPositive = transformData

		context.scene.action_prop.armatureName = armatureName #'TestArmature'
		context.scene.action_prop.animBone = animBone#'TestAnimBone'
		context.scene.action_prop.transformChannel = transformChannel #"LOCATION_Z"
		context.scene.action_prop.isPositive = isPositive #"True"

		return {'FINISHED'}


class ActionEditingCompleteConstraintOperator(Operator):
	"""add action constraints to all bones in action missing them. using a source bone
	"""
	bl_idname = "obj.do_editcompleteconstraint"
	bl_label = "Add Action Constraints to Missing Bones"
	bl_options = {"REGISTER"}

	def execute(self, context):
		self.report({'INFO'}, "Editing Action")

		actionName = context.scene.action_prop.actionName #"lipUUUAction.C"
		armatureName = context.scene.action_prop.armatureName #"Armature"
		sourceBone = context.scene.action_prop.sourceBoneNameForConstraints #"browAllAnim.L"

		editing.completeMissingConstraintsBySourceBone(armatureName=armatureName, sourceBoneName=sourceBone, actionName=actionName)
		return {'FINISHED'}

class ActionEditingMirrorPoseOperator(Operator):
	"""mirror pose
	"""
	bl_idname = "obj.do_editmirrorpose"
	bl_label = "Mirror pose for current action"
	bl_options = {"REGISTER"}

	def execute(self, context):
		self.report({'INFO'}, "Mirroring single pose")
		mirPose = editing.MirrorPose()
		mirPose.doIt()
		return {'FINISHED'}

class ActionEditingSelectRightSideBonesOperator(Operator):
	"""select right side bones
	"""
	bl_idname = "obj.do_editselectrightsidebones"
	bl_label = "Select all right side bones of current action"
	bl_options = {"REGISTER"}

	def execute(self, context):
		self.report({'INFO'}, "Selecting right side bones")
		boneSelector = editing.ActionBoneSelector()
		boneSelector.selectRightSideBones()
		return {'FINISHED'}

def register():
	bpy.utils.register_class(ActionCreateOperator)
	bpy.utils.register_class(ActionMirrorOperator)
	bpy.utils.register_class(ActionEditingSelectOperator)
	bpy.utils.register_class(ActionEditingLoadSelectedOperator)
	bpy.utils.register_class(ActionEditingCompleteConstraintOperator)
	bpy.utils.register_class(ActionEditingMirrorPoseOperator)
	bpy.utils.register_class(ActionEditingSelectRightSideBonesOperator)
	bpy.utils.register_class(ActionProperties)
	bpy.utils.register_class(ActionPanel)

	#name property that holds text fields
	bpy.types.Scene.action_prop = PointerProperty(type=ActionProperties)

def unregister():
	bpy.utils.unregister_class(ActionCreateOperator)
	bpy.utils.unregister_class(ActionMirrorOperator)
	bpy.utils.unregister_class(ActionEditingSelectOperator)
	bpy.utils.unregister_class(ActionEditingLoadSelectedOperator)
	bpy.utils.unregister_class(ActionEditingCompleteConstraintOperator)
	bpy.utils.unregister_class(ActionEditingMirrorPoseOperator)
	bpy.utils.unregister_class(ActionEditingSelectRightSideBonesOperator)	
	bpy.utils.unregister_class(ActionProperties)
	bpy.utils.unregister_class(ActionPanel)

	del bpy.types.Scene.action_prop