import bpy from mathutils import kdtree bl_info = { "name": "FVNE-Expression-Manager", "author": "Blacky", "version": (1, 0), "blender": (3, 3, 0), "location": "Object > Mirror Shape Keys", "description": "Add,Split,Combine and Mirror emotes for FVNE", "category": "Object" } # FVNE Shape Key Preset Groups EYE_PRESETS = [ "ue_Eye/Wide", "ue_Eye/Squint", "ue_Eye/Blink", "ue_Eye/CloseHappy", "ue_Eye/CloseHard", ] BROW_PRESETS = [ "ue_Brow/Up", "ue_Brow/Frown", "ue_Brow/Anger", "ue_Brow/Happy", "ue_Brow/Relieve", "ue_Brow/Sad", ] MOUTH_EMOTE_PRESETS = [ "ue_Mouth/Emote/Smile", "ue_Mouth/Emote/Grin", "ue_Mouth/Emote/Anger", "ue_Mouth/Emote/Fear", "ue_Mouth/Emote/Sad", "ue_Mouth/Emote/LipBite", "ue_Mouth/Emote/OOOH", "ue_Mouth/Emote/Open", "ue_Mouth/Emote/Close", ] MOUTH_VISEME_PRESETS = [ "ue_Mouth/Visemes/Aa", "ue_Mouth/Visemes/Ch", "ue_Mouth/Visemes/Dd", "ue_Mouth/Visemes/Ee", "ue_Mouth/Visemes/Ff", "ue_Mouth/Visemes/Ih", "ue_Mouth/Visemes/Kk", "ue_Mouth/Visemes/Nn", "ue_Mouth/Visemes/Oh", "ue_Mouth/Visemes/Ou", "ue_Mouth/Visemes/Pp", ] FACE_PRESETS = [ "ue_Face/Happy", "ue_Face/Relieved", "ue_Face/Surprised", "ue_Face/Serious", "ue_Face/Sad", "ue_Face/Fearful", "ue_Face/Angry", "ue_Face/Painful", ] BODY_PRESETS = [ "ue_Body/ChestBreathing", "ue_Body/BellyBreathing", "ue_Body/CheekBloat", ] FULL_PRESET_SET = ( EYE_PRESETS + BROW_PRESETS + MOUTH_EMOTE_PRESETS + MOUTH_VISEME_PRESETS + FACE_PRESETS + BODY_PRESETS ) # --- Utility Functions --- def find_basis_key(key_blocks): for key in key_blocks: if key.name.lower() == "basis": return key return None import re def get_side_from_name(name): if re.search(r'_L\b', name): return 'L' elif re.search(r'_R\b', name): return 'R' return None def ensure_r_above_l(obj, r_name, l_name): key_blocks = obj.data.shape_keys.key_blocks r_index = key_blocks.find(r_name) l_index = key_blocks.find(l_name) while r_index > l_index: obj.active_shape_key_index = r_index bpy.ops.object.shape_key_move(type='UP') r_index -= 1 l_index = key_blocks.find(l_name) import bpy # Function to add "_R" to shape keys that don't already have "_L" or "_R" def add_R_suffix_to_shape_keys(): obj = bpy.context.object if not obj or not obj.data.shape_keys: return key_blocks = obj.data.shape_keys.key_blocks existing = {kb.name for kb in key_blocks} for key in list(key_blocks): name = key.name # only operate on ue_ keys, and skip any already suffixed if "ue_" not in name or name.endswith(("_L", "_R")): continue target = f"{name}_R" # skip if the exact _R version already exists if target in existing: continue key.name = target existing.add(target) # Function to add "_L" to shape keys that don't already have "_L" or "_R" def add_L_suffix_to_shape_keys(): obj = bpy.context.object if not obj or not obj.data.shape_keys: return key_blocks = obj.data.shape_keys.key_blocks existing = {kb.name for kb in key_blocks} for key in list(key_blocks): name = key.name # only operate on ue_ keys, and skip any already suffixed if "ue_" not in name or name.endswith(("_L", "_R")): continue target = f"{name}_L" # skip if the exact _L version already exists if target in existing: continue key.name = target existing.add(target) def sort_shape_keys_smart(obj): key_blocks = obj.data.shape_keys.key_blocks all_names = [kb.name for kb in key_blocks] # 1. Fixed top block (do not move) fixed_order = ["basis", "Shape", "i_flatchested", "i_breasts", "i_belly", "i_ass_hips"] fixed_present = [n for n in fixed_order if n in all_names] # 2. Priority ordered keys (Eyes, Brow, Mouth, Face, Body) priority_order = [ # Eyes "ue_Eye/Wide", "ue_Eye/Squint", "ue_Eye/Blink", "ue_Eye/CloseHappy", "ue_Eye/CloseHard", # Brows "ue_Brow/Up", "ue_Brow/Frown", "ue_Brow/Anger", "ue_Brow/Happy", "ue_Brow/Relieve", "ue_Brow/Sad", # Mouth Emotes "ue_Mouth/Emote/Smile", "ue_Mouth/Emote/Grin", "ue_Mouth/Emote/Anger", "ue_Mouth/Emote/Fear", "ue_Mouth/Emote/Sad", "ue_Mouth/Emote/LipBite", "ue_Mouth/Emote/OOOH", "ue_Mouth/Emote/Open", "ue_Mouth/Emote/Close", # Visemes "ue_Mouth/Visemes/Aa", "ue_Mouth/Visemes/Ch", "ue_Mouth/Visemes/Dd", "ue_Mouth/Visemes/Ee", "ue_Mouth/Visemes/Ff", "ue_Mouth/Visemes/Ih", "ue_Mouth/Visemes/Kk", "ue_Mouth/Visemes/Nn", "ue_Mouth/Visemes/Oh", "ue_Mouth/Visemes/Ou", "ue_Mouth/Visemes/Pp", # Face Expressions "ue_Face/Happy", "ue_Face/Relieved", "ue_Face/Surprised", "ue_Face/Serious", "ue_Face/Sad", "ue_Face/Fearful", "ue_Face/Angry", "ue_Face/Painful", # Body "ue_Body/ChestBreathing", "ue_Body/BellyBreathing", "ue_Body/CheekBloat", ] priority_map = {n.lower(): i for i, n in enumerate(priority_order)} # 3. Categorizer for sub-buckets def get_subcat(nl): if nl.startswith("ue_eye/"): return 0 if nl.startswith("ue_brow/"): return 1 if nl.startswith("ue_mouth/emote/"): return 2 if nl.startswith("ue_Mouth/Visemes/"): return 3 if nl.startswith("ue_face/"): return 4 if nl.startswith("ue_body/"): return 5 return 6 # unknown bucket def sort_key(kb): nl = kb.name.lower() # strip _L/_R suffix if nl.endswith("_l") or nl.endswith("_r"): base = kb.name[:-2] suffix = 1 if nl.endswith("_r") else 2 else: base = kb.name suffix = 0 bl = base.lower() subcat = get_subcat(bl) # Priority check if bl in priority_map: rank = (0, priority_map[bl]) else: rank = (1, bl) return (subcat, rank, suffix) # 4. Filter everything not in fixed order rest = [n for n in all_names if n not in fixed_present] rest_kb = [key_blocks[n] for n in rest] sorted_rest = sorted(rest_kb, key=sort_key) sorted_rest_names = [kb.name for kb in sorted_rest] # 5. Final target order full_order = fixed_present + sorted_rest_names # 6. Move into place for target_idx, name in enumerate(full_order): cur_idx = key_blocks.find(name) while cur_idx > target_idx: obj.active_shape_key_index = cur_idx bpy.ops.object.shape_key_move(type='UP') cur_idx -= 1 while cur_idx < target_idx: obj.active_shape_key_index = cur_idx bpy.ops.object.shape_key_move(type='DOWN') cur_idx += 1 # --- Core Functionality --- def create_tree(vertices): tree = kdtree.KDTree(len(vertices)) for i, vertex in enumerate(vertices): tree.insert(vertex.co.copy(), i) tree.balance() return tree def mirror_shape(shape, base_verts, tree, direction, is_exclusive=False): shape_verts = shape.data for i, base_vert in enumerate(base_verts): base_co = base_vert.co.copy() shape_co = shape_verts[i].co.copy() is_left = base_co.x < -1e-7 is_right = base_co.x > 1e-7 is_center = not is_left and not is_right if is_center: shape_co.x = 0.0 shape_verts[i].co = shape_co elif (direction == 'RL' and is_left) or (direction == 'LR' and is_right): mirrored_co = shape_co.copy() mirrored_co.x = -mirrored_co.x mirrored_Wide = base_co.copy() mirrored_Wide.x = -mirrored_Wide.x _, target_index, _ = tree.find(mirrored_Wide) shape_verts[target_index].co = mirrored_co if is_exclusive: shape_verts[i].co = base_verts[i].co.copy() elif is_exclusive: shape_verts[i].co = base_verts[i].co.copy() def mirror_selected_shape_keys_vertex(direction): obj = bpy.context.active_object if not obj or not obj.data.shape_keys: print("No shape keys found.") return active_index = obj.active_shape_key_index if active_index == 0: print("Cannot mirror basis shape key.") return key_blocks = obj.data.shape_keys.key_blocks shape = key_blocks[active_index] basis_verts = key_blocks[0].data tree = create_tree(basis_verts) mirror_shape(shape, basis_verts, tree, direction, is_exclusive=False) def smart_mirror_shape_keys_by_suffix(): obj = bpy.context.active_object if not obj or not obj.data.shape_keys: print("No shape keys found.") return key_blocks = obj.data.shape_keys.key_blocks existing_keys = {k.name for k in key_blocks} keys_to_process = [k.name for k in key_blocks[1:]] for name in keys_to_process: if name.endswith('_L'): suffix_from, suffix_to = '_L', '_R' elif name.endswith('_R'): suffix_from, suffix_to = '_R', '_L' else: continue mirrored_name = name[:-2] + suffix_to if mirrored_name in existing_keys: continue print(f"Mirroring {name} → {mirrored_name}") original_index = key_blocks.find(name) obj.active_shape_key_index = original_index bpy.ops.object.shape_key_add(from_mix=False) new_key = obj.active_shape_key new_key.name = mirrored_name for i, vert in enumerate(key_blocks[name].data): new_key.data[i].co = vert.co.copy() bpy.ops.object.shape_key_mirror(use_topology=False) if suffix_to == '_R': ensure_r_above_l(obj, mirrored_name, name) else: ensure_r_above_l(obj, name, mirrored_name) import bpy def find_basis_key(key_blocks): for kb in key_blocks: if kb.name.lower() == "basis": return kb return None def create_combined_symmetric_shape_keys(): obj = bpy.context.active_object if obj is None or obj.type != 'MESH' or obj.data.shape_keys is None: return key_blocks = obj.data.shape_keys.key_blocks key_names = [k.name for k in key_blocks] left_keys = [k for k in key_names if k.endswith("_L")] basis = find_basis_key(key_blocks) if not basis: print("Basis key not found!") return for left_key in left_keys: base_name = left_key[:-2] # Remove "_L" right_key = base_name + "_R" # Restrict to only shape keys starting with "ue_" if not base_name.startswith("ue_"): print(f"Skipping {base_name}: does not start with 'ue_'") continue if right_key not in key_names: print(f"Skipping {base_name}: missing right key") continue if base_name in key_names: print(f"Skipping {base_name}: combined shape key already exists") continue # Create new combined shape key obj.active_shape_key_index = key_names.index(left_key) bpy.ops.object.shape_key_add(from_mix=False) combined_key = obj.active_shape_key combined_key.name = base_name left_data = key_blocks[left_key].data right_data = key_blocks[right_key].data combined_data = combined_key.data basis_data = basis.data for i in range(len(combined_data)): base = basis_data[i].co delta_l = left_data[i].co - base delta_r = right_data[i].co - base combined_data[i].co = base + delta_l + delta_r print(f"Created symmetric shape key: {base_name}") import bpy import mathutils def split_symmetric_shape_keys(): obj = bpy.context.object if not obj or not obj.data.shape_keys: return key_blocks = obj.data.shape_keys.key_blocks basis = find_basis_key(key_blocks) if not basis: return symmetric_keys = [ kb for kb in key_blocks if kb.name.lower() != "basis" and not kb.name.endswith(("_L", "_R")) and kb.name.startswith("ue_") ] for key in symmetric_keys: name = key.name verts = obj.data.vertices has_L = f"{name}_L" in key_blocks has_R = f"{name}_R" in key_blocks if has_L and has_R: continue key_L = key_blocks.get(f"{name}_L") or obj.shape_key_add(name=f"{name}_L", from_mix=False) key_R = key_blocks.get(f"{name}_R") or obj.shape_key_add(name=f"{name}_R", from_mix=False) for i, v in enumerate(verts): base_co = basis.data[i].co shape_co = key.data[i].co delta = shape_co - base_co if base_co.x > 0: key_L.data[i].co = shape_co key_R.data[i].co = base_co elif base_co.x < 0: key_R.data[i].co = shape_co key_L.data[i].co = base_co else: # Optional: handle center-line verts symmetrically mid = base_co + delta * 0.5 key_L.data[i].co = mid key_R.data[i].co = mid print(f"Split “{name}” → “{name}_L” + “{name}_R”") import bpy import mathutils def find_basis_key(key_blocks): for kb in key_blocks: if kb.name.lower() == "basis": return kb return None import bpy import mathutils def find_basis_key(key_blocks): for kb in key_blocks: if kb.name.lower() == "basis": return kb return None def split_active_symmetric_shape_key(): obj = bpy.context.active_object if not obj or obj.type != 'MESH': return keys = obj.data.shape_keys if not keys: return key_blocks = keys.key_blocks basis = find_basis_key(key_blocks) if not basis: print("No Basis key found.") return idx = obj.active_shape_key_index if idx is None or idx == 0: print("No active non‑Basis key.") return active = key_blocks[idx] name = active.name nl = name.lower() if not nl.startswith("ue_"): print(f"Skipping '{name}' (must start with 'ue_').") return if nl.endswith("_l") or nl.endswith("_r"): print(f"Skipping '{name}' (already _L/_R).") return l_name = f"{name}_L" r_name = f"{name}_R" has_L = l_name in key_blocks has_R = r_name in key_blocks if has_L and has_R: print(f"Both '{l_name}' and '{r_name}' already exist.") return key_L = key_blocks[l_name] if has_L else obj.shape_key_add(name=l_name, from_mix=False) key_R = key_blocks[r_name] if has_R else obj.shape_key_add(name=r_name, from_mix=False) verts = obj.data.vertices for i, v in enumerate(verts): base_co = basis.data[i].co key_co = active.data[i].co delta = key_co - base_co if delta.length_squared < 1e-12: continue # +X (right side of mesh) → character's LEFT (_L) if base_co.x > 0: key_L.data[i].co = base_co + delta # -X → character's RIGHT (_R) elif base_co.x < 0: key_R.data[i].co = base_co + delta print(f"Split '{name}' → '{l_name}' + '{r_name}'") def apply_shape_key(obj, shape_key_name): if shape_key_name not in obj.data.shape_keys.key_blocks: obj.shape_key_add(name=shape_key_name, from_mix=False) def add_shape_keys(obj, key_list): for key_name in key_list: apply_shape_key(obj, key_name) # --- Operators --- class FVNE_OT_smart_mirror_keys(bpy.types.Operator): bl_idname = "object.fvne_smart_mirror_keys" bl_label = "Create R / L Counterparts" bl_description = "Auto-creates the counterpart of already present _R or_L ue shape keys." bl_options = {'REGISTER', 'UNDO'} def execute(self, context): smart_mirror_shape_keys_by_suffix() return {'FINISHED'} class FVNE_OT_mirror_vertex_lr(bpy.types.Operator): bl_idname = "object.fvne_mirror_vertex_lr" bl_label = "L → R" bl_description = "Mirror selected shape key from Left to Right using vertex index" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): mirror_selected_shape_keys_vertex(direction='LR') return {'FINISHED'} class FVNE_OT_mirror_vertex_rl(bpy.types.Operator): bl_idname = "object.fvne_mirror_vertex_rl" bl_label = "R → L" bl_description = "Mirror selected shape key from Right to Left using vertex index" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): mirror_selected_shape_keys_vertex(direction='RL') return {'FINISHED'} class FVNE_OT_split_symmetric_keys(bpy.types.Operator): bl_idname = "object.fvne_split_symmetric_keys" bl_label = "Split All Main Keys" bl_description = "Creates _R and _L shape key versions based on all present ue_ shapekeys (X-Axis)" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): split_symmetric_shape_keys() return {'FINISHED'} class FVNE_OT_split_active_symmetric_key(bpy.types.Operator): bl_idname = "object.fvne_split_active_symmetric_key" bl_label = "Split Active Key" bl_description = "Creates _R and _L shape key versions only for the currently selected ue_ shapekey (X-Axis)" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): split_active_symmetric_shape_key() return {'FINISHED'} class FVNE_OT_combine_symmetric_keys(bpy.types.Operator): bl_idname = "object.fvne_combine_symmetric_keys" bl_label = "Combine _R & _L to Main Key" bl_description = "Combine the values of _R and _L shape keys into one new unified key" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): create_combined_symmetric_shape_keys() return {'FINISHED'} import bpy # 1) PropertyGroup for each shape key entry class FVNE_ShapeKeyItem(bpy.types.PropertyGroup): name: bpy.props.StringProperty(name="Shape Key Name") select: bpy.props.BoolProperty(name="Include") # 2) UIList to display that collection import bpy class FVNE_UL_shape_key_list(bpy.types.UIList): """Show full shape-key names left-aligned in our combine dialog.""" def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): # data is the Operator, item is one list item type (with .name, .select) split = layout.split(factor=0.05, align=True) # draw the checkbox: split.prop(item, "select", text="") # draw the full name left-aligned: split.label(text=item.name) # 3) The “combine selected” operator class FVNE_OT_combine_selected_shape_keys(bpy.types.Operator): bl_idname = "object.fvne_combine_selected_shape_keys" bl_label = "Combine Selected Shape Keys" bl_description = "Bake a new key from the mix of selected shape keys" bl_options = {'REGISTER','UNDO'} keys_list: bpy.props.CollectionProperty(type=FVNE_ShapeKeyItem) keys_index: bpy.props.IntProperty() new_name: bpy.props.StringProperty(name="New Key Name", default="Combined") @classmethod def poll(cls, context): return context.object and context.object.type == 'MESH' and context.object.data.shape_keys def invoke(self, context, event): self.keys_list.clear() for kb in context.object.data.shape_keys.key_blocks: if kb.name.lower() == "basis": continue item = self.keys_list.add() item.name = kb.name item.select = False return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context): layout = self.layout layout.label(text="Select shape keys to combine:") layout.template_list( "FVNE_UL_shape_key_list", # our new UIList "", # list_id self, "keys_list", # data, collection self, "keys_index", # data, active_prop rows=6 ) layout.separator() layout.prop(self, "new_name") def execute(self, context): obj = context.object kbs = obj.data.shape_keys.key_blocks name = self.new_name.strip() if not name: self.report({'ERROR'}, "New key needs a name.") return {'CANCELLED'} if kbs.get(name): self.report({'WARNING'}, f"Shape key '{name}' already exists.") return {'CANCELLED'} to_bake = [item.name for item in self.keys_list if item.select] if not to_bake: self.report({'WARNING'}, "No shape keys selected.") return {'CANCELLED'} old_vals = {kb.name: kb.value for kb in kbs} for kb in kbs: kb.value = 1.0 if kb.name in to_bake else 0.0 bpy.ops.object.shape_key_add(from_mix=True) new_kb = obj.active_shape_key new_kb.name = name for nm, val in old_vals.items(): kbs[nm].value = val self.report({'INFO'}, f"Created '{name}' from {len(to_bake)} keys.") return {'FINISHED'} class FVNE_OT_mirror_selected_rl(bpy.types.Operator): bl_idname = "object.fvne_mirror_rl" bl_label = "Mirror Selected (Right → Left)" bl_description = "Mirror selected shape key from Right to Left" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): mirror_selected_shape_keys(direction='RL') return {'FINISHED'} class FVNE_OT_mirror_selected_lr(bpy.types.Operator): bl_idname = "object.fvne_mirror_lr" bl_label = "Mirror Selected (Left → Right)" bl_description = "Mirror selected shape key from Left to Right" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): mirror_selected_shape_keys(direction='LR') return {'FINISHED'} class FVNE_OT_add_eye_keys(bpy.types.Operator): bl_idname = "object.fvne_add_eye_keys" bl_label = "Add Eye Keys" bl_description = "Adds preset user expressions for the eyes." bl_options = {'REGISTER', 'UNDO'} def execute(self, context): add_shape_keys(context.object, EYE_PRESETS) return {'FINISHED'} class FVNE_OT_add_brow_keys(bpy.types.Operator): bl_idname = "object.fvne_add_brow_keys" bl_label = "Add Brow Keys" bl_description = "Adds preset user expressions for the eyebrows." bl_options = {'REGISTER', 'UNDO'} def execute(self, context): add_shape_keys(context.object, BROW_PRESETS) return {'FINISHED'} class FVNE_OT_add_emote_keys(bpy.types.Operator): bl_idname = "object.fvne_add_emote_keys" bl_label = "Add Mouth Emote Keys" bl_description = "Adds preset user expressions for mouth emotes." bl_options = {'REGISTER', 'UNDO'} def execute(self, context): add_shape_keys(context.object, MOUTH_EMOTE_PRESETS) return {'FINISHED'} class FVNE_OT_add_viseme_keys(bpy.types.Operator): bl_idname = "object.fvne_add_viseme_keys" bl_label = "Add Mouth Viseme Keys" bl_description = "Adds preset user expressions for mouth visemes." bl_options = {'REGISTER', 'UNDO'} def execute(self, context): add_shape_keys(context.object, MOUTH_VISEME_PRESETS) return {'FINISHED'} class FVNE_OT_add_face_keys(bpy.types.Operator): bl_idname = "object.fvne_add_face_keys" bl_label = "Add Face Keys" bl_description = "Adds preset user expressions for the face." bl_options = {'REGISTER', 'UNDO'} def execute(self, context): add_shape_keys(context.object, FACE_PRESETS) return {'FINISHED'} class FVNE_OT_add_body_keys(bpy.types.Operator): bl_idname = "object.fvne_add_body_keys" bl_label = "Add Body Keys" bl_description = "Adds preset user expressions for the body." bl_options = {'REGISTER', 'UNDO'} def execute(self, context): add_shape_keys(context.object, BODY_PRESETS) return {'FINISHED'} class FVNE_OT_add_full_keyset(bpy.types.Operator): bl_idname = "object.fvne_add_full_shape_keys" bl_label = "Add Full Set" bl_description = "Adds all preset user expressions." bl_options = {'REGISTER', 'UNDO'} def execute(self, context): add_shape_keys(context.object, FULL_PRESET_SET) return {'FINISHED'} class FVNE_OT_sort_shape_keys(bpy.types.Operator): bl_idname = "object.fvne_sort_keys" bl_label = "Sort Shape Keys" bl_description = "Sort shape keys in preferred order" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): sort_shape_keys_smart(context.active_object) return {'FINISHED'} class AddRSuffixOperator(bpy.types.Operator): bl_idname = "object.add_r_suffix" bl_label = "Add _R Suffix to Shape Keys" bl_description = "Adds the _R suffix to all present shape keys." bl_options = {'REGISTER', 'UNDO'} def execute(self, context): add_R_suffix_to_shape_keys() return {'FINISHED'} class AddLSuffixOperator(bpy.types.Operator): bl_idname = "object.add_l_suffix" bl_label = "Add _L Suffix to Shape Keys" bl_description = "Adds the _L suffix to all present shape keys." bl_options = {'REGISTER', 'UNDO'} def execute(self, context): add_L_suffix_to_shape_keys() return {'FINISHED'} # --- UI Panel --- class FVNE_PT_expression_tools(bpy.types.Panel): bl_label = "FVNE Expression Manager" bl_idname = "OBJECT_PT_fvne_expression" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'FVNE' def draw(self, context): layout = self.layout scene = context.scene # Presets Section box = layout.box() row = box.row() row.prop(scene, "fvne_show_presets", icon="TRIA_DOWN" if scene.fvne_show_presets else "TRIA_RIGHT", emboss=False, text="Add Presets") if scene.fvne_show_presets: col = box.column(align=True) row = col.row(align=True) row.operator("object.fvne_add_eye_keys", text="Eyes") row.operator("object.fvne_add_brow_keys", text="Brows") row = col.row(align=True) row.operator("object.fvne_add_emote_keys", text="Mouth Emotes") row.operator("object.fvne_add_viseme_keys", text="Mouth Visemes") row = col.row(align=True) row.operator("object.fvne_add_face_keys", text="Face") row.operator("object.fvne_add_body_keys", text="Body") col.separator() col.operator("object.fvne_add_full_shape_keys", text="Add Full Set") # Smart Operations Section box = layout.box() row = box.row() row.prop(scene, "fvne_show_smart_ops", icon="TRIA_DOWN" if scene.fvne_show_smart_ops else "TRIA_RIGHT", emboss=False, text="Smart Operations") if scene.fvne_show_smart_ops: col = box.column(align=True) col.operator("object.fvne_split_symmetric_keys", text="Split All Main Keys") col.operator("object.fvne_split_active_symmetric_key", text="Split Only Active Key") col.separator() col.operator("object.fvne_smart_mirror_keys", text="Create R/L Counterparts") col.operator("object.fvne_combine_symmetric_keys", text="Combine _R &_L to Main Key") col.separator() col.operator("object.fvne_combine_selected_shape_keys", text="Combine Selected Keys") # Utilities Section box = layout.box() row = box.row() row.prop(scene, "fvne_show_utilities", icon="TRIA_DOWN" if scene.fvne_show_utilities else "TRIA_RIGHT", emboss=False, text="Utilities") if scene.fvne_show_utilities: col = box.column(align=True) col.operator("object.fvne_sort_keys", text="Sort Shape Keys") col.separator() col.operator("object.add_r_suffix", text="Add _R Suffix to Shape Keys") col.operator("object.add_l_suffix", text="Add _L Suffix to Shape Keys") # Vertex Mirror Section box = layout.box() row = box.row() row.prop(scene, "fvne_show_vertex_mirror", icon="TRIA_DOWN" if scene.fvne_show_vertex_mirror else "TRIA_RIGHT", emboss=False, text="Mirror Shape Key(Vertex)") if scene.fvne_show_vertex_mirror: col = box.column(align=True) col.operator("object.fvne_mirror_vertex_lr", text="Right → Left") col.operator("object.fvne_mirror_vertex_rl", text="Left → Right") # --- Registration --- classes = [ FVNE_ShapeKeyItem, FVNE_UL_shape_key_list, FVNE_OT_combine_selected_shape_keys, FVNE_OT_smart_mirror_keys, FVNE_OT_split_symmetric_keys, FVNE_OT_split_active_symmetric_key, FVNE_OT_mirror_vertex_lr, FVNE_OT_mirror_vertex_rl, FVNE_OT_combine_symmetric_keys, FVNE_OT_sort_shape_keys, FVNE_OT_add_eye_keys, FVNE_OT_add_brow_keys, FVNE_OT_add_emote_keys, FVNE_OT_add_viseme_keys, FVNE_OT_add_face_keys, FVNE_OT_add_body_keys, FVNE_OT_add_full_keyset, AddLSuffixOperator, AddRSuffixOperator, FVNE_PT_expression_tools, ] def register(): for cls in classes: bpy.utils.register_class(cls) # Panel section toggles bpy.types.Scene.fvne_show_vertex_mirror = bpy.props.BoolProperty( name="Mirror Vertex", default=True) bpy.types.Scene.fvne_show_smart_ops = bpy.props.BoolProperty( name="Smart Operations", default=True) bpy.types.Scene.fvne_show_utilities = bpy.props.BoolProperty( name="Utilities", default=True) bpy.types.Scene.fvne_show_presets = bpy.props.BoolProperty( name="Add Presets", default=True) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) del bpy.types.Scene.fvne_show_vertex_mirror del bpy.types.Scene.fvne_show_topology_mirror del bpy.types.Scene.fvne_show_smart_ops del bpy.types.Scene.fvne_show_utilities del bpy.types.Scene.fvne_show_presets if __name__ == "__main__": register()