Reorder bpy_prop_collection (data.shape_keys.key_blocks)

I’m working on a script that messes with shape keys. At some point I’m transferring a key (making it the last by default) and moving it up to a pre-determined index position in the shape keys list.

The script works beautifully except that when reordering the shape keys, done through bpy.ops.object.shape_key_move(). It’s not only an operator but one of the slow ones, making Blender take its sweet time while it shuffles the key around.

Can this be done more efficiently? I’m already checking whether the target index is closer to the top of the list and sending the key straight there when going down is faster. I tried to figure out ways to alter the indexes of the keys collection directly but couldn’t think of anything that worked.

It seems like there is no other way https://blender.stackexchange.com/a/91741/21828

If you want to reorder them just for visual convenience, you can name them in the desired order and enable sorting https://i.stack.imgur.com/18wWP.jpg

Or maybe instead of moving them swap their data and names

For example

import bpy
import numpy as np
context = bpy.context
ob = context.object

def move_shape_key(ob, key_name, index):
    me = ob.data
    verts = me.vertices
    c = len(verts)

    key1 = me.shape_keys.key_blocks[key_name]
    key2 = me.shape_keys.key_blocks[index]

    key1_data = np.zeros(c*3, dtype=np.float32)
    key1.data.foreach_get("co", key1_data.ravel())

    key2_data = np.zeros(c*3, dtype=np.float32)
    key2.data.foreach_get("co", key2_data.ravel())

    key1.data.foreach_set("co", key2_data)
    key2.data.foreach_set("co", key1_data)

    init_key1_name = key1.name
    init_key2_name = key2.name
    key2.name = "_temp_name"
    key1.name = init_key2_name
    key2.name = init_key1_name
    
    
move_shape_key(ob, "Key 1", 2)
1 Like

Ohh, swapping the data, of course! @Cirno, you’re a genius! Thank you!

One of the things my script (and future stinky but freebie addon) does is duplicate keys. The way you’re handling the data is way more elegant than what I was doing since I’m not an actual coder, so I think I’ll steal this for the duplicate function as well. :smile:

Here’s what I did with the data swap code (besides uglifying it!): Noodled a snippet to copy properties (values, mute key etc) too, sneaked into the duplicate function and used to position dup keys before/after their sources. I’m halfway writing the functions to copy drivers and actions for the sake of completeness, but the code is still messy so I left it out here.

import bpy
import numpy as np

c = bpy.context
ob = c.object
me = ob.data
skeys = me.shape_keys

### Shape Key functions
def shape_key_swap_vertices(key_name, index, shape_keys=skeys, me=me):
    """Swap vertices and properties between key1 and key2"""
    verts = me.vertices
    c = len(verts)    

    key1 = skeys.key_blocks[key_name]
    key2 = skeys.key_blocks[index]
    
    key1_data = np.zeros(c*3, dtype=np.float32)
    key1.data.foreach_get("co", key1_data.ravel())

    key2_data = np.zeros(c*3, dtype=np.float32)
    key2.data.foreach_get("co", key2_data.ravel())

    key1.data.foreach_set("co", key2_data)
    key2.data.foreach_set("co", key1_data)

    shape_key_clone_props(key1, key2, swap=True)

    # TODO: Drivers, animation support

def shape_key_clone_props(source_key, target_key, swap=False):
    """Copy shape source_key proprieties to target_key, optitionally swapping between them"""
    if swap:
        key_temp = {
            "source_name" : source_key.name,
            "name" : target_key.name,
            "value" : target_key.value,
            "mute" : target_key.mute,
            "vertex_group" : target_key.vertex_group,
            "relative_key" : target_key.relative_key,
            "slider_min" : target_key.slider_min,
            "slider_max" : target_key.slider_max,
        }

    target_key.name = source_key.name
    target_key.value = source_key.value
    target_key.mute = source_key.mute
    target_key.vertex_group = source_key.vertex_group
    target_key.relative_key = source_key.relative_key
    target_key.slider_min = source_key.slider_min
    target_key.slider_max = source_key.slider_max

    if swap:
        source_key.name = key_temp["name"]
        source_key.value = key_temp["value"]
        source_key.mute = key_temp["mute"]
        source_key.vertex_group = key_temp["vertex_group"]
        source_key.relative_key = key_temp["relative_key"]
        source_key.slider_min = key_temp["slider_min"]
        source_key.slider_max = key_temp["slider_max"]
        target_key.name = key_temp["source_name"]

def shape_key_duplicate(key_name, object=ob, shape_keys=skeys, me=me):
    """Copy vertices and properties from source_key to target_key"""
    verts = me.vertices
    c = len(verts)
    
    bpy.data.objects[object.name].shape_key_add(name=key_name, from_mix=False) # TODO: Preferences option for naming convention

    source_key = skeys.key_blocks[key_name]
    source_key_idx = skeys.key_blocks.keys().index(key_name)
    target_key = skeys.key_blocks[-1]
    
    # Clone vertex coordinates
    source_key_data = np.zeros(c*3, dtype=np.float32)
    source_key.data.foreach_get("co", source_key_data.ravel())
    target_key.data.foreach_set("co", source_key_data)
    
    # Clone attributes
    shape_key_clone_props(source_key, target_key)    
    shape_key_reorder(source_key_idx, target_key=target_key, after=True) # TODO: Preferences option for reordering
    
    # TODO: Drivers, animation support

def shape_key_reorder(destination_idx, object=ob, shape_keys=skeys, target_key=ob.active_shape_key, after=False):
    """Reorder Shape Key list, positioning target key at or after destination_idx"""
    target_key_name = target_key.name
    target_key_idx = skeys.key_blocks.keys().index(target_key_name)

    travel = destination_idx
    if after:
        travel += 1
    activate = travel

    for key in range(destination_idx, target_key_idx):
        new_target_name = skeys.key_blocks[target_key_idx].name
        shape_key_swap_vertices(new_target_name, travel)
        travel += 1
    object.active_shape_key_index = activate

### RUN!
target_key = ob.active_shape_key
shape_key_duplicate(target_key.name, ob, skeys)
1 Like