0
$\begingroup$

My Goal is to have 4 dropdown lists of shapekeys in a 3D view panel, allow the user to select 1 shapekey in each dropdown, and then reference those user-selected shapekeys in scripting that modifies their values based on relative camera position. They should dynamically update anytime the user changes the shapekeys.

I want it to operate like this:

enter image description here

To that end, I'm trying to create a dropdown using an enum: bpy.context.active_object.data.shape_keys.key_blocks.items()

I'd ideally like to place it inside a row.prop() I've defined, but for testing purposes I've tried placing it inside a layout.prop() and nothing populates in the panel. Can someone help me understand what I'm doing wrong?

Here's a look at the raw code: (Disclaimer: I'm a newbie Blender Python programmer, and there is a lot of code below that is redundant and for experimentation only)

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": "",
}
import bpy
import mathutils

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


    shapekey_enum : bpy.props.EnumProperty(
        name ="",
        description= "Select a shapekey",
        items = bpy.context.active_object.data.shape_keys.key_blocks.items()

    )
    
    def draw(self, context):
        layout = self.layout
        layout.prop(self, "shapekey_enum")
        box = layout.box()
        col = box.column(align=True)
        row = col.row(align=True)
        row.label(text='Select Camera Angle Shapekeys')

        scene = context.scene
        row = col.row(align=True)
        row.scale_y = 1.1
        row.prop(scene, "camera")

        obj = bpy.context.active_object
        shape_keys = obj.data.shape_keys
        active_key = obj.active_shape_key
        active_index = obj.active_shape_key_index
        shapeKeys =  bpy.context.active_object.data.shape_keys.key_blocks

        row = col.row(align=True)
        row.scale_y = 1.1
        #dropdown = row.template_list(align=True)
        row.prop(self, "shapekey_enum")#, icon='SHAPEKEY_DATA', text='Test')
        row = col.row(align=True)
        row.scale_y = 1.1
        row.prop(obj, 'active_shape_key', icon='SHAPEKEY_DATA', text='Up')
        row = col.row(align=True)
        row.scale_y = 1.1
        row.prop(obj, 'active_shape_key', icon='SHAPEKEY_DATA', text='Down')
        row = col.row(align=True)
        row.scale_y = 1.1
        row.prop(obj, 'active_shape_key', icon='SHAPEKEY_DATA', text='Left')
        row = col.row(align=True)
        row.scale_y = 1.1
        row.prop(obj, 'active_shape_key', icon='SHAPEKEY_DATA', text='Right')

        col.separator()


def register(): 
    bpy.utils.register_class(CamShapeMaticPanel)
    bpy.utils.register_class(DriveShapekey)
    print('CamShapeMatic Addon Loaded')
def unregister(): 
    bpy.utils.unregister_class(DriveShapekey)
    bpy.utils.unregister_class(CamShapeMaticPanel)
#if __name__ == "__main__": 
#    register()
register()
$\endgroup$
7
  • $\begingroup$ I don't think panels can have props. What is your goal with shapekey_enum? Is there one for the whole scene, or is there one per mesh? $\endgroup$
    – scurest
    Commented Apr 4, 2022 at 5:04
  • $\begingroup$ This sort of thing is most easily done with a UIList. See this tutorial for an introduction. $\endgroup$ Commented Apr 4, 2022 at 15:11
  • $\begingroup$ @scurest my goal is to list all the shapekeys for the active object, allow a user to select one from the list, and then use that selection to do other things via Python. But interestingly enough my panel does show props for bpy.context.active_object.active_shape_key. $\endgroup$
    – Tyler
    Commented Apr 4, 2022 at 15:46
  • $\begingroup$ @MartyFouts thanks for sharing that tutorial. I have seen some tutorials on UILists and I don't want the user to be able to add or delete shapekeys via my panel. I simply want to list the existing shapekeys, allow the user to select one, and then do something in Python based on that selection. $\endgroup$
    – Tyler
    Commented Apr 4, 2022 at 15:48
  • 2
    $\begingroup$ @Tyler If you don't want to modify (add/remove/re-order) the shape keys you really don't need to use an enum property at all since you can list all the keys of a given object obj_key = [k for k in bpy.data.shape_keys if k.user.name == obj.name][0] furthermore if you use an enum property you'll still want to use a dynamic enum with several potential updates in the event someone modifies the keys outside your panel. I think a more robust description of your overall goal may be useful for people to offer aid. $\endgroup$
    – Ratt
    Commented Apr 4, 2022 at 16:59

1 Answer 1

1
$\begingroup$

I'll tell you how to do it with an EnumProperty without commenting on whether that's the best way.

The first thing to decide when you create a prop is where it's stored. You can't store it on a Panel, but if you could, you would only have one of them. So what would happen if you switch the active object and the new object has different shape keys? To avoid this, let's store your enum on a mesh so every mesh has its own, like mesh.my_shapekey.

You create the enum items with items = bpy.context.active_object.data.shape_keys.key_blocks.items(). There are two problems:

  • This is not the right format. The docs say it expects a "sequence of enum items formatted: [(identifier, name, description, icon, number), ...]" (with only the first 3 being necessary).
  • This code is only run once when the EnumProperty is first created, so it only uses the active_object at that time. What you want is to always use the current active object. For that you need a dynamic EnumProperty.

From the docs: "For dynamic values a callback can be passed which returns a list in the same format as the static list. This function must take 2 arguments (self, context), context may be None."

So let's begin by writing that callback:

# 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 = [
            (f'KB{i}', kb.name, '')
            for i, kb in enumerate(self.shape_keys.key_blocks)
        ]

    return intern_enum_items_strings(items)

To work around the "known bug" mentioned in the EnumProperty docs, you need to call the following function on the enum items. It ensures a reference to the strings are kept alive on the Python side. See this Q for more. Feel free to ignore this, just know you need it when you use a dynamic EnumProperty.

# To workaround the "known bug with using a callback" mentioned
# in the EnumProperty docs, the function needs to be called on
# the 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
    ]

Okay, now create your EnumProperty.

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

And finally create a panel to show it

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

    def draw(self, context):
        layout = self.layout

        # draw my_shapekey for the current object
        # only if it's a mesh with shapekeys though
        if (
            context and
            context.object and
            context.object.type == 'MESH' and
            context.object.data.shape_keys
        ):
            layout.prop(context.object.data, "my_shapekey")


bpy.utils.register_class(CamShapeMaticPanel)

It's important to note that the enum value can go "out of sync" with the actual key blocks. For example, if you delete or add a keyblock, the enum value does not change. So when you actually use the enum value for something keep that in mind.

$\endgroup$
8
  • $\begingroup$ Worked like a charm!!! Thank you very much for the thorough answer. It really shed light on how blender python is supposed to work. Especially how to look for limitations within the documentation, and that I need to watch out for things like different enum formats. Really really exceptional. Bravo! $\endgroup$
    – Tyler
    Commented Apr 5, 2022 at 4:47
  • $\begingroup$ @Ratt X and Y and Z stops at the first false. $\endgroup$
    – scurest
    Commented Apr 5, 2022 at 6:25
  • $\begingroup$ @Ratt How would there be an error there? It's already checked that context.object.data exists and is a Mesh. $\endgroup$
    – scurest
    Commented Apr 5, 2022 at 6:35
  • $\begingroup$ @scurest sorry after starting from fresh file and re-copying script only warning shown was panel naming convention. I'll remove comments as the errors I was seeing were not based on your coding must have been something I was adjusting. $\endgroup$
    – Ratt
    Commented Apr 5, 2022 at 7:04
  • $\begingroup$ Is there a recommended way to save these shapekeys to a variable rather than to each mesh? $\endgroup$
    – Tyler
    Commented Apr 5, 2022 at 8:02

You must log in to answer this question.

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