3

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()

Tyler
  • 586
  • 6
  • 20
  • 3
    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 – Gorgious Apr 11 '22 at 08:29
  • @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? – Tyler Apr 12 '22 at 05:16

1 Answers1

8

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.

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