3
$\begingroup$

Disclaimer: I'm new to Blender Python. Thank you in advance for your assistance, and patience. I saw this related question but I'm not sure it applies here.

I am developing an addon with a panel class that doesn't unregister when I disable the addon in blender preferences. Not even when I remove the addon. The panel won't be removed unless I restart blender after disabling/removing the addon. So even after removing the addon, I'm left with this panel until I restart blender:

enter image description here

I'm not sure what blender is looking for on the back end when enabling and disabling addons, and having a look at blender's documentation hasn't cleared things up for me. So I'm hoping the wonderful community can shed some light here. What am I doing wrong?

Here's the code

bl_info = {
    "Cam-Shape-Matic" : "CamShpMtc",   
    "category": "3D View",   
    "author": "BeyondDev",
    "blender": (2, 8, 0),
    "version": (0, 0, 1),
    "category": "Object",
    "description": "Drive Shapekeys on Objects based on Relative Camera Angle.",
    "doc_url": "",
    "tracker_url": "",
}
from asyncio.windows_events import NULL
import bpy
import mathutils


# To workaround the "known bug with using a callback" mentioned
# in the EnumProperty docs, this function needs to be called on
# any EnumProperty's items.
ENUM_STRING_CACHE = {}
def intern_enum_items_strings(items):
    def intern_string(s):
        if isinstance(s, str):
            ENUM_STRING_CACHE.setdefault(s, s)
            s = ENUM_STRING_CACHE[s]
        return s

    return [
        tuple([intern_string(s) for s in item])
        for item in items
    ]


# This callback will return a list of enum items in the usual
# [(identifier, name, description), ...] format.
def my_shapekey_enum_items_callback(self, context):
    items = []

    if self and self.shape_keys:
        items = [
            (kb.name, kb.name, '')
            for i, kb in enumerate(self.shape_keys.key_blocks)
        ]

    return intern_enum_items_strings(items)


# This callback will return a list of enum items in the usual
# [(identifier, name, description), ...] format.
def my_object_enum_items_callback(self, context):
    items = []

    if  bpy.context.window and bpy.context.window.view_layer:
        items = [
            (ob.name, ob.name, '')
            for i, ob in enumerate(bpy.context.window.view_layer.objects) if bpy.context.window.view_layer.objects[i].type == 'MESH'
        ]

    return intern_enum_items_strings(items)


def my_bone_enum_items_callback(self, context):
    items = []
    selObj = bpy.context.window_manager.object
    selArmatureParse = bpy.data.objects[selObj].find_armature().name.split('_') #parse returned name 'Object_rig' into list [Object, rig]
    selArmature = selArmatureParse[len(selArmatureParse)-1] #get name of rig from last item in parsed list (above)

    if  bpy.context.window and bpy.context.window.view_layer and selArmature:
        items = [
            (ob.name, ob.name, '')
            for i, ob in enumerate(bpy.data.armatures[selArmature].bones) if bpy.data.armatures[selArmature].bones[i].use_deform == True
        ]

    return intern_enum_items_strings(items)
    

class CamShapeMaticPanel(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'CamShpMtc'
    bl_label = "CamShapeMatic"
    bl_idname = "MAIN_PT_camshapematic_panel"    

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        col = box.column(align=True)
        row = col.row(align=True)

        col.separator()
        row = col.row(align=True)
        row.scale_y = 1.1
        row.prop(context.window_manager, "EnableCamShapeMatic", text="Enable/Disable Shape Changes")

        scene = context.scene

        col.separator()
        row = col.row(align=True)
        row.label(text='Select Camera')
        row = col.row(align=True)
        row.scale_y = 1.1
        row.prop(scene, "camera")
        row = col.row(align=True)
        row.scale_y = 1.1
        row.prop(context.window_manager, "AngleSensitivity", text="Cam Angle Sensitivity")

        col.separator()
        row = col.row(align=True)
        row.label(text='Select Object')
        row = col.row(align=True)
        row.scale_y = 1.1
        row.prop(context.window_manager, "object", icon="MESH_MONKEY", text='Mesh')

        selObj = bpy.context.window_manager.object
        AngleSens = bpy.context.window_manager.AngleSensitivity
        Enable = bpy.context.window_manager.EnableCamShapeMatic

        # draw shapekey_top/bottom/left/right for the current object
        # only if it's a mesh with shapekeys though
        if (
            bpy.data and
            bpy.data.objects and
            bpy.data.objects[selObj].type == 'MESH' and
            bpy.data.objects[selObj].data.shape_keys
        ):            
            
            col.separator()
            row = col.row(align=True)
            row.label(text='Select Shapekeys')

            row = col.row(align=True)
            row.scale_y = 1.1
            row.prop(bpy.data.objects[selObj].data, "shapekey_top", icon='SHAPEKEY_DATA', text='Top')
            row = col.row(align=True)
            row.scale_y = 1.1
            row.prop(bpy.data.objects[selObj].data, "shapekey_bottom", icon='SHAPEKEY_DATA', text='Bottom')
            row = col.row(align=True)
            row.scale_y = 1.1
            row.prop(bpy.data.objects[selObj].data, "shapekey_left", icon='SHAPEKEY_DATA', text='Left')
            row = col.row(align=True)
            row.scale_y = 1.1
            row.prop(bpy.data.objects[selObj].data, "shapekey_right", icon='SHAPEKEY_DATA', text='Right')
        
        
        # If the selected object has an armature
        if (
            bpy.data and
            bpy.data.objects and
            bpy.data.objects[selObj].find_armature()
        ):
            col.separator()
            row = col.row(align=True)
            row.label(text='Select Bone (ex: Head Bone)')

            row = col.row(align=True)
            row.scale_y = 1.1
            row.prop(context.window_manager, "bone", icon='BONE_DATA', text='Deform Bone')
            row = col.row(align=True)



        # only if it's a mesh with shapekeys though
        if (
            bpy.data and
            bpy.data.objects and
            bpy.data.objects[selObj].type == 'MESH' and
            bpy.data.objects[selObj].data.shape_keys and
            Enable == True
        ):    


            if bpy.data.objects[selObj].data.shapekey_top != NULL:

                cam = bpy.context.scene.camera
                obj = bpy.data.objects[selObj]

                if (
                    bpy.data and
                    bpy.data.objects and
                    bpy.data.objects[selObj].find_armature()
                ):
                    selArmature = bpy.data.objects[selObj].find_armature().name
                    rot = bpy.data.objects[selArmature].pose.bones[bpy.context.window_manager.bone]
                    self.obj_eul = rot.matrix.to_euler('XYZ') #this works for an obj OR a bone, world space
                    self.loc = rot.matrix.to_translation()
                else:
                    rot = bpy.data.objects[selObj]
                    self.obj_eul = rot.rotation_euler
                    self.loc = rot.location

                def drive_shapekey(scene, post):

                    try:
                        del line
                    except:
                        pass

                    objKey = obj.data.shape_keys #bpy.data.shapekeys["Key.00_"]


                    if Enable == True:
                        #LEFT -----------------------------------------------------
                        #obj_eul = rot.rotation_euler
                        cam_vec = self.loc - cam.matrix_world.to_translation()
                        cam_vec = cam_vec.normalized()*mathutils.Vector((1.0,1.0,0.0))
                        obj_vec = mathutils.Vector((-1.0, 0.0, 0.0))
                        obj_vec.rotate(self.obj_eul)
                        difference = cam_vec.dot(obj_vec)

                        shapekey_Name = obj.data.shapekey_left #Name of shapekey
                        key_left = objKey.key_blocks[shapekey_Name] #Datapath ending for shapekey value
                        key_left.value = difference*AngleSens

                        #RIGHT -----------------------------------------------------
                        #obj_eul = rot.rotation_euler
                        cam_vec = self.loc - cam.matrix_world.to_translation()
                        cam_vec = cam_vec.normalized()*mathutils.Vector((1.0,1.0,0.0))
                        obj_vec = mathutils.Vector((1.0, 0.0, 0.0))
                        obj_vec.rotate(self.obj_eul)
                        difference = cam_vec.dot(obj_vec)

                        shapekey_Name = obj.data.shapekey_right #Name of shapekey
                        key_right = objKey.key_blocks[shapekey_Name] #Datapath ending for shapekey value
                        key_right.value = difference*AngleSens

                        #TOP -----------------------------------------------------
                        #obj_eul = rot.rotation_euler
                        cam_vec = self.loc - cam.matrix_world.to_translation()
                        cam_vec = cam_vec.normalized()*mathutils.Vector((0.0,1.0,1.0))
                        obj_vec = mathutils.Vector((0.0, -1.0, 0.0)) #without rotate math below, this should be (0.0, 0.0, -1.0)
                        obj_vec.rotate(self.obj_eul)
                        difference = cam_vec.dot(obj_vec)

                        shapekey_Name = obj.data.shapekey_top #Name of shapekey
                        key_top = objKey.key_blocks[shapekey_Name] #Datapath ending for shapekey value
                        key_top.value = difference*AngleSens - key_right.value - key_left.value

                        line = [self.loc, cam.location]

                        #BOTTOM -----------------------------------------------------
                        #obj_eul = rot.rotation_euler
                        cam_vec = self.loc - cam.matrix_world.to_translation()
                        cam_vec = cam_vec.normalized()*mathutils.Vector((0.0,1.0,1.0))
                        obj_vec = mathutils.Vector((0.0, 1.0, 0.0)) #without rotate math below, this should be (0.0, 0.0, 1.0)
                        obj_vec.rotate(self.obj_eul)
                        difference = cam_vec.dot(obj_vec)

                        shapekey_Name = obj.data.shapekey_bottom #Name of shapekey
                        key_bottom = objKey.key_blocks[shapekey_Name] #Datapath ending for shapekey value
                        key_bottom.value = difference*AngleSens - key_right.value - key_left.value


            pre_handlers_scene = bpy.app.handlers.depsgraph_update_pre #Drive Shapekey on schene updates
            pre_handlers_anim = bpy.app.handlers.frame_change_pre #Drive shapekey before frame changes
            pre_handlers_render = bpy.app.handlers.render_pre #Drive shapekey during renders
            
            [pre_handlers_scene.remove(h) for h in pre_handlers_scene if h.__name__ == "drive_shapekey"]
            pre_handlers_scene.append(drive_shapekey)

            [pre_handlers_anim.remove(h) for h in pre_handlers_anim if h.__name__ == "drive_shapekey"]
            pre_handlers_anim.append(drive_shapekey) 

            [pre_handlers_render.remove(h) for h in pre_handlers_render if h.__name__ == "drive_shapekey"]
            pre_handlers_render.append(drive_shapekey) 


classes = [CamShapeMaticPanel]

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    
    bpy.types.WindowManager.object = bpy.props.EnumProperty(
    name="Selected Object",
    description="Select an object",
    items=my_object_enum_items_callback,
    )
    
    bpy.types.WindowManager.bone = bpy.props.EnumProperty(
    name="Selected Bone",
    description="Select a bone",
    items=my_bone_enum_items_callback,
    )

    bpy.types.Mesh.shapekey_top = bpy.props.EnumProperty(
        name="Top Shapekey",
        description="Select a shapekey",
        items=my_shapekey_enum_items_callback,
    )

    bpy.types.Mesh.shapekey_bottom = bpy.props.EnumProperty(
        name="Bottom Shapekey",
        description="Select a shapekey",
        items=my_shapekey_enum_items_callback,
    )

    bpy.types.Mesh.shapekey_left = bpy.props.EnumProperty(
        name="Left Shapekey",
        description="Select a shapekey",
        items=my_shapekey_enum_items_callback,
    )

    bpy.types.Mesh.shapekey_right = bpy.props.EnumProperty(
        name="Right Shapekey",
        description="Select a shapekey",
        items=my_shapekey_enum_items_callback,
    )

    bpy.types.WindowManager.AngleSensitivity = bpy.props.FloatProperty(default=1.5, max=2.0, min=1.0)
    bpy.types.WindowManager.EnableCamShapeMatic = bpy.props.BoolProperty(default=True)

def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)
    
    del bpy.types.WindowManager.object
    del bpy.types.WindowManager.bone
    del bpy.types.Mesh.shapekey_top
    del bpy.types.Mesh.shapekey_bottom
    del bpy.types.Mesh.shapekey_left
    del bpy.types.Mesh.shapekey_right
    del bpy.types.WindowManager.AngleSensitivity
    del bpy.types.WindowManager.EnableCamShapeMatic
    
if __name__ == "__main__" or __name__ == "CamShapeMatic":
    register()

$\endgroup$
2
  • 3
    $\begingroup$ I would delete the 4 last lines and put the bpy.types... custom properties definitions in both the register and unregister with del bpy.types.... otherwise I think you'll still have a bunch of custom properties associated with your objects after disabling the addon $\endgroup$
    – Gorgious
    Apr 11, 2022 at 8:29
  • $\begingroup$ @Gorgious thanks for those tips! It makes sense that I should be only defining those bpy.types in the reg and ureg functions. However, by removing those bottom 4 lines of code my addon panel doesn't show up at all after enabling the addon from preferences. Any idea why? $\endgroup$
    – Tyler
    Apr 12, 2022 at 5:16

1 Answer 1

8
+50
$\begingroup$

Your main problem is your bl_info structure. You can read about it in the Blender official wiki Here's a corrected version:

bl_info = {
    "name" : "Cam-Shape-Matic",  
    "category": "3D View",   
    "author": "BeyondDev",
    "blender": (2, 80, 0),
    "version": (0, 0, 1),
    "description": "Drive Shapekeys on Objects based on Relative Camera Angle.",
    "doc_url": "",
    "tracker_url": "",
}
  • You had a field that Blender didn't recognize. I removed it.
  • You didn't have a name field. I added it
  • You had two category fields. I deleted the second one, as the first one is what you wanted.
  • You had 8 as the minor version number in the Blender field. It should be 80. I replaced it.

Having done that, I can now install your add-on using this technique:

  1. Create a directory called Cam-Shape-Matic.
  2. Place the modified code in the directory in a file called __init__.py
  3. Zip the directory into Cam-Sphae-Matic.zip
  4. Use the "install" command of preferencesaddons to install it.

Having done that, if I disable or remove the addon using the Preferences command, the unregister() function is properly called.

I did not debug the addon's code, of course.

The only other change needed is that this line:

if __name__ == "__main__" or __name__ == "CamShapeMatic":

should be

if __name__ == "__main__"

__main__ is a special variable. You can read about how it is used here. Or you can read the Python manual entry for details.

The convention in Blender is to place test code inside this if block, such as a test for registering your classes.

$\endgroup$
2
  • 1
    $\begingroup$ Thank you very much for the thorough answer and the revised code! You fixed even more than necessary, and helped my understanding greatly. Cheers! $\endgroup$
    – Tyler
    Apr 16, 2022 at 17:43
  • 2
    $\begingroup$ @Tyler you're very welcome. I'm sure you'll enjoy writing add-ons now. $\endgroup$ Apr 16, 2022 at 18:46

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .