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

Thursday, October 16, 2025

Unreal Engine control rig blendshape doodle using Blender blendshapes

i'm still very new to Unreal Engine's control rig but thought this doodle may be helpful for learning.  there may be better ways to implement.

here is an example of driving 4 blendshapes (the blendshapes were created in Blender and exported to fbx so they could be imported into Unreal Engine):


here is a picture of the control rig graph for a simpler example driving 2 blendshapes.  similar logic can be used by copying and pasting these nodes and changing up the blendshape names and driving transformation channel to get it to work for 4 blendshapes.

here is a picture of the control rig graph that has the various nodes used to drive 2 blendshapes.


basically starting left to right:

on far left is the animator control that will be used to drive the blendshapes.

next it uses a 'Greater' node to decide if the controls transform z channel is positive or negative. here the z axis of the control will be used to drive 2 blendshapes.  if the z translate axis is positive it uses its value after clamping it to a number between 0 and 1 to drive the first blendshape which is set in the 'set curve value' node. if the control's z is negative it first multiplies its value by -1 to get a positive number then it also eventually clamps its value and sends it to the second blendshape 'set curve value' node. (some of the float multiplies was to tweak the sensitivity of how much the animator control moves is enough to turn on blendshapes).

Hope this is helpful.

Happy Sketching!


also i highly recommend checking out JC's YouTube channel for learning about control rig.

inspired by,

JC's '3D Education with JC' YouTube channel's 'Build a control rig from scratch' video

Saturday, August 30, 2025

Illustrator javascript doodle color shapes and export layers to png

this is a simple doodle in Illustrator using javascript that has an example of creating functions, coloring shapes with a fill color, and exporting layers to png.  i'm still new to javascript so this example doesn't have a ui yet.  there may be bugs or better ways to implement so please use modify at your own risk.



//a simple tool to color all shapes in illustrator document and then export each layer to png


//utility methods
function getLayersVisibilityDict(doc)
{
	/*
	Args:
		doc (document object)
	Returns:
		(dictionary): keys int indexes values bool for whether layer is visible
	*/
	var result = {};
	for(var i=0; i<doc.layers.length; i++)
	{
		result[i] = doc.layers[i].visible;
	}

	return result;
}

function setLayersVisibilityByDict(doc, data)
{
	/*
	Args:
		doc (document object)
		data (dictionary): keys are layer index value is visiblity to set to
	*/
	for(var i=0; i<doc.layers.length; i++)
	{
		doc.layers[i].visible = data[i];
	}
}
//end utility methods


//color all shapes
function colorAllShapes(r, g, b)
{
	/*
	Args:
		r (int) red rgb value
		g (int) green rgb value
		b (int) blue rgb value
	*/

	//requires setting all layers visible first.

	alert("coloring all shapes");

	//get reference to active document
	var doc = app.activeDocument;

	layerVisibilityDict = getLayersVisibilityDict(doc); //starting layer visiblity

	//make all layers visible
	for(var i=0; i<doc.layers.length; i++)
	{
		doc.layers[i].visible = true;
	}

	//loop all paths
	for(var i=0; i<doc.pathItems.length; i++)
	{
		shape = doc.pathItems[i];
		color = new RGBColor();
		color.red = r;
		color.green = g;
		color.blue = b;
		//fill shape with color
		shape.fillColor = color;
		//ensure shape has fill
		shape.filled = true;
	}

	//restore layer visibility
	setLayersVisibilityByDict(doc, layerVisibilityDict);
}



//function that acts as a constructor. for older illustrator that doesnt have class available
function LayerExporter(){
	/*responsible for exporting illustrator layers to individual png files
	*/
	
	this.doc = app.activeDocument;

	//export each layer to individual png files
	this.doIt = function()
	{
		var doc = this.doc;
		if(!doc)
		{	
			alert("no active document");
			return;
		}

		//todo get export folder from artist
		//var exportDir = "/Users/Nathaniel/Desktop/learning_/illustrator_dev_testing/coloringShapes";

		var exportFolder = Folder.selectDialog("Select a folder to export layers to");
		//new Folder(exportDir);

		if(!exportFolder)
		{
			alert("no export folder");
			return;
		}

		//export options
		var exportOptions = new ExportOptionsPNG24();
		exportOptions.antiAliasing = true; //enable anti aliasing
		exportOptions.transparency = true; //preserve transparency
		exportOptions.artBoardClipping = true; //if false it might export stuff outside art clip board
		exportOptions.horizontalScale = 100; //100 pct scale
		exportOptions.verticalScale = 100; //100 pct scale


		//store starting layer visibility
		const layersVisiblityDict = getLayersVisibilityDict(this.doc);

		//iterate through each layer
		for(var i=0; i<doc.layers.length; i++)
		{
			var currentLayer = doc.layers[i];
			//hide all other layers
			this.hideAllLayersExceptIndex(i);

			//construct file name
			var exportFileName = currentLayer.name+".png";
			var exportFileObj = new File(exportFolder.fsName + "/" + exportFileName);

			//export current visible layer
			doc.exportFile(exportFileObj, ExportType.PNG24, exportOptions);
		}
		
		//restore visibility of layers
		setLayersVisibilityByDict(this.doc, layersVisiblityDict);
	}


	this.hideAllLayersExceptIndex = function(indexShow)
	{
		/*
		Args:
			indexShow (int): the index to make visible
		*/
		var doc = this.doc;

		for(var i=0; i<doc.layers.length; i++)
		{
			doc.layers[i].visible = (i == indexShow); //google searched tip to shorten code. it gives false most of time except when counter is index
		}
	}

}

function doIt()
{
	//hard coded color
	colorAllShapes(r=0, g=0, b=255);


	//exporting layers
	var layerExporter = new LayerExporter();
	layerExporter.doIt();
}

doIt();

 

Thanks for looking