Tuesday, March 3, 2026

shapekey blendshape addon for Blender 2.79

this is a personal work blendshape addon which has some snippets on mirroring shape keys. it may need to be tweaked to work in higher Blender versions.  also there may be bugs so please modify/use at your own risk

#naShapekeyUtilAddOn.py
#modify use at your own risk



#last modified
#112321 -- added mirror and copy sculpt to blendshape buttons
#111721 -- added create corrective button. working on create clean mesh button
#070321 -- found bug in mirror topology tool. bug was because mesh had no center line. 
#       -- the mirror topo seems to make all shapekeys in list symmetric. may want to give it a list of shapekeys to mirror. not sure the deleting half of mesh will mess up vertex order
#       -- not really sure the practical use of mirror topo
#050121 -- worked on initial release



import bpy
import os
import math
import bmesh
import logging
import json
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG) #without this info logs wouldnt show in console


####add on portion
bl_info = {
    "name":"shapekey blendshape editing tools",
    "description":"some tools to edit shapekeys",
    "category": "Object",
    "author":"Nathaniel Anozie",
    "blender":(2,79,0)
}

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

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


class makeCorrectiveShapeKeyOperator(Operator):
    """create a corrective sculpt which can be added on
    """
    bl_idname = "obj.makecorrectiveshapekey" #needs to be all lowercase
    bl_label = "makeCorrectiveShapeKey"
    bl_options = {"REGISTER"}

    def execute(self, context):
        createCorrectiveSculpt( pose = context.scene.shapekeyutils_prop.poseMesh, sculpt = context.scene.shapekeyutils_prop.sculptMesh, shape_key_mesh = context.scene.shapekeyutils_prop.shapekeyMesh )
        return {'FINISHED'}


class mirrorShapeKeyOperator(Operator):
    """mirror shapekeys. it assumes blendshape mesh is selected. if no shapekeys provided it tries to mirror all shapes ending with .L
    """
    bl_idname = "obj.mirrorshapekey"
    bl_label = "mirrorShapeKey"
    bl_options = {"REGISTER"}

    def execute(self, context):
        mesh = context.selected_objects[0].name
        blendshapes = []
        blendshapes_arg = context.scene.shapekeyutils_prop.mirrorShapekey
        if blendshapes_arg:
            blendshapes = blendshapes_arg.split(' ')
        else:
            #use all left side blendshapes
            blendshapes = getAllLeftSideBlendshapes(mesh)
        print("blendshapes>>",blendshapes)
        use_topology = True if context.scene.shapekeyutils_prop.topoEnum == 'True' else False 
        mirrorBlendshape( mesh = mesh, blendshapes = blendshapes, use_topology = use_topology )
        return {'FINISHED'}
        

class makeCleanPoseOperator(Operator):
    """create a clean mesh (no shapekeys) at the given pose. requires selection of mesh object with shapekeys
    """
    bl_idname = "obj.makecleanpose" #needs to be all lowercase
    bl_label = "makeCleanPose"
    bl_options = {"REGISTER"}

    def execute(self, context):
        apply_types = ["ARMATURE"] if context.scene.shapekeyutils_prop.applyArmatureEnum == 'True' else [] 
        
        createCleanMesh(keepPose=True, apply_types = apply_types)
        return {'FINISHED'}

class copySculptToBlendshapeOperator(Operator):
    """copies a separate sculpt mesh to an existing blendshape. first select sculpt mesh, last mesh with blendshapes with a blendshape highlighted in dropdown
    """
    bl_idname = "obj.copysculpttoblendshape" #needs to be all lowercase
    bl_label = "copySculptToBlendshape"
    bl_options = {"REGISTER"}

    def execute(self, context):
        copySculptDict = context.scene.shapekeyutils_prop.copySculptDict
        #bit to read sculpt to shapekey dicitionary from a json file
        copySculptPath = context.scene.shapekeyutils_prop.copySculptToShapeKeyPath
        copySculptFileName = bpy.path.abspath(copySculptPath) if copySculptPath else ''
        
        ##
        copySculptJsonDict = {}
        if os.path.isfile(copySculptFileName):
            with open(copySculptFileName, 'r') as fileName:
                copySculptJsonDict = json.load(fileName)
        if copySculptJsonDict:
            copySculptToBlendshapeByDict(copySculptJsonDict)
            return {'FINISHED'}
        ##
        
        if copySculptDict:
            #use dictionary to copy sculpt
            copySculptDict = eval(copySculptDict)
            copySculptToBlendshapeByDict(copySculptDict)
            return {'FINISHED'}
        
        copySculptToBlendshape()
        return {'FINISHED'}


class OBJECT_OT_copySculptToBlendshapeByWorldPosOperator(Operator):
    """copies a separate sculpt mesh to an existing mesh. first select sculpt mesh, last mesh we want to edit.
    it uses world positions to detect match. put a vertex group for vertices of interest on destination if want to speed things up.
    if no vertex group provided it looks at all vertices.
    """
    bl_idname = "obj.copysculpttoblendshapebyworld" #needs to be all lowercase
    bl_label = "copySculptToBlendshapeByWorld"
    bl_options = {"REGISTER"}

    def execute(self, context):
        defaultMesh = context.scene.shapekeyutils_prop.defaultMeshForCopyByWorld
        bsRecover = BlendshapeRecover()
        bsRecover.copySculptBySelection(context, defaultMesh)
        return {'FINISHED'}
        
class splitSymmetricShapeKeyOperator(Operator):
    """split selected shapekey.  first select single mesh with shapekey in object mode and highlight shapekey wish to split.
    """
    bl_idname = "obj.splitsymmetricshapekey" #needs to be all lowercase
    bl_label = "splitSymmetricShapeKey"
    bl_options = {"REGISTER"}

    def execute(self, context):
        splitSymmetricShapeKey(obj=context.selected_objects[0],removeSourceShapeKey = False, context = context)
        return {'FINISHED'}


class mirrorShapekeyTopologyOperator(Operator):
    """first select single mesh with shapekey in object mode. requires mesh to have a center line, required for mirroring.
    """
    bl_idname = "obj.mirrorshapekeytopology"
    bl_label = "mirrorShapekeyTopology"
    bl_options = {"REGISTER"}
    
    def execute(self, context):
        mirrorShapekeyTopology(obj=context.selected_objects[0],context=context)
        return {'FINISHED'}

class mergeShapekeysOperator(Operator):
    """merge selected meshes shapekeys into one mesh. places all shapekeys on last selected mesh
    """
    bl_idname = "obj.mergeshapekeys"
    bl_label = "mergeShapekeys"
    bl_options = {"REGISTER"}
    
    def execute(self, context):
        mergeShapekeys(context=context)
        return {'FINISHED'}

class zeroKeyOnSelectedVerticesOperator(Operator):
    """set selected vertices to basis position
    """
    bl_idname = "obj.zerokeyonselectedvertices"
    bl_label = "zeroKeyOnSelectedVertices"
    bl_options = {"REGISTER"}
    
    def execute(self, context):
        zeroKeyOnSelectedVertices(context=context)
        return {'FINISHED'}

class putMeshesInRowOperator(Operator):
    """arrange selected meshes in row with a little gap
    """
    bl_idname = "obj.putmeshesinrow"
    bl_label = "putMeshesInRow"
    bl_options = {"REGISTER"}
    
    def execute(self, context):
        putMeshesInRow(context.selected_objects,gridWidth=1)
        return {'FINISHED'}

class importObjOperator(Operator):
    """import objs in a directory
    """
    bl_idname = "obj.importobj"
    bl_label = "importObj"
    bl_options = {"REGISTER"}
    
    def execute(self, context):
        path = context.scene.shapekeyutils_prop.importObjPath
        dirpath = bpy.path.abspath(path)
        print("importObjOperator dir path: %s" %(dirpath))
        importObj(dirpath)
        return {'FINISHED'}

class OT_setToDefault(Operator):
    """if want all verts on right side default select mesh with shapekeys.
    if want to use vertex group (all verts in vertex group get default) - first select mesh with vertex group then select mesh with shapekeys.
    """
    bl_idname = "obj.settodefault"
    bl_label = "set to default blendshapes"
    bl_options = {"REGISTER"}
    
    def execute(self, context):
        selection = [obj.name for obj in bpy.context.selected_objects]
        if len(selection) == 2:
            setToDefaultByVertexGroupCmd(context)
        else:
            setToDefaultBySideCmd(context)
        
        return {'FINISHED'}
        
class naShapekeyUtilPanel(Panel):
    bl_label = "naShapekeyUtil Panel"
    bl_space_type = "VIEW_3D" #needed for ops working properly
    bl_region_type = "UI"
    
    def draw(self, context):
        layout = self.layout
        layout.label(text = "for corrective making")
        layout.prop( context.scene.shapekeyutils_prop, "poseMesh", text = "poseMesh" )
        layout.prop( context.scene.shapekeyutils_prop, "sculptMesh", text = "sculptMesh" )
        layout.prop( context.scene.shapekeyutils_prop, "shapekeyMesh", text = "shapekeyMesh" )
        layout.operator( "obj.makecorrectiveshapekey")
        layout.label(text = "-"*50)
        ##
        layout.label(text = "for mirroring")
        layout.prop( context.scene.shapekeyutils_prop, "mirrorShapekey", text = "mirrorShapekey" )
        layout.prop( context.scene.shapekeyutils_prop, "topoEnum", text = "useTopology" )
        layout.operator( "obj.mirrorshapekey")
        layout.label(text = "-"*50)
        ##
        layout.label(text = "for clean pose"+"-"*50)
        layout.operator( "obj.makecleanpose" )
        layout.prop( context.scene.shapekeyutils_prop, "applyArmatureEnum", text = "applyArmature" )
        layout.label(text = "-"*50)
        layout.label(text = "-"*10)
        layout.operator( "obj.copysculpttoblendshape" )
        layout.label(text = "import json sculpt to shapekey mapping")
        layout.prop(context.scene.shapekeyutils_prop, "copySculptToShapeKeyPath")
        layout.prop( context.scene.shapekeyutils_prop, "copySculptDict", text = "(optional) copy sculpt python dictionary")
        layout.label(text = "-"*10)
        layout.label(text = "-"*50)
        layout.operator( "obj.copysculpttoblendshapebyworld" )
        layout.prop(context.scene.shapekeyutils_prop, "defaultMeshForCopyByWorld")
        layout.operator( "obj.settodefault")
        layout.operator( "obj.splitsymmetricshapekey")
        #layout.operator( "obj.mirrorshapekeytopology")
        layout.operator( "obj.mergeshapekeys")
        layout.operator( "obj.zerokeyonselectedvertices")
        layout.operator( "obj.putmeshesinrow")
        #for import obj
        layout.label(text = "import all obj in directory")
        layout.prop(context.scene.shapekeyutils_prop, "importObjPath")
        layout.operator( "obj.importobj")
        ##


class shapekeyUtilsProperties(PropertyGroup):
    importObjPath = StringProperty(
        name = "Browse Directory",
        description = "Pick directory with .obj files",
        maxlen = 200,
        subtype = 'FILE_PATH'
    )
    
    ##for corrective
    poseMesh = StringProperty(
        name = "poseMesh",
        description = "pose mesh. its the data object name for mesh that differs from default but is before sculpting"
        )    
    
    sculptMesh = StringProperty(
        name = "sculptMesh",
        description = "sculpt mesh. its the data object name for mesh that is a sculpt ontop of pose. it is how we want face to eventually look"
        )   
    
    shapekeyMesh = StringProperty(
        name = "shapekeyMesh",
        description = "shapekey mesh. its the data object name for mesh has all shapekeys. it will be used to get the default face"
        )
    ###
    
    ##for mirror
    mirrorShapekey = StringProperty(
        name = "mirrorShapekey",
        description = "shapekey names to mirror space separated. if non provided it tries to mirror all blendshapes ending with .L"
        )   
    topoEnum = EnumProperty(
        name = "useTopology",
        description = "whether or not to use Blenders use_topology",
        items = [   ('True',"True",""),
                    ('False',"False","")
                ],
        default = 'False'
        )
    ##
    
    #for clean pose
    applyArmatureEnum = EnumProperty(
        name = "applyArmature",
        description = "whether or not to apply armature to clean pose",
        items = [   ('True',"True",""),
                    ('False',"False","")
                ],
        default = 'False'
        )    

    #for copy sculpt
    copySculptDict = StringProperty(
    name = "copySculptDict",
    description = "(optional) dictionary mapping sculpt:mesh blendshape name ex: {'sculptBrowUp':'browAllUp.L', 'sculptBrowDn':'browAllDn.L'}"
    )
    
    copySculptToShapeKeyPath = StringProperty(
        name = "Json File Name",
        description = "Pick json file path with dictionary mapping sculpt to blender shapkey name",
        maxlen = 200,
        subtype = 'FILE_PATH'
    )
    
    #for copy sculpt by world position
    defaultMeshForCopyByWorld = StringProperty(
        name = "copySculptToMeshDefault",
        description = "default mesh its the data object name for mesh that has some matching vertices by world position we wish to use"
        )    
    
def register():
    bpy.utils.register_class(makeCorrectiveShapeKeyOperator)
    bpy.utils.register_class(mirrorShapeKeyOperator)
    bpy.utils.register_class(makeCleanPoseOperator)
    bpy.utils.register_class(copySculptToBlendshapeOperator)
    bpy.utils.register_class(OBJECT_OT_copySculptToBlendshapeByWorldPosOperator)
    bpy.utils.register_class(splitSymmetricShapeKeyOperator)
    bpy.utils.register_class(mirrorShapekeyTopologyOperator)
    bpy.utils.register_class(mergeShapekeysOperator)
    bpy.utils.register_class(zeroKeyOnSelectedVerticesOperator)
    bpy.utils.register_class(putMeshesInRowOperator)
    bpy.utils.register_class(importObjOperator)
    bpy.utils.register_class(OT_setToDefault)
    bpy.utils.register_class(naShapekeyUtilPanel)
    
    bpy.utils.register_class(shapekeyUtilsProperties)
    bpy.types.Scene.shapekeyutils_prop = PointerProperty( type = shapekeyUtilsProperties )
    
def unregister():
    bpy.utils.unregister_class(makeCorrectiveShapeKeyOperator)
    bpy.utils.unregister_class(mirrorShapeKeyOperator)
    bpy.utils.unregister_class(makeCleanPoseOperator)
    bpy.utils.unregister_class(copySculptToBlendshapeOperator)
    bpy.utils.unregister_class(OBJECT_OT_copySculptToBlendshapeByWorldPosOperator)
    bpy.utils.unregister_class(splitSymmetricShapeKeyOperator)
    bpy.utils.unregister_class(mirrorShapekeyTopologyOperator)
    bpy.utils.unregister_class(mergeShapekeysOperator)
    bpy.utils.unregister_class(zeroKeyOnSelectedVerticesOperator)
    bpy.utils.unregister_class(putMeshesInRowOperator)
    bpy.utils.unregister_class(importObjOperator)
    bpy.utils.unregister_class(OT_setToDefault)
    bpy.utils.unregister_class(naShapekeyUtilPanel)
    
    bpy.utils.unregister_class(shapekeyUtilsProperties)
    del bpy.types.Scene.shapekeyutils_prop
    
if __name__ == "__main__":
    register()
####



def splitSymmetricShapeKey(obj = None, removeSourceShapeKey = False, context = None):
    """going from symmetric shape key to a .L and .R shapekey
    works in xz plane only.
    duplicate shapekey twice > on one shape key set -x side to basis vert position, on other set +x side to basis vert position
    option to remove source shapekey after split
    """
    
    def duplicateShapeKeyAtIndex(obj = None, sourceIndex = None, context = None):
        #returns index of created shapekey
        result = None
        obj.active_shape_key_index = sourceIndex
        obj.show_only_shape_key = True
        bpy.ops.object.shape_key_add(from_mix=True)
        result = context.object.active_shape_key_index
        return result


    #duplicate shape key twice
    #then zero out appropriate side of meshes
    sourceIndex = obj.active_shape_key_index
    sourceName = obj.data.shape_keys.key_blocks[sourceIndex].name
    leftIndex = duplicateShapeKeyAtIndex(obj,sourceIndex,context)
    rightIndex = duplicateShapeKeyAtIndex(obj,sourceIndex,context)
    
    obj.data.shape_keys.key_blocks[leftIndex].name = sourceName+'.L'
    obj.data.shape_keys.key_blocks[rightIndex].name = sourceName+'.R'
    
    context.object.active_shape_key_index = leftIndex
    zeroSelectedKeyInX(sign="-",includeCenter = False, context=context) #to avoid double transformation of center vertices

    bpy.context.object.active_shape_key_index = rightIndex
    zeroSelectedKeyInX(sign="+",includeCenter = True, context=context)
    
    #optionally delete source shape key
    if removeSourceShapeKey:
        obj.active_shape_key_index = sourceIndex
        bpy.ops.object.shape_key_remove(all=False)
        
        

    
    
def mirrorShapekeyTopology(obj=None, context = None):
    """for getting all shapekeys have mirrored topology. requires basis default mesh to have a center line
    
    first copy out all shapekeys to a new mesh > each mesh no shapekeys > each mesh symmetric topology
    second, remove all shapekeys on mesh we wish to mirror > make its topology mirrored
    third, apply all created meshes as shapekeys on mesh
    
    note doesnt preserve drivers on shapkeys.
    """
    dupObjs = [] #list of tuples obj, shapekey name


    def getShapekeyName( geoName = None, dupObjsL = [] ):
        #dependent on data format tuples 
        for arg in dupObjsL:
            if arg[0].name == geoName:
                return arg[1]
        
    if not obj.data.shape_keys:
        return
           
    countShapeKeys = len(obj.data.shape_keys.key_blocks)
    for i in range(1,countShapeKeys):  
        #print('i:%s' %i)
        #print('obj name: %s' %(obj.name))
        dupObj = makeMeshUsingShapekeyIndex(obj,i,context)
        #make dupped object symmetric
        deleteHalfMesh(dupObj)
        makeMeshWhole(dupObj)
        dupObjs.append( (dupObj, obj.data.shape_keys.key_blocks[i].name) )
    
    #print('dupObjs')
    #print(dupObjs)
    
    #done duping all objects
    #remove all shapekeys from source object
    #make it mirrored
    #note not preserving drivers on shapekeys
    bpy.ops.object.select_all(action='DESELECT')
    obj.select = True
    context.scene.objects.active = obj
    bpy.ops.object.shape_key_remove(all=True)
    deleteHalfMesh(obj)
    makeMeshWhole(obj)    
    #
    #apply all of the duped objects as shapekeys
    bpy.ops.object.select_all(action='DESELECT')
    for dupTuple in dupObjs:
        dObj = dupTuple[0]
        dObj.select = True
    obj.select = True
    context.scene.objects.active = obj    
    bpy.ops.object.join_shapes()
    
    
    #fix names of shapekeys to match original
    #getting a bug here 070321
    for j in range(1,len(obj.data.shape_keys.key_blocks)):
        kblock = obj.data.shape_keys.key_blocks[j]
        n = getShapekeyName( kblock.name, dupObjs )
        kblock.name = n
        
    #cleanup
    bpy.ops.object.select_all(action='DESELECT')    
    for dupTuple in dupObjs:
        dObj = dupTuple[0]
        dObj.select = True
    context.scene.objects.active = dupObjs[0][0]
    bpy.ops.object.delete()
    
    #restore selection
    obj.select = True
    context.scene.objects.active = obj


def makeMeshUsingShapekeyIndex( obj=None, index = 1,context=None ):
    """not sure what this does
    """
    #result mesh no shapekeys. its shape would have matched shapekey at index
    dupMeshObj = None
    bpy.ops.object.select_all(action='DESELECT')
    obj.select = True
    context.scene.objects.active = obj
    
    bpy.ops.object.duplicate()
    dupMeshObj = context.selected_objects[-1]
    
    #first remove all shape keys on dupped object
    #then transfer just the given index onto dupped object
    #finally remove the basis shape and then last the only shapekey to get duped object at pose of index
    
    bpy.ops.object.shape_key_remove(all=True)
    obj.active_shape_key_index = index
    
    bpy.ops.object.select_all(action='DESELECT')
    obj.select = True
    dupMeshObj.select = True
    context.scene.objects.active = dupMeshObj
    bpy.ops.object.shape_key_transfer()

    bpy.ops.object.select_all(action='DESELECT')
    dupMeshObj.select = True
    context.scene.objects.active = dupMeshObj
    dupMeshObj.active_shape_key_index = 0
    bpy.ops.object.shape_key_remove(all=False)
    bpy.ops.object.shape_key_remove(all=True)
    
    return dupMeshObj
        
        
def importObj(shapeDir = ''):
    """import all .obj in shapeDir, use obj names for blender geo names
    """
    
    if not os.path.exists(shapeDir):
        return
    
    #find all objs in folder
    objFileNames = []
    toNames = [] #names to use for meshes in blender
    
    #if its a folder get all objs in it
    if os.path.isdir(shapeDir):
        for fpath in os.listdir(shapeDir):
            ff,fext = os.path.splitext(fpath)
            if fext.lower() == ".obj":
                objFileNames.append(fpath)
                toNames.append(ff)
    else:
        #import just the single obj path
        fpath = shapeDir
        fffull,fext = os.path.splitext(fpath)
        if fext.lower() == ".obj":
            fpathshort = os.path.split(fffull)[-1]
            objFileNames.append(fpath)
            toNames.append(fpathshort)
                
    #import objs into blender
    for f,toName in zip(objFileNames,toNames):
        fileName = os.path.join(shapeDir,f)
        importedObj = bpy.ops.import_scene.obj(filepath = fileName,
                                                use_split_objects = False,
                                                use_split_groups = False)
        obj = bpy.context.selected_objects[0]
        obj.name = toName
        obj.data.name = toName


def putMeshesInRow(meshObjs = [], gridWidth = 1):
    """
    #position given mesh objects in grid > nice to use bounding box
    #input how far apart want mesh
    """
    meshes = [m for m in meshObjs if m.type == 'MESH']
    
    xpos = 0
    for mesh in meshes:
        mesh.location = (xpos,0,0)
        xdim = mesh.dimensions[0]
        xpos += xdim+gridWidth #move a whole width of object plus given x offset
        
        
    
def mergeShapekeys(context = None):
    """
    1.given bunch of meshes with shape keys put all those shape keys on last selected object.
    last selection is target mesh
    """
    targetMesh = context.active_object
    sourceMeshes = [msh for msh in context.selected_objects if msh.name != targetMesh.name] 
    
    if len(sourceMeshes) == 0:
        print("please select 1 or more source meshes then last target mesh")
        return

    for sourceMesh in sourceMeshes:
        #selection
        bpy.ops.object.select_all(action='DESELECT')
        sourceMesh.select = True
        targetMesh.select = True
        context.scene.objects.active = targetMesh
        
        #changing active shapekey index to transfer shapes
        shapeKey = sourceMesh.data.shape_keys
        #if no shape key on source mesh skip it
        if not shapeKey:
            continue
        maxShapeKeyIndex = len(shapeKey.key_blocks) 
        for i in range(1,maxShapeKeyIndex):
            sourceMesh.active_shape_key_index = i
            bpy.ops.object.shape_key_transfer()
            targetMesh.show_only_shape_key = False
    
    #cleanup selection
    bpy.ops.object.select_all(action='DESELECT')
    targetMesh.select = True
    context.scene.objects.active = targetMesh
    
def zeroKeyOnSelectedVertices(context = None):
    """
    this zero out all selected vertices of shape key.
    """

    obj = context.active_object
    
    curMode = None
    if context.object:
        curMode = context.object.mode
        bpy.ops.object.mode_set(mode='OBJECT') #if something is selected go to object mode
    
    #assumes basis shape at index 0
    vertIndex = [v.index for v in obj.data.vertices if v.select]
    for i in vertIndex:
        basisx = obj.data.shape_keys.key_blocks[0].data[i].co.x
        basisy = obj.data.shape_keys.key_blocks[0].data[i].co.y
        basisz = obj.data.shape_keys.key_blocks[0].data[i].co.z
        #modify selected shape key
        activeIndex = obj.active_shape_key_index
        if activeIndex > 0:
            #zero out shape
            obj.data.shape_keys.key_blocks[activeIndex].data[i].co.x = basisx
            obj.data.shape_keys.key_blocks[activeIndex].data[i].co.y = basisy
            obj.data.shape_keys.key_blocks[activeIndex].data[i].co.z = basisz

    #restore mode
    if curMode:
        bpy.ops.object.mode_set(mode=curMode)





def zeroSelectedKeyInX(sign="+", includeCenter = False, context = None):
    """
    this zero out all vertices of shape key that are in direction of x axis.
    supports positive or negative x axis
    optionally zero out center vertices
    """
    def zeroShape(obj=None,vid=None):
        #zero shape on given vertex id
        basisx = obj.data.shape_keys.key_blocks[0].data[vid].co.x
        basisy = obj.data.shape_keys.key_blocks[0].data[vid].co.y
        basisz = obj.data.shape_keys.key_blocks[0].data[vid].co.z        
        activeIndex = obj.active_shape_key_index
        if activeIndex > 0:
            #zero out shape
            obj.data.shape_keys.key_blocks[activeIndex].data[vid].co.x = basisx
            obj.data.shape_keys.key_blocks[activeIndex].data[vid].co.y = basisy
            obj.data.shape_keys.key_blocks[activeIndex].data[vid].co.z = basisz


    obj = context.active_object
    
    #assumes basis shape at index 0
    verts = obj.data.vertices

    if sign == "+":
        for i in range(len(verts)):
            basisx = obj.data.shape_keys.key_blocks[0].data[i].co.x
            if includeCenter:
                if basisx >= 0:
                    zeroShape(obj,i)
            else:
                if basisx > 0:
                    zeroShape(obj,i)            
    else:
        for i in range(len(verts)):
            basisx = obj.data.shape_keys.key_blocks[0].data[i].co.x
            if includeCenter:
                if basisx <= 0:
                    zeroShape(obj,i)
            else:
                if basisx < 0:
                    zeroShape(obj,i)


def zeroAllKeys():
    """
    when first testing shapekeys. this zeros them all out again
    """
    obj = bpy.context.active_object
    allKeys = obj.data.shape_keys.key_blocks.keys()
    for key in allKeys:
        obj.data.shape_keys.key_blocks[key].value = 0



def removeDigitShapeKeys():
    """noticed in fbx import from zbrush extra shape keys with digits at end
    this removes those shape keys
    """
    obj = bpy.context.active_object
    
    allKeys = obj.data.shape_keys.key_blocks.keys()
    keysEndDigit = [x for x in allKeys if x[-1].isdigit()]
    #print(keysEndDigit)
    
    for shape in keysEndDigit:
        index = obj.data.shape_keys.key_blocks.keys().index(shape)
        obj.active_shape_key_index = index
        bpy.ops.object.shape_key_remove()


def makeMeshWhole(obj):
    """default mirror from +x to -x of selected mesh, assumes no mirror modifiers on mesh to start
    """
    bpy.ops.object.modifier_add(type='MIRROR')
    bpy.ops.object.modifier_apply(modifier='Mirror') 
    
    
def deleteHalfMesh(obj):
    """
    default deletes -x side of selected mesh. standalone need give it bpy.context.object for selected object.
    doesnt work if no center line for mesh
    """
    #get current position
    curpos = ()
    curpos = (obj.location.x,obj.location.y,obj.location.z)
    
    #put obj at origin
    setLocation(obj,(0,0,0))
    
    selectedObj = obj #bpy.context.selected_objects[0]
    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all( action='DESELECT')
    bpy.ops.mesh.select_mode(type='FACE')
    
    
    bm = bmesh.from_edit_mesh(selectedObj.data)
    for face in bm.faces:
        faceWorldPos = selectedObj.matrix_world*face.calc_center_median() #calc_center_median same as face.center using obj.data.polygons
        #[0] > 0.0 would be delete right half of mesh
        #[1] < 0.0 would be delete in y direction
        if faceWorldPos[0] < 0.0:
            face.select = True
            
    bm.select_flush(True)
    bpy.ops.mesh.delete(type='FACE')
    bpy.ops.object.mode_set(mode='OBJECT')
            
    #restore location
    setLocation(obj,curpos)
    
def setLocation(obj,pos):
    """ does location only. pos tuple (2.3,0,0) """
    obj.location.x = pos[0]
    obj.location.y = pos[1]
    obj.location.z = pos[2]






def createCorrectiveSculpt( pose = None, sculpt = None, shape_key_mesh = None ):
    """
    1. given a pose mesh name, a sculpt mesh name, and the mesh that has all shape keys (object names)
    2. create a default mesh from the mesh with all shape keys
    3. add pose and sculpt as shapkeys to temp default mesh
    4. set pose -1 value and sculpt to 1
    5. duplicate tmp default mesh keeping the corrective but clean
    6. delete tmp default mesh
    """
    #assumes all inputs provided and exist and in object mode
    
    #create temporary default mesh
    default_mesh = createCleanMesh(mesh = shape_key_mesh, keepPose=False)
    default_mesh_obj = bpy.data.objects[default_mesh]
    
    #add pose and sculpt as shapkeys to temp default mesh
    bpy.ops.object.select_all(action='DESELECT')
    bpy.data.objects[pose].select = True
    bpy.data.objects[sculpt].select = True
    bpy.context.scene.objects.active = default_mesh_obj
    bpy.ops.object.join_shapes()
    
    #set pose -1 value and sculpt to 1 > so when we eventually use the corrective can add it ontop of pose
    default_mesh_obj.data.shape_keys.key_blocks[pose].slider_min = -1.0 #needed before can change shapekey value to negative
    default_mesh_obj.data.shape_keys.key_blocks[pose].value = -1.0
    default_mesh_obj.data.shape_keys.key_blocks[sculpt].value = 1.0
    
    #duplicate tmp default mesh keeping the corrective but clean
    corrective_mesh = createCleanMesh(mesh = default_mesh_obj.name, keepPose=True)
    
    #delete tmp default mesh
    tmp_dat = default_mesh_obj.data
    bpy.data.objects.remove(default_mesh_obj)
    bpy.data.meshes.remove(tmp_dat)
    
    #maybe later position corrective mesh, and or give it a name


def createCleanMesh(mesh = None, keepPose=False, apply_types=[]):
    """
    mesh - mesh with shape keys object name > if its not provided it uses current selection
    keepPose - whehter or not to keep the current pose for clean mesh
    apply_types - list of modifier type that should be applied. ex: ["ARMATURE"] if empty it just deletes all modifiers
    
    1. given selected mesh with shape keys
    2. duplicate mesh
    3. create shape from mix (if keepPose)
    4. delete all shape keys except for mix > then delete mix shapekey
    (later add support to delete any modifiers)
    5. position clean mesh next to original
    
    returns the create clean mesh object name
    """
    
    """
    if mesh:
        mesh_obj = bpy.data.objects[mesh]
        #make it only selection
        bpy.ops.object.select_all(action='DESELECT')
        mesh_obj.select = True
        bpy.context.scene.objects.active = mesh_obj
    """
    _selectOnlyMesh(mesh)
    
    #assume have a selected mesh that has shapekeys in object mode
    orig_mesh = bpy.context.object
    
    #duplicate mesh
    bpy.ops.object.duplicate()
    pose_mesh = bpy.context.object
    
    #create shape from mix so we keep the current pose
    if keepPose:
        bpy.ops.object.shape_key_add(from_mix = True)
    
    #delete all shapekeys on mesh > remove all other shapes before last the active shape
    active_index = 0 #the basis by default
    if keepPose:
        active_index = pose_mesh.active_shape_key_index
    active_shapekey_name = pose_mesh.data.shape_keys.key_blocks[active_index].name
    
    non_active_shps = [shp for shp in pose_mesh.data.shape_keys.key_blocks if shp.name != active_shapekey_name]
    for shp in non_active_shps:
        pose_mesh.shape_key_remove(shp)
    mix_shp = pose_mesh.data.shape_keys.key_blocks[0]
    pose_mesh.shape_key_remove(mix_shp)
    
    
    #delete any modifiers
    _cleanModifiers(mesh = pose_mesh.name, apply_types = apply_types )
    #delete any vertex groups
    _cleanVertexGroups(mesh = pose_mesh.name)
    
    #position clean mesh
    width = orig_mesh.dimensions.x 
    pose_mesh.location.x = width+width/3.0
    
    return pose_mesh.name

def _selectOnlyMesh(mesh = None):
    #might need to be in object mode
    if mesh:
        mesh_obj = bpy.data.objects[mesh]
        #make it only selection
        bpy.ops.object.select_all(action='DESELECT')
        mesh_obj.select = True
        bpy.context.scene.objects.active = mesh_obj    
    

def _cleanModifiers(mesh = None, apply_types = [] ):
    """
    mesh - data object name for mesh to edit
    apply_types - list of modifier type that should be applied. ex: ["ARMATURE"] if empty it just deletes all modifiers
    """
    if not mesh:
        return
    
    _selectOnlyMesh(mesh)
    
    modifier_names = [ mod.name for mod in bpy.data.objects[mesh].modifiers ] 
    
    for mod_name in modifier_names:
        if bpy.data.objects[mesh].modifiers[mod_name].type in apply_types:
            bpy.ops.object.modifier_apply(apply_as='DATA', modifier = mod_name)
            continue
            
        bpy.ops.object.modifier_remove(modifier = mod_name)
        
    #make sure mesh not parented to anything example armature
    if bpy.data.objects[mesh].parent:
        bpy.data.objects[mesh].parent = None    
    

def _cleanVertexGroups(mesh = None):
    """remove any vertex groups on given mesh
    mesh - data object name for mesh to edit
    """
    if not mesh:
        return
    
    _selectOnlyMesh(mesh)
    
    #ops errors out if no vertex group exists
    if len(bpy.data.objects[mesh].vertex_groups) > 0:
        bpy.ops.object.vertex_group_remove(all=True)


def copySculptToBlendshapeByDict(copySculptDict):
    #@param copySculptToDict sculpt:meshShape ex: {"Cube.001":"shape.L"}
    
    selection = [obj.name for obj in bpy.context.selected_objects]
    if len(selection) != 2:
        print("please select two meshes first any sculpt mesh, last mesh with blendshapes")
        return
    mesh = bpy.context.active_object.name
    
    for sculptMesh, shapeKeyName in copySculptDict.items():
        #index of shapekey on mesh
        matchIndex = [i for i,kb in enumerate(bpy.data.objects[mesh].data.shape_keys.key_blocks) if kb.name == shapeKeyName] 
        if not matchIndex:
            logger.info("skipping {} could not find matching shapekey".format(shapeKeyName))
            continue
        active_index = matchIndex[0]
        
        for v, key_point in enumerate(bpy.data.objects[mesh].data.shape_keys.key_blocks[active_index].data):
            #might want to later verify sculpt has same topology vertex count as shapekey
            sculptPos = bpy.data.objects[sculptMesh].data.vertices[v].co
            bpy.data.objects[mesh].data.shape_keys.key_blocks[active_index].data[v].co = sculptPos
            
        
    
def copySculptToBlendshape():
    """copies a separate sculpt mesh to an existing blendshape
    1. given two things selected: first sculpt mesh, last mesh with blendshapes and a blendshape selected
    2. loop all verts in blendshape and set its local position to the sculpt meshes local position
    """        
    sculpt_mesh = None
    mesh = None
    
    #assumes proper selections made
    selection = [obj.name for obj in bpy.context.selected_objects]
    if len(selection) != 2:
        print("please select two meshes first sculpt mesh, last mesh with blendshapes")
        return
    mesh = bpy.context.active_object.name
    #print("selection>>>",selection)
    sculpt_mesh = list( set(selection) - set([mesh]) )[0] #to get first selected remove active selection from selection
    #print("mesh, sculpt_mesh>>>",mesh,sculpt_mesh)

    sculpt_vert_count = len(bpy.data.objects[sculpt_mesh].data.vertices)
    mesh_vert_count = len(bpy.data.objects[mesh].data.vertices)
    if  sculpt_vert_count != mesh_vert_count:
        print("requires same topology sculpt: %s mesh: %s" %(sculpt_vert_count,mesh_vert_count) )
        return

    #2. loop all verts in blendshape and set its local position to the sculpt meshes local position
    active_index = 0
    active_index = bpy.data.objects[mesh].active_shape_key_index
    
    for v, key_point in enumerate(bpy.data.objects[mesh].data.shape_keys.key_blocks[active_index].data):
        #might want to later verify sculpt has same topology vertex count as shapekey
        sculpt_pos = bpy.data.objects[sculpt_mesh].data.vertices[v].co
        bpy.data.objects[mesh].data.shape_keys.key_blocks[active_index].data[v].co = sculpt_pos
        
        

def mirrorBlendshape( mesh = None, blendshapes = [], use_topology = False ):
    """
    mesh - name of mesh with blendshapes. the data object name
    blendshapes - list of blendshapes we want to mirror
    use_topology - the bpy.ops shape_key_mirror topology option. i think sometimes might need to have this True
    
    1. given list of shapes
    2. select a shape and isolate it on
    3. make new shape from mix
    4. mirror new shape using argument usetopo
    5. rename new shape (if original shape ends with .L substitute L with R)
    6. repeat for all shapes
    
    for mirror shape when right side already exists:
    1. 1.
    2. 2.
    3. 3.
    4. 4.
    5. get index of temp mirrored shape
    6. get index of existing old mirrored shape
    7. set the old mirrored shape to be same as temp mirrored shape
    8. delete temp mirrored shape
    """
    for blendshape in blendshapes:
        
        exists_rhs = False
        #if right side already exists we need to do some additional steps
        mirrored_blendshape_new_name = blendshape.split('.')[0]+'.R'
        if mirrored_blendshape_new_name in [shp.name for shp in bpy.data.objects[mesh].data.shape_keys.key_blocks]:
            exists_rhs = True
            
        bs_index = getBlendshapeIndexFromName( mesh, blendshape ) #assumes it exists
        if bs_index is None:
            print("skipping %s >> could not find it in blendshapes drop down" %blendshape)
            continue
        #2. select a shape and isolate it on
        bpy.data.objects[mesh].active_shape_key_index = bs_index
        bpy.data.objects[mesh].show_only_shape_key = True

        #3. make new shape from mix
        bpy.ops.object.shape_key_add(from_mix = True)
        active_index = bpy.data.objects[mesh].active_shape_key_index
        
        #4. mirror new shape using argument usetopo
        bpy.ops.object.shape_key_mirror(use_topology = use_topology)
        
        ###additional steps if right side exists
        if exists_rhs:
            #shape key index of already existing right side
            rhs_index = getBlendshapeIndexFromName( mesh, mirrored_blendshape_new_name )
            
            #set the old mirrored shape to be same as temp mirrored shape
            for v, key_point in enumerate(bpy.data.objects[mesh].data.shape_keys.key_blocks[rhs_index].data):
                #print("updating rhs vertex %s" %v)
                mirror_pos = bpy.data.objects[mesh].data.shape_keys.key_blocks[active_index].data[v].co
                bpy.data.objects[mesh].data.shape_keys.key_blocks[rhs_index].data[v].co = mirror_pos
                
            #delete temp mirrored shape
            print("need to delete temp mirrored shape")
            bpy.ops.object.shape_key_remove(all=False)
        ###
        
        
        #5. rename new shape (if original shape ends with .L substitute L with R)
        if blendshape.endswith('.L') and not exists_rhs:
            print(">>>mirroring name")
            bpy.data.objects[mesh].data.shape_keys.key_blocks[active_index].name = mirrored_blendshape_new_name

        #restore might want to be more specific here
        bpy.data.objects[mesh].show_only_shape_key = False
        
def getBlendshapeIndexFromName( mesh = None, blendshape = None):
    result = None
    
    for i in range( 0,len(bpy.data.objects[mesh].data.shape_keys.key_blocks) ):
        if bpy.data.objects[mesh].data.shape_keys.key_blocks[i].name == blendshape:
            result = i
            return result
            
    return result
    
def getAllLeftSideBlendshapes(mesh = None):
    """returns all left side blendshape names
    """
    result = []
    
    for i in range( 0,len(bpy.data.objects[mesh].data.shape_keys.key_blocks) ):
        bs_name = bpy.data.objects[mesh].data.shape_keys.key_blocks[i].name
        if bs_name.endswith(".L"):
            result.append(bs_name)
            
    return result


###set to default related
def setToDefaultByVertexGroupCmd(context):
    """first select vertex group mesh, then mesh with shapekeys
    """
    #assumes proper selections made
    selection = [obj.name for obj in context.selected_objects]
    if len(selection) != 2:
        print("please select two meshes first vertex group mesh, last mesh with blendshapes")
        return
    mesh = context.active_object.name
    #print("selection>>>",selection)
    vertexGroupMesh = list( set(selection) - set([mesh]) )[0] #to get first selected remove active selection from selection
    
    blendDefault = BlendshapeDefault(mesh)
    blendDefault.setToDefaultByVertexGroup(vertexGroupMesh=vertexGroupMesh)
    
def setToDefaultBySideCmd(context):    
    #assumes proper selections made
    selection = [obj.name for obj in context.selected_objects]
    if len(selection) != 1:
        print("please select mesh with blendshapes")
        return
    mesh = context.active_object.name
    blendDefault = BlendshapeDefault(mesh)
    blendDefault.setToDefaultBySide(side='R')


class BlendshapeDefault(object):
    """handles defaulting vertices for cleanup. example for ZBrush sculpts

    usage:
    import imp
    testTool = imp.load_source("tool","/Users/Nathaniel/Desktop/learning_python_blender_faceRigApproach/shapekey_snippets.py")

    blendDefault = testTool.BlendshapeDefault("Cube")
    blendDefault.setToDefaultByVertexGroup(vertexGroupMesh="Cube.002")
    
    """
    def __init__(self, mesh):
        self._mesh = mesh #mesh object name that has all shapekeys we want to edit

    def setToDefaultByVertexGroup(self, vertexGroupMesh=None, blendshapes=[]):
        """
        #so can have zbrush sculpts cleaned up where vertices at back of head were moved accidentally
        @param vertexGroupMesh (str) name for vertex group mesh. assumes it has a single vertex group
        @param blendshapes (list) - (optional) blendshape names we wish to edit. if empty it edits all blendshapes
        """
        mesh = self._mesh

        blendshape_names = []
        if blendshapes:
            blendshape_names = blendshapes
        else:
            all_shps = [shp.name for shp in bpy.data.objects[mesh].data.shape_keys.key_blocks]
            blendshape_names = all_shps

        #for each ex blendshape  use basis default to set position of all verts of vertex group
        for shp in blendshape_names:
            for vert_id, shp_key_point in enumerate(bpy.data.objects[mesh].data.shape_keys.key_blocks[shp].data ):
                should_edit_vert = False #should this vertex be set to default position
                
                #if vertex is part of a vertex group should apply default to it
                if len(bpy.data.objects[vertexGroupMesh].data.vertices[vert_id].groups) > 0:
                    should_edit_vert = True
                    
                if not should_edit_vert:
                    continue
                    
                default_pos = bpy.data.objects[mesh].data.shape_keys.key_blocks['Basis'].data[vert_id].co #vert_id is the vertex index
                #does the actual changing of shapekey vertex position
                shp_key_point.co = default_pos



    def setToDefaultBySide(self, side='R', blendshapes=[]):
        """ex: zero out right side of multiple shapes
        @param side (str) - side that should get set to default. default is right side of face or screen left
        @param blendshapes (list) - (optional) blendshape names we wish to edit. if empty it edits all blendshapes ending with .L
        
        1. given mesh with all .L blendshapes
        2. get all .L blendshapes
        3. for each ex .L blendshape  use basis default to set position of all verts on right side
        """
        #assumes mesh has all shapekeys we wish to edit and in object mode with no blendshapes isolated
        mesh = self._mesh

        blendshape_names = []
        if blendshapes:
            blendshape_names = blendshapes
        else:
            left_side_shps = [shp.name for shp in bpy.data.objects[mesh].data.shape_keys.key_blocks if shp.name.endswith('.L')]
            blendshape_names = left_side_shps
        
        if not blendshape_names:
            print("could not find any blendshapes to edit. either add suffix like .L to blendshapes or provide specific blendshape names in text field")
            
        #for each ex .L blendshape  use basis default to set position of all verts on right side
        for shp in blendshape_names:
            for vert_id, shp_key_point in enumerate( bpy.data.objects[mesh].data.shape_keys.key_blocks[shp].data ):
                should_edit_vert = False #should this vertex be set to default position
                
                if side == 'R' and shp_key_point.co.x < 0.0: #vertex on screen left
                    should_edit_vert = True
                elif side == 'L' and shp_key_point.co.x > 0.0: #vertex on screen right
                    should_edit_vert = True
                    
                if not should_edit_vert:
                    continue
                    
                default_pos = bpy.data.objects[mesh].data.shape_keys.key_blocks['Basis'].data[vert_id].co #vert_id is the vertex index
                #does the actual changing of shapekey vertex position
                shp_key_point.co = default_pos

#end set to default related


class BlendshapeRecover(object):
    """class to help create blendshape from a different topology mesh based off of world position
    """
    def __init__(self, main=None, sculpt=None, defaultSculpt=None):
        """
        @param main (str) the mesh (data object name) that should be sculpted. it should have vertex group for transfer region
        @param sculpt (str) the sculpt mesh
        @param defaultSculpt (str) the default sculpt mesh
        """
        self._main = main
        self._sculpt = sculpt
        self._defaultSculpt = defaultSculpt
        self._epsilon = 0.0001 #anything smaller than this counts as same position
    
    def copySculptBySelection(self, context, defaultSculpt):
        #first select sculpt then select mesh to apply sculpt
        selection = [obj.name for obj in context.selected_objects]
        #logger.info("selection")
        #logger.info(selection)
        if len(selection) == 2:
            self._main = context.active_object.name
            selection.remove(self._main)
            self._sculpt = selection[0]
            self._defaultSculpt = defaultSculpt
            self.doIt()
        
    def doIt(self):
        """copy sculpt onto main mesh for given main region
        ###
        #loop all vertices in main region (later if no main region look at all vertices of main)
        #
        #find closest vertex in defaultSculpt
        #
        #save the delta of that vertex going from defaultSculpt to sculpt
        #
        #when done above. finally loop all vertices in main region apply saved vertex delta position
        ###
        """
        if (not self._main in bpy.data.objects) or (not self._sculpt in bpy.data.objects) or (not self._defaultSculpt in bpy.data.objects):
            logger.warning("missing inputs skipping copy sculpt")
            return
            
        mainVertexIdToDeltaDict = {}
        mainRegionVertices = [vert_id.index for vert_id in bpy.data.objects[self._main].data.vertices if len(bpy.data.objects[self._main].data.vertices[vert_id.index].groups) > 0]
        #if no region vertices to use it looks at all vertices.
        if not mainRegionVertices:
            mainRegionVertices = [vert_id.index for vert_id in bpy.data.objects[self._main].data.vertices]
            
        
        for vertex in mainRegionVertices:
            mainPosition = bpy.data.objects[self._main].data.vertices[vertex].co #local position, so should work if main is initially translated
            #find closest vertex id in defaultSculpt
            for defaultSculptVertexObj in bpy.data.objects[self._defaultSculpt].data.vertices:
                defaultSculptPosition = defaultSculptVertexObj.co
                distanceVec = mainPosition - defaultSculptPosition
                distance = math.sqrt((distanceVec[0]*distanceVec[0]) + (distanceVec[1]*distanceVec[1]) + (distanceVec[2]*distanceVec[2]))
                if distance <= self._epsilon:
                    #found closest vertex on defaultSculpt
                    sculptPosition = bpy.data.objects[self._sculpt].data.vertices[defaultSculptVertexObj.index].co
                    
                    defaultToSculptDelta = sculptPosition - defaultSculptPosition #this is offset to go from defaultSculpt to Sculpt
                    
                    mainVertexIdToDeltaDict[vertex] = defaultToSculptDelta
                    break #exit loop
                    
        #logger.info("testing delta dictionary")
        #logger.info(mainVertexIdToDeltaDict)

        #apply sculpt to main mesh
        for vertexId, offset in mainVertexIdToDeltaDict.items():
            currentPos = bpy.data.objects[self._main].data.vertices[vertexId].co
            bpy.data.objects[self._main].data.vertices[vertexId].co = currentPos + offset
            
            


"""
import bpy
import imp
test_tool = imp.load_source("tool", "/users/Nathaniel/Documents/src_blender/python/naBlendShape/naShapekeyUtilAddOn.py")

bsRecover = test_tool.BlendshapeRecover()
bsRecover.copySculptBySelection("defaultSculpt") #first select sculpt then mesh want to apply sculpt

#bsRecover = test_tool.BlendshapeRecover(main="Cube", sculpt="sculpt", defaultSculpt="defaultSculpt")
#bsRecover.doIt()
"""

"""
import bpy
import sys
sys.path.append("/users/Nathaniel/Documents/src_blender/python/naBlendShape")
import naShapekeyUtilAddOn as mod
import imp
imp.reload(mod)

bsRecover = mod.BlendshapeRecover(main="Cube", sculpt="sculpt", defaultSculpt="defaultSculpt")
bsRecover.doIt()

#mod.importObj('/Users/Nathaniel/Documents/src_blender/python/snippets/pipeTools')
#mod.mirrorShapekeyTopology(obj=bpy.context.selected_objects[0],context=bpy.context)

"""

#inspired by
#https://blender.stackexchange.com/questions/1412/efficient-way-to-get-selected-vertices-via-python-without-iterating-over-the-en
#https://blender.stackexchange.com/questions/111661/creating-shape-keys-using-python
#https://blenderartists.org/t/delete-shape-key-by-name-via-python/521762/3
#https://stackoverflow.com/questions/14471177/python-check-if-the-last-characters-in-a-string-are-numbers
#https://stackoverflow.com/questions/3964681/find-all-files-in-a-directory-with-extension-txt-in-python
#https://stackoverflow.com/questions/541390/extracting-extension-from-filename-in-python
#https://stackoverflow.com/questions/8933237/how-to-find-if-directory-exists-in-python
#https://blender.stackexchange.com/questions/18035/code-inside-function-not-working-as-it-should
#https://blender.stackexchange.com/questions/43820/how-to-use-the-file-browsers-with-importhelper-execute-function
#https://blender.stackexchange.com/questions/42654/ui-how-to-add-a-file-browser-to-a-panel
#https://blender.stackexchange.com/questions/23258/trouble-file-stringproperty-subtype-file-path
#https://stackoverflow.com/questions/3391076/repeat-string-to-certain-length 
Thanks for looking 

Saturday, February 28, 2026

face sculpts in ZBrush

 

i mostly used insert sphere/cube/cylinder, move, trim dynamic, clay buildup, smooth, dam_standard, move and dynamesh brushes/tools to make these sculpts. (original design and sculpt for personal work)

 

Thanks for looking

Saturday, February 21, 2026

Face sketch

 


Thanks for looking