Showing posts with label doodles. Show all posts
Showing posts with label doodles. Show all posts

Thursday, June 4, 2026

Using big brush to help sketch poses

 


















Thanks for looking



Thursday, May 21, 2026

scripting a storyboard template tool in photoshop javascript

this tool allows placing an image in a grid layout in a new photoshop document.  it has some examples for creating a simple ui in photoshop using javascript. it also has some examples of writing methods/creating variables in photoshop javascript. it also is an example of storing a photoshop ui file in separate file than the core of the tool.  there may be bugs or better ways to implement so please modify/use at your own risk.

here is a video of the tool in action:



//ui.jsx
//a simple tool to create story panels from an input image. example from a blank panel
//tested in Photoshop 10.0.1
//please modify/use at your own risk

#include "core.jsx"

/*check if input path is a valid image path
Args:
    path (str): path to check ex: Users/Nathaniel/Desktop/photoshopTools/storyPanels/test_scene.jpg
Returns:
    (bool): true if path is a valid image.
*/
function isPathValidImage(path)
{
    var fileObj = new File(path);
    var validExtensions = ["jpg", "jpeg", "png", "tif"];

    if(!fileObj.exists)
    {
        return false;
    }

    var pathExt = path.split('.').pop().toLowerCase();
    //check if path's extension is in valid extensions
    for(var i=0; i<validExtensions.length; i++)
    {
        if(pathExt == validExtensions[i])
        {
            //path is valid extension. dont need to check any more extensions
            return true;
        }
    }

    return false;
}


/*
Returns:
    (bool): true if there is a document open false otherwise
*/
function isDocumentOpen()
{
    return app.documents.length > 0
}

function showUI(){

    if(!isDocumentOpen())
    {
        alert("requires a document open to use tool.")
        return
    }

    var dlg = new Window("dialog", "Panel Importer UI");
    dlg.orientation = "column";
    dlg.preferredSize = [600, 200];
    dlg.alignChildren = "left"; //align all widgets in window to the left

    //stores options for generating panels
    var parameterGroupLabel = dlg.add("statictext", undefined, "1. choose parameters for final panel layout")
    var parameterGroup = dlg.add("group");
    parameterGroup.orientation = "row";
    var numRowsLabel = parameterGroup.add("statictext", undefined, "rows:");
    var numRowsIntField = parameterGroup.add("edittext", undefined, "2");
    numRowsIntField.characters = 4; //set width

    var numColsLabel = parameterGroup.add("statictext", undefined, "cols:");
    var numColsIntField = parameterGroup.add("edittext", undefined, "2");
    numColsIntField.characters = 4; //set width

    var borderWidthPixelsLabel = parameterGroup.add("statictext", undefined, "border width (pixels):");
    var borderWidthPixelsIntField = parameterGroup.add("edittext", undefined, "20");
    borderWidthPixelsIntField.characters = 4; //set width    
    //

    //limit int fields to numbers only
    var intFields = [numRowsIntField, numColsIntField, borderWidthPixelsIntField];    
    //limit int fields to numbers
    var sharedOnChange = function()
    {
        //alert("field changed:" + this.text);
        //remove non numeric character
        this.text = this.text.replace(/[^\d]/g, '');
    }
    //assign shared onChange function
    for(var j=0; j<intFields.length; j++)
    {
        intFields[j].onChanging = sharedOnChange;
    }
    //


    //button
    var addPathBtn = dlg.add("button", undefined, "2. add image path to use for panel")
    //stores image path
    var imagePathText = dlg.add("statictext", undefined, "No Image Selected");
    imagePathText.preferredSize = [600, 50];

    var createPanelBtn = dlg.add("button", undefined, "3. create panels from image");
    var closeBtn = dlg.add("button", undefined, "close");

    //add function to button click
    addPathBtn.onClick = function()
    {
        var fileObj = File.openDialog("Select Image");
        if(fileObj)
        {
            selectedPath = fileObj.fsName;
            if(!isPathValidImage(selectedPath))
            {
                alert("selected path not a valid image. exiting");
                return;
            }
            imagePathText.text = selectedPath;
        }

    }

    createPanelBtn.onClick = function()
    {
        var imagePath = imagePathText.text;

        //get parameters from ui
        var numRows = parseInt(numRowsIntField.text);
        var numCols = parseInt(numColsIntField.text);
        var numPanels = numRows*numCols;
        var borderWidth = parseInt(borderWidthPixelsIntField.text);

        var panelImporter = new PanelImporter(imagePath=imagePath, 
                                            numPanels=numPanels, 
                                            numRows=numRows, 
                                            numCols=numCols, 
                                            borderWidth=borderWidth);
        panelImporter.doIt();
        alert("yaay creating storyboard panels done!");
        dlg.close(); //close ui when done so active document refreshed with story panels
    }

    closeBtn.onClick = function()
    {
        dlg.close();
    }

    dlg.show();

}


showUI();

//core.jsx
//old style class with ability to import an image multiple times to form a panel layout
/*
Args:
    imagePath(str): path to image to use for each panel
    numPanels(int): number of panels to create
    numRows(int): number of rows for storyboard
    numCols(int): number of cols for storyboard
    borderWidth(int): how many pixels to separate panels
*/
function PanelImporter(imagePath, numPanels, numRows, numCols, borderWidth)//(imagePath, numPanels, numRows, numCols, borderWidth)
{

    this.doc = app.activeDocument; //assumes in a created document. todo make a document if there is none
    this._imagePath = imagePath;
    this._numPanels = numPanels;
    this._numRows = numRows;
    this._numCols = numCols;
    this._borderWidth = borderWidth;

    /*do any additional initializations for this class
    apparently cant run a method of this class in its init. when using old style function for class
    */
    this._init = function()
    {
        this._images = this._getImages(this._imagePath, this._numPanels);//will hold image paths to use for each panel
    }
    
    /*
    Args:
        imagePath(str): path to image to use for each panel ex: /Users/Nathaniel/Desktop/photoshopTools/storyPanels/test_scene.jpg
    */
    this.doIt = function()
    {
        this._init();
        this._createPanels();   
    }

    /*
    Args:
        imagePath(str): path to image to use for each panel ex: /Users/Nathaniel/Desktop/photoshopTools/storyPanels/test_scene.jpg
        numPanels(int): number of times to repeat imagePath
    Returns:
        list(str): the imagePath repeated numPanels many times
    */
    this._getImages = function(imagePath, numPanels)
    {
        var images = [];
        //var imagePath = "/Users/Nathaniel/Desktop/photoshopTools/storyPanels/test_scene.jpg";
        for(var i=0; i<numPanels; i++)
        {
            images.push(imagePath);
        }

        return images;
    }

    /*creates storyboard panels using a single image for each panel
    */
    this._createPanels = function()
    {
        //var doc = app.activeDocument;
        var targetDoc = this.doc;//app.activeDocument;
        var images = this._images;
        var numRows = this._numRows;
        var numCols = this._numCols;
        var borderWidth = this._borderWidth;


        var imageIndex = 0; //will be used to query images
        var numImages = images.length;
        if(numImages > numRows*numCols)
        {
            alert("too many images. make sure number of images is smaller than number of rows multiplied by number of columns");
            return;
        }


        for(var i=0; i<numRows; i++)
        {
            for(var j=0; j<numCols; j++)
            {
                if(imageIndex >= numImages)
                {
                    break;
                }

                var pathToImage = images[imageIndex];
                var fileObj = new File(pathToImage);


                if(!fileObj.exists){
                    alert("image file doesnt exist");
                    return;
                }

                //open image file
                var imgDoc = app.open(fileObj);

                //select all and copy
                imgDoc.selection.selectAll();
                imgDoc.selection.copy();

                

                //close the opened image file
                imgDoc.close(SaveOptions.DONOTSAVECHANGES);

                //paste image into active document
                var newLayer = targetDoc.paste();

                var originalRulerUnits = app.preferences.rulerUnits;
                app.preferences.rulerUnits = Units.PIXELS;

                //set goal x y pixels
                //width = right - left
                //bounds [left, top, right, bottom]
                var widthImage = newLayer.bounds[2].value - newLayer.bounds[0].value;
                var heightImage = newLayer.bounds[3].value - newLayer.bounds[1].value;

                var goalX = j*widthImage + (j+1)*borderWidth; //the columns are used for x coordinate
                var goalY = i*heightImage + (i+1)*borderWidth;//the rows are used for the y coordinate

                var curX = newLayer.bounds[0].value;
                var curY = newLayer.bounds[1].value;

                var dx = goalX - curX;
                var dy = goalY - curY;

                //move imported image to goal x, y pixels
                newLayer.translate(dx, dy);

                //increment image index
                imageIndex += 1;
            }

        }
        //restore original untis
        app.preferences.rulerUnits = originalRulerUnits;
    }


}

Thanks for looking

Tuesday, April 7, 2026

Sketches













Thanks for looking

Tuesday, February 17, 2026

some python doodles to help with rigging in Blender 2.79

these are some miscellaneous doodles and snippets i used when building the facial rigs in the rigging page of this blog.  They would need lots of editing to be usable in different situations and will need tweaking to get them working with higher blender versions.  They have some examples with looping through armature bones, editing bendy bone attributes, putting bones in layers etc.   please modify/use at your own risk.

 

#changing all bones rotation mode to euler xyz (assumes in pose mode)
####
import bpy
#set deform off on selected edit bones
armatureName = bpy.context.object.name #"Armature"
for pb in bpy.data.objects[armatureName].pose.bones:
    pb.rotation_mode = 'XYZ' #bpy.context.object.pose.bones["ANIM_spine_end.C"].rotation_mode = 'XYZ'
####



#putting some bones in last layer to hide for animation
######
#ex put all non anim bones in last layer
import bpy
armatureName = bpy.context.object.name
bonesToHideNames = [b.name for b in bpy.data.objects[armatureName].data.bones if not b.name.startswith('ANIM_')]

#hide backend bones
def UT_PutInLayer(items=[], layer=32, armatureName=None ):
    """put given bones into specified int layer. i think max is 32
    """ 
    layerValues = [False]*32
    layerValues[layer-1] = True

    armObj = bpy.data.objects[armatureName]
    bpy.context.scene.objects.active = armObj    
    bpy.ops.object.mode_set(mode='EDIT', toggle=False)
    
    for it in items:
        itObj = armObj.data.edit_bones[it] #assumes it exists
        itObj.layers = layerValues
        #put widgets in last layer so they are not seen
        #rootWidgetShape.layers = [False]*19+[True]

UT_PutInLayer(items=bonesToHideNames, layer=32, armatureName=armatureName)
#########





#lock rotation on selected bones (assumes in pose mode)
####
import bpy
lockRotation = True
lockScale = True

#set deform off on selected edit bones
armatureName = bpy.context.object.name #"Armature"
for bone in bpy.data.objects[armatureName].data.bones:
    #skip bones not selected
    if not bone.select:
        continue
    pb = bpy.data.objects[armatureName].pose.bones[bone.name]
    if lockRotation:
        pb.lock_rotation = [True, True, True]
    if lockScale:
        pb.lock_scale = [True, True, True]

####




#parenting some geo to some bones. assumes armature selected in object mode
#change this to names that exist. first is deformation bone, second is list of geo names to parent to bone
import bpy
boneToGeoDict = {
    "ANIM_base.C":["base_geo.C","tail_sphere_a_geo.C"],
    "ANIM_tail_a.C":["tail_a_geo.C","tail_sphere_b_geo.C"],
    "ANIM_tail_b.C":["tail_b_geo.C","tail_sphere_c_geo.C"],
    "ANIM_tail_c.C":["tail_c_geo.C"]
}
armatureName = bpy.context.object.name #"Armature"

def parentGeoToBone(amt = None, geo=None, bone=None):
    """#3. parent cube geo to a single bone, cube and bone exist
    inputs object, object, str
    lots of mode switching cause i think doing without ops will be longer
    """
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')
    amt.select = True
    bpy.context.scene.objects.active = amt
    bpy.ops.object.mode_set(mode='EDIT')
    amt.data.edit_bones.active = amt.data.edit_bones[bone]
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')
    
    geo.select = True
    amt.select = True
    bpy.context.scene.objects.active = amt
    bpy.ops.object.parent_set(type='BONE',keep_transform=True)

for bone, geos in boneToGeoDict.items():
    for geo in geos:
        parentGeoToBone(amt=bpy.data.objects[armatureName], geo=bpy.data.objects[geo], bone=bone)

####



###set all mesh objects selectability. helpful so animator not accidentally selecting geo
import bpy
isSelectable = False #change to True for setting all meshes to selectable
meshNames = [obj.name for obj in bpy.data.objects if obj.type=="MESH"]
#print(meshNames)
for mesh in meshNames:
    meshObj = bpy.data.objects[mesh]
    meshObj.hide_select = not isSelectable
####




import bpy
#set deform off on selected edit bones
armatureName = "Armature"
for eb in bpy.data.armatures[armatureName].edit_bones:
    if not eb.select:
        continue
    eb.use_deform = False




#add .L suffix to selected bones
import bpy
#set deform off on selected edit bones
armatureName = bpy.context.object.name
for eb in bpy.data.armatures[armatureName].edit_bones:
    if not eb.select:
        continue
    curName = eb.name
    newName = "{0}.L".format(curName) #add left suffix
    eb.name = newName


#snippet to add one armature to different scene:
#(use ui to append armature data)
import bpy
bpy.data.objects.new("Armature", bpy.data.armatures['Armature'])
bpy.context.scene.objects.link(bpy.data.objects['Armature']) #will show bones in viewport

import bpy
#remove right side of armature
#select right side of armature in edit mode
armatureName = "Armature"
for eb in bpy.data.armatures[armatureName].edit_bones:
    if eb.matrix.translation.x >= 0:
        continue
    eb.select = True
    
    
import bpy
#select bendy bone ends to remove
armatureName = "Armature"
for eb in bpy.data.armatures[armatureName].edit_bones:
    if ("_head" in eb.name) or ("_tail" in eb.name):
        eb.select = True



import bpy
#select lid span bones to remove
armatureName = "Armature"
for eb in bpy.data.armatures[armatureName].edit_bones:
    if ("eyeLid_" in eb.name):
        eb.select = True

##for fitting helper (need to do fitting in edit mode so pose bone gets updated automatically)
import bpy
#reset curve offsets on selected bone in edit mode
armatureName = bpy.context.object.name
ebones = [eb for eb in bpy.data.armatures[armatureName].edit_bones if eb.select]
for eb in ebones:
    for attr in ["bbone_curveinx", "bbone_curveoutx","bbone_curveiny", "bbone_curveouty"]:
        setattr(eb,attr,0)


#ctrl + alt + s > mouse move #to scale bendy bone in edit mode

#testing stuff with actions
#removing all actions in scene
>>> for action in bpy.data.actions:
...     bpy.data.actions.remove(action)



#loosing actions set on constraints. so after appending action also want to set action names in constraints.
import bpy

#create a dictionary.  boneName:(constraintName, actionName)
armatureName = "Armature"
result = dict()

for pb in bpy.data.objects[armatureName].pose.bones:
    #action constraints 
    actionConstraints = [cnt for cnt in pb.constraints if cnt.type == "ACTION"]
    if not actionConstraints:
        continue
    #loop constraints getting action name
    for cnt in actionConstraints:
        #there can be more than one constraint for a single bone so need to store a list
        result.setdefault(pb.name, []).append((cnt.name, cnt.action.name))
print(result)
        

#setting the constraints in scene to use missing actions. assumes have appended actions already
import bpy
result = {} #set this dictionary
armatureName = "Armature"

for k,val in result.items():
    for v in val:
    	cnt, actionName = v
    	pb = bpy.data.objects[armatureName].pose.bones[k]
    	pb.constraints[cnt].action = bpy.data.actions[actionName]


##appending an armature object brings in constraints and actions
#v1 (skips bone if cannot find bone in destination armature)
import bpy
#copy constraint from source armature to destination

sourceArmature = "Armature.001"
destinationArmature = "Armature"

for pb in bpy.data.objects[destinationArmature].pose.bones:
    if pb.name not in bpy.data.objects[sourceArmature].pose.bones:
        print("could not find {} skipping".format(pb.name))
        continue
    src_pb = bpy.data.objects[sourceArmature].pose.bones[pb.name]
    srcActionConstraints = [cnt for cnt in src_pb.constraints if cnt.type == "ACTION"]
    if not srcActionConstraints:
        continue
    for cnt in srcActionConstraints:
        new_cnt = pb.constraints.new(cnt.type)
        #attributes of constraint
        for prop in dir(cnt):
            #dont copy target
            if prop == "target":
                setattr(new_cnt, prop, bpy.data.objects[destinationArmature])
                continue
            try:
                setattr(new_cnt, prop, getattr(cnt, prop))
            except:
                pass




#v0
##copying constraints/and/Actions from one armature to a different armature with identical bone names
import bpy
#copy constraint from source armature to destination

sourceArmature = "Armature"
destinationArmature = "Armature.001"

for pb in bpy.data.objects[destinationArmature].pose.bones:
    src_pb = bpy.data.objects[sourceArmature].pose.bones[pb.name]
    srcActionConstraints = [cnt for cnt in src_pb.constraints if cnt.type == "ACTION"]
    if not srcActionConstraints:
        continue
    for cnt in srcActionConstraints:
        new_cnt = pb.constraints.new(cnt.type)
        #attributes of constraint
        for prop in dir(cnt):
            #dont copy target
            if prop == "target":
                setattr(new_cnt, prop, bpy.data.objects[destinationArmature])
                continue
            try:
                setattr(new_cnt, prop, getattr(cnt, prop))
            except:
                pass

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


#########
#ungroup all channels for selected action
import bpy
obj = bpy.context.active_object
action = obj.animation_data.action

#get fcurve objects to ungroup
fcurvesGroupObjsToUngroup = []
for fcurve in action.fcurves:
    if not fcurve.group:
        continue
    fcurvesGroupObjsToUngroup.append(fcurve.group)

#ungroup the given fcurves
for fcurveGroupObj in fcurvesGroupObjsToUngroup:
    if not fcurveGroupObj.name in action.groups.keys():
        continue
    action.groups.remove(fcurveGroupObj)
#to refresh view
curFrame = bpy.context.scene.frame_current
bpy.context.scene.frame_set(curFrame)
#####


#mirror pose of selected bones. (example for jaw action. first copy pose, then paste pose flipped)
import bpy
bpy.ops.pose.copy()
bpy.ops.pose.paste(flipped=True)


#select all right side bones of current action
import re
import bpy
obj = bpy.context.active_object
action = obj.animation_data.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')]
#print("bonesInAction", bonesInAction)
bonesRightSide = [bone.replace('.L', '.R') for bone in bonesInActionLeftSide]
print('right side', bonesRightSide)
#deselect all pose bones
bpy.ops.pose.select_all(action='DESELECT')
for b in bonesRightSide:
    obj.pose.bones[b].bone.select = True
########


#go to frame 0 of current action
bpy.context.scene.frame_set(0)

#go to default ArmatureAction



######
#save out vertex selection
import bpy
verticesSelected = []
meshName = 'gorilla'
verticesSelected = [v.index for v in bpy.data.objects[meshName].data.vertices if v.select]

#reselect vertex selection (need to be in object mode)
#verticesSelected = [] #paste in vertex ids from above step
verticesSelected = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 578, 579, 581, 616, 660, 661, 703, 704, 705, 773, 774, 775, 776, 781, 782, 828, 830, 937, 1279, 1280, 1281, 1282, 1283, 1305, 1306, 1307, 1308, 1309, 1323, 1324, 1325, 1326, 1327, 1342, 1344, 1347, 1348, 1349, 1350, 1351, 1352, 1353, 1354, 1365, 1366, 1367, 1368, 1369, 1426, 1437, 1554, 1555, 1556, 1557, 1558, 1559, 1560, 1561, 1562, 1563, 1564, 1565, 1566, 1567, 1568, 1569, 1570, 1571, 1572, 1573, 1574, 1575, 1576, 1577, 1578, 1579, 1580, 1581, 1582, 1583, 1584, 1585, 1586, 1587, 1588, 1589, 1590, 1591, 1592, 1593, 1594, 1595, 1596, 1597, 1598, 1599, 1600, 1601, 1602, 1603, 1604, 1605, 1606, 1607, 1608, 1609, 1610, 1611, 1612, 1613, 1614, 1615, 1616, 1617, 1618, 1619, 1620, 1621, 1622, 1623, 1624, 1625, 1626, 1627, 1628, 1629, 1630, 1631, 1632, 1633, 1634, 1635, 1636, 1637, 1638, 1639, 1640, 1641]
meshName = 'gorilla'
for v in bpy.data.objects[meshName].data.vertices:
    if v.index in verticesSelected:
        v.select = True
    else:
        v.select = False
#######




### select all tail and head of given bones
import bpy
mainBones = ["eyeLid_up", "eyeLid_dn"] #put bones here
#eyeLid_up_a.L eyeLid_up_b.L

armatureName = "Armature"
#start by deselecting all bones
for eb in bpy.data.armatures[armatureName].edit_bones:
    eb.select = False
for eb in bpy.data.armatures[armatureName].edit_bones:
    for bone in mainBones:
        #should include tail
        if eb.name.startswith(bone):
            eb.select = True
            break
###




###copy edit bone positions from source armature to destination armature
#copy:
#head
#tail
#roll
#curve xy offsets
###

import bpy

sourceArmature = "Armature_b"
destinationArmature = "Armature"

def _enterEdit(armatureName):
    bpy.data.objects[armatureName].hide = False 
    bpy.ops.object.mode_set(mode='OBJECT')
    #enter edit mode of given armature
    bpy.ops.object.select_all(action='DESELECT')
    bpy.data.objects[armatureName].select = True
    bpy.context.scene.objects.active = bpy.data.objects[armatureName]
    bpy.ops.object.mode_set(mode='EDIT')


destinationBoneNames = [b.name for b in bpy.data.armatures[destinationArmature].bones]
for b in destinationBoneNames:
    
    if b not in bpy.data.armatures[sourceArmature].bones:
        print("could not find {} skipping".format(b))
        continue
    
    #enter edit mode source to get position
    _enterEdit(sourceArmature)

    src_eb = bpy.data.armatures[sourceArmature].edit_bones[b]
    src_head = src_eb.head.copy()
    src_tail = src_eb.tail.copy()
    src_roll = src_eb.roll
    src_bboneAttrsDict = dict()

    bboneAttrs = ['bbone_curveinx', 
                'bbone_curveoutx',
                'bbone_curveiny',
                'bbone_curveouty']
    for attr in bboneAttrs:
        src_bboneAttrsDict[attr] = eval("src_eb.{0}".format(attr))

    #enter edit mode destination to apply
    _enterEdit(destinationArmature)
    eb = bpy.data.armatures[destinationArmature].edit_bones[b]
    #head
    eb.head[0] = src_head[0]
    eb.head[1] = src_head[1]
    eb.head[2] = src_head[2]
    #tail
    eb.tail[0] = src_tail[0]
    eb.tail[1] = src_tail[1]
    eb.tail[2] = src_tail[2]
    #roll
    eb.roll = src_roll
    #bbone attrs
    for at, val in src_bboneAttrsDict.items():
        setattr(eb, at, val)

#end copy edit bone positions from one armature to another



#remove all fcurves of bones that dont exist anymore. this could happen if addded/removed bones in the middle of action creation
import bpy
import re
armatureName = 'lipArmature'
action = bpy.data.objects[armatureName].animation_data.action #use selected action
fcurvesToRemove = []
for fcurve in action.fcurves:
    #print(fcurve.data_path)
    dataPath = fcurve.data_path 
    boneName = re.match('.*\["(.*)"\].*', dataPath).groups()[0]
    if boneName not in bpy.data.objects[armatureName].data.bones:
        fcurvesToRemove.append(fcurve)
fcurvesToRemove = list(set(fcurvesToRemove)) #remove duplicates
print('removing fcurves:')
print(fcurvesToRemove)
for fcrv in fcurvesToRemove:
    action.fcurves.remove(fcrv)
#### 
 
Thanks for looking
 

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