Pose Shape Keys: Major Update #321
@ -46,13 +46,13 @@ class EASYWEIGHT_OT_force_apply_mirror(bpy.types.Operator):
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
if not obj or obj.type != 'MESH':
|
||||
cls.set_poll_message("There must be an active mesh object deformed by an Armature.")
|
||||
cls.poll_message_set("There must be an active mesh object deformed by an Armature.")
|
||||
return False
|
||||
for mod in obj.modifiers:
|
||||
if mod.type == 'MIRROR':
|
||||
return True
|
||||
|
||||
cls.set_poll_message("This mesh is not deformed by an Armature modifier.")
|
||||
cls.poll_message_set("This mesh is not deformed by an Armature modifier.")
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
|
@ -139,7 +139,7 @@ class EASYWEIGHT_OT_toggle_weight_paint(Operator):
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
if not obj and obj.type == 'MESH':
|
||||
cls.set_poll_message("Active object must be a mesh.")
|
||||
cls.poll_message_set("Active object must be a mesh.")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
@ -1,19 +0,0 @@
|
||||
## 0.0.4 - 2024-02-23
|
||||
|
||||
|
||||
### CHANGED
|
||||
- Change name separator from . to -
|
||||
- Format w/ Black + an icon API breakage
|
||||
- Use consistent registration pattern
|
||||
|
||||
## 0.0.3 - 2023-08-02
|
||||
|
||||
### FIXED
|
||||
- Fix Changelog Rendering (#125)
|
||||
- Fix Typo in README
|
||||
- Fix line ends from DOS to UNIX (#68)
|
||||
|
||||
## 0.0.2 - 2023-06-02
|
||||
|
||||
## DOCUMENTED
|
||||
- Initial release
|
@ -1,29 +1,13 @@
|
||||
# Pose Shape Keys addon for Blender
|
||||
# Copyright (C) 2022 Demeter Dzadik
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
bl_info = {
|
||||
"name": "Pose Shape Keys",
|
||||
"author": "Demeter Dzadik",
|
||||
"version": (0, 0, 4),
|
||||
"version": (1, 0, 0),
|
||||
"blender": (3, 1, 0),
|
||||
"location": "Properties -> Mesh Data -> Shape Keys -> Pose Keys",
|
||||
"description": "Create shape keys that blend deformed meshes into a desired shape",
|
||||
"category": "Rigging",
|
||||
"doc_url": "",
|
||||
"tracker_url": "",
|
||||
"doc_url": "https://studio.blender.org/pipeline/addons/pose_shape_keys",
|
||||
"tracker_url": "https://projects.blender.org/studio/blender-studio-pipeline/src/branch/main/scripts-blender/addons/pose_shape_keys",
|
||||
}
|
||||
|
||||
import importlib
|
||||
@ -32,12 +16,11 @@ import bpy
|
||||
from . import ui
|
||||
from . import pose_key
|
||||
from . import ui_list
|
||||
from . import reset_rig
|
||||
from . import symmetrize_shape_key
|
||||
from . import prefs
|
||||
|
||||
# Each module can have register() and unregister() functions and a list of classes to register called "registry".
|
||||
modules = [prefs, ui, pose_key, ui_list, reset_rig, symmetrize_shape_key]
|
||||
modules = [prefs, ui, pose_key, ui_list, symmetrize_shape_key]
|
||||
|
||||
|
||||
def register_unregister_modules(modules, register: bool):
|
||||
|
19
scripts-blender/addons/pose_shape_keys/blender_manifest.toml
Normal file
19
scripts-blender/addons/pose_shape_keys/blender_manifest.toml
Normal file
@ -0,0 +1,19 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "pose_shape_keys"
|
||||
version = "1.0.0"
|
||||
name = "Pose Shape Keys"
|
||||
tagline = "Continue iterating on your weights and constraints without destroying your shape keys"
|
||||
maintainer = "Demeter Dzadik <demeter@blender.org>"
|
||||
type = "add-on"
|
||||
website = "https://studio.blender.org/pipeline/addons/pose_shape_keys"
|
||||
tags = ["Rigging"]
|
||||
|
||||
blender_version_min = "4.2.0"
|
||||
|
||||
license = [
|
||||
"SPDX:GPL-3.0-or-later",
|
||||
]
|
||||
copyright = [
|
||||
"2019-2024 Demeter Dzadik & Blender Studio",
|
||||
]
|
@ -1,10 +1,10 @@
|
||||
import bpy
|
||||
from typing import List
|
||||
from bpy.types import PropertyGroup, Object, Operator, Action, ShapeKey, VertexGroup, MeshVertex
|
||||
from bpy.props import PointerProperty, IntProperty, CollectionProperty, StringProperty, BoolProperty
|
||||
from mathutils import Vector
|
||||
from math import sqrt
|
||||
from .symmetrize_shape_key import mirror_mesh
|
||||
from .prefs import get_addon_prefs
|
||||
|
||||
# When saving or pushing shapes, disable any modifier NOT in this list.
|
||||
DEFORM_MODIFIERS = [
|
||||
@ -28,18 +28,14 @@ DEFORM_MODIFIERS = [
|
||||
GOOD_MODIFIERS = ['ARMATURE']
|
||||
|
||||
|
||||
def get_addon_prefs(context):
|
||||
return context.preferences.addons[__package__].preferences
|
||||
|
||||
|
||||
class PoseShapeKeyTarget(PropertyGroup):
|
||||
def update_name(self, context):
|
||||
if self.block_name_update:
|
||||
return
|
||||
ob = context.object
|
||||
if not ob.data.shape_keys:
|
||||
obj = context.object
|
||||
if not obj.data.shape_keys:
|
||||
return
|
||||
sk = ob.data.shape_keys.key_blocks.get(self.shape_key_name)
|
||||
sk = obj.data.shape_keys.key_blocks.get(self.shape_key_name)
|
||||
if sk:
|
||||
sk.name = self.name
|
||||
self.shape_key_name = self.name
|
||||
@ -70,7 +66,7 @@ class PoseShapeKeyTarget(PropertyGroup):
|
||||
)
|
||||
|
||||
@property
|
||||
def key_block(self) -> List[ShapeKey]:
|
||||
def key_block(self) -> list[ShapeKey]:
|
||||
mesh = self.id_data
|
||||
if not mesh.shape_keys:
|
||||
return
|
||||
@ -81,25 +77,25 @@ class PoseShapeKey(PropertyGroup):
|
||||
target_shapes: CollectionProperty(type=PoseShapeKeyTarget)
|
||||
|
||||
def update_active_sk_index(self, context):
|
||||
ob = context.object
|
||||
if not ob.data.shape_keys:
|
||||
obj = context.object
|
||||
if not obj.data.shape_keys:
|
||||
return
|
||||
try:
|
||||
sk_name = self.target_shapes[self.active_target_shape_index].shape_key_name
|
||||
except IndexError:
|
||||
ob.active_shape_key_index = len(ob.data.shape_keys.key_blocks) - 1
|
||||
obj.active_shape_key_index = len(obj.data.shape_keys.key_blocks) - 1
|
||||
return
|
||||
key_block_idx = ob.data.shape_keys.key_blocks.find(sk_name)
|
||||
key_block_idx = obj.data.shape_keys.key_blocks.find(sk_name)
|
||||
if key_block_idx > -1:
|
||||
ob.active_shape_key_index = key_block_idx
|
||||
obj.active_shape_key_index = key_block_idx
|
||||
|
||||
# If in weight paint mode and there is a mask vertex group,
|
||||
# also set that vertex group as active.
|
||||
if context.mode == 'PAINT_WEIGHT':
|
||||
key_block = ob.data.shape_keys.key_blocks[key_block_idx]
|
||||
vg_idx = ob.vertex_groups.find(key_block.vertex_group)
|
||||
key_block = obj.data.shape_keys.key_blocks[key_block_idx]
|
||||
vg_idx = obj.vertex_groups.find(key_block.vertex_group)
|
||||
if vg_idx > -1:
|
||||
ob.vertex_groups.active_index = vg_idx
|
||||
obj.vertex_groups.active_index = vg_idx
|
||||
|
||||
active_target_shape_index: IntProperty(update=update_active_sk_index)
|
||||
|
||||
@ -121,12 +117,12 @@ class PoseShapeKey(PropertyGroup):
|
||||
|
||||
|
||||
def get_deforming_armature(mesh_ob: Object) -> Object:
|
||||
for m in mesh_ob.modifiers:
|
||||
if m.type == 'ARMATURE':
|
||||
return m.object
|
||||
for mod in mesh_ob.modifiers:
|
||||
if mod.type == 'ARMATURE':
|
||||
return mod.object
|
||||
|
||||
|
||||
class OBJECT_OT_Create_ShapeKey_For_Pose(Operator):
|
||||
class OBJECT_OT_create_shape_key_for_pose(Operator):
|
||||
"""Create and assign a Shape Key"""
|
||||
|
||||
bl_idname = "object.create_shape_key_for_pose"
|
||||
@ -136,13 +132,13 @@ class OBJECT_OT_Create_ShapeKey_For_Pose(Operator):
|
||||
|
||||
def update_sk_name(self, context):
|
||||
def set_vg(vg_name):
|
||||
ob = context.object
|
||||
vg = ob.vertex_groups.get(vg_name)
|
||||
obj = context.object
|
||||
vg = obj.vertex_groups.get(vg_name)
|
||||
if vg:
|
||||
self.vg_name = vg.name
|
||||
return vg
|
||||
|
||||
ob = context.object
|
||||
obj = context.object
|
||||
vg = set_vg(self.sk_name)
|
||||
if not vg and self.sk_name.endswith(".L"):
|
||||
vg = set_vg("Side.L")
|
||||
@ -162,13 +158,13 @@ class OBJECT_OT_Create_ShapeKey_For_Pose(Operator):
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
ob = context.object
|
||||
if ob.data.shape_keys:
|
||||
self.sk_name = f"Key {len(ob.data.shape_keys.key_blocks)}"
|
||||
obj = context.object
|
||||
if obj.data.shape_keys:
|
||||
self.sk_name = f"Key {len(obj.data.shape_keys.key_blocks)}"
|
||||
else:
|
||||
self.sk_name = "Key"
|
||||
|
||||
pose_key = ob.data.pose_keys[ob.data.active_pose_key_index]
|
||||
pose_key = obj.data.pose_keys[obj.data.active_pose_key_index]
|
||||
if pose_key.name:
|
||||
self.sk_name = pose_key.name
|
||||
|
||||
@ -177,26 +173,26 @@ class OBJECT_OT_Create_ShapeKey_For_Pose(Operator):
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, 'sk_name')
|
||||
ob = context.object
|
||||
layout.prop_search(self, 'vg_name', ob, "vertex_groups")
|
||||
obj = context.object
|
||||
layout.prop_search(self, 'vg_name', obj, "vertex_groups")
|
||||
|
||||
def execute(self, context):
|
||||
ob = context.object
|
||||
obj = context.object
|
||||
|
||||
# Ensure Basis shape key
|
||||
if not ob.data.shape_keys:
|
||||
basis = ob.shape_key_add()
|
||||
if not obj.data.shape_keys:
|
||||
basis = obj.shape_key_add()
|
||||
basis.name = "Basis"
|
||||
ob.data.update()
|
||||
obj.data.update()
|
||||
|
||||
# Add new shape key
|
||||
new_sk = ob.shape_key_add()
|
||||
new_sk = obj.shape_key_add()
|
||||
new_sk.name = self.sk_name
|
||||
new_sk.value = 1
|
||||
if self.vg_name:
|
||||
new_sk.vertex_group = self.vg_name
|
||||
|
||||
pose_key = ob.data.pose_keys[ob.data.active_pose_key_index]
|
||||
pose_key = obj.data.pose_keys[obj.data.active_pose_key_index]
|
||||
target = pose_key.target_shapes[pose_key.active_target_shape_index]
|
||||
target.name = new_sk.name
|
||||
|
||||
@ -210,34 +206,34 @@ class SaveAndRestoreState:
|
||||
self.disabled_mods_storage = []
|
||||
self.disabled_mods_rigged = []
|
||||
self.disabled_fcurves = []
|
||||
for ob, lst in zip(
|
||||
for obj, lst in zip(
|
||||
[storage_ob, rigged_ob], [self.disabled_mods_storage, self.disabled_mods_rigged]
|
||||
):
|
||||
if not ob:
|
||||
if not obj:
|
||||
continue
|
||||
for m in ob.modifiers:
|
||||
if m.type not in GOOD_MODIFIERS and m.show_viewport:
|
||||
lst.append(m.name)
|
||||
m.show_viewport = False
|
||||
if m.show_viewport:
|
||||
data_path = f'modifiers["{m.name}"].show_viewport'
|
||||
fc = ob.animation_data.drivers.find(data_path)
|
||||
for mod in obj.modifiers:
|
||||
if mod.type not in GOOD_MODIFIERS and mod.show_viewport:
|
||||
lst.append(mod.name)
|
||||
mod.show_viewport = False
|
||||
if mod.show_viewport:
|
||||
data_path = f'modifiers["{mod.name}"].show_viewport'
|
||||
fc = obj.animation_data.drivers.find(data_path)
|
||||
if fc:
|
||||
fc.mute = True
|
||||
self.disabled_fcurves.append(data_path)
|
||||
m.show_viewport = False
|
||||
mod.show_viewport = False
|
||||
|
||||
def restore_non_deform_modifiers(self, storage_ob: Object, rigged_ob: Object):
|
||||
# Re-enable non-deforming modifiers
|
||||
for ob, m_list in zip(
|
||||
for obj, mod_list in zip(
|
||||
[storage_ob, rigged_ob], [self.disabled_mods_storage, self.disabled_mods_rigged]
|
||||
):
|
||||
if not ob:
|
||||
if not obj:
|
||||
continue
|
||||
for m_name in m_list:
|
||||
ob.modifiers[m_name].show_viewport = True
|
||||
for mod_namee in mod_list:
|
||||
obj.modifiers[mod_namee].show_viewport = True
|
||||
for data_path in self.disabled_fcurves:
|
||||
fc = ob.animation_data.drivers.find(data_path)
|
||||
fc = obj.animation_data.drivers.find(data_path)
|
||||
if fc:
|
||||
fc.mute = False
|
||||
|
||||
@ -300,20 +296,61 @@ class OperatorWithWarning:
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
def set_pose_of_active_pose_key(context):
|
||||
bpy.ops.object.posekey_reset_rig()
|
||||
def reset_rig(rig, *, reset_transforms=True, reset_props=True, pbones=[]):
|
||||
if not pbones:
|
||||
pbones = rig.pose.bones
|
||||
for pb in pbones:
|
||||
if reset_transforms:
|
||||
pb.location = (0, 0, 0)
|
||||
pb.rotation_euler = (0, 0, 0)
|
||||
pb.rotation_quaternion = (1, 0, 0, 0)
|
||||
pb.scale = (1, 1, 1)
|
||||
|
||||
if not reset_props or len(pb.keys()) == 0:
|
||||
continue
|
||||
|
||||
rna_properties = [prop.identifier for prop in pb.bl_rna.properties if prop.is_runtime]
|
||||
|
||||
# Reset custom property values to their default value
|
||||
for key in pb.keys():
|
||||
if key.startswith("$"):
|
||||
continue
|
||||
if key in rna_properties:
|
||||
continue # Addon defined property.
|
||||
|
||||
property_settings = None
|
||||
try:
|
||||
property_settings = pb.id_properties_ui(key)
|
||||
if not property_settings:
|
||||
continue
|
||||
property_settings = property_settings.as_dict()
|
||||
if not 'default' in property_settings:
|
||||
continue
|
||||
except TypeError:
|
||||
# Some properties don't support UI data, and so don't have a default value. (like addon PropertyGroups)
|
||||
pass
|
||||
|
||||
if not property_settings:
|
||||
continue
|
||||
|
||||
if type(pb[key]) not in (float, int, bool):
|
||||
continue
|
||||
pb[key] = property_settings['default']
|
||||
|
||||
|
||||
def set_pose_of_active_pose_key(context):
|
||||
rigged_ob = context.object
|
||||
pose_key = rigged_ob.data.pose_keys[rigged_ob.data.active_pose_key_index]
|
||||
|
||||
arm_ob = get_deforming_armature(rigged_ob)
|
||||
reset_rig(arm_ob)
|
||||
if pose_key.action:
|
||||
# Set Action and Frame to get the right pose
|
||||
arm_ob.animation_data.action = pose_key.action
|
||||
context.scene.frame_current = pose_key.frame
|
||||
|
||||
|
||||
class OBJECT_OT_PoseKey_Set_Pose(Operator):
|
||||
class OBJECT_OT_pose_key_set_pose(Operator):
|
||||
"""Set the rig pose to the specified action and frame (Reset any other posing)"""
|
||||
|
||||
bl_idname = "object.posekey_set_pose"
|
||||
@ -322,16 +359,23 @@ class OBJECT_OT_PoseKey_Set_Pose(Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
rigged_ob = context.object
|
||||
arm_ob = get_deforming_armature(rigged_ob)
|
||||
obj = context.object
|
||||
if not obj or obj.type != 'MESH':
|
||||
cls.poll_message_set("Active object must be a mesh.")
|
||||
return False
|
||||
arm_ob = get_deforming_armature(obj)
|
||||
if not arm_ob:
|
||||
cls.poll_message_set("Mesh must be deformed by an Armature modifier.")
|
||||
return False
|
||||
if rigged_ob.type != 'MESH' or not rigged_ob.data.shape_keys:
|
||||
if not obj.data.shape_keys:
|
||||
cls.poll_message_set("Mesh must have shape keys.")
|
||||
return False
|
||||
if len(rigged_ob.data.pose_keys) == 0:
|
||||
if len(obj.data.pose_keys) == 0:
|
||||
cls.poll_message_set("Mesh must have pose keys.")
|
||||
return True
|
||||
pose_key = rigged_ob.data.pose_keys[rigged_ob.data.active_pose_key_index]
|
||||
pose_key = obj.data.pose_keys[obj.data.active_pose_key_index]
|
||||
if not pose_key.action:
|
||||
cls.poll_message_set("Pose Key must have an Action assigned.")
|
||||
return False
|
||||
|
||||
return True
|
||||
@ -341,40 +385,57 @@ class OBJECT_OT_PoseKey_Set_Pose(Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_active_pose_key(ob):
|
||||
if ob.type != 'MESH':
|
||||
def get_active_pose_key(obj):
|
||||
if obj.type != 'MESH':
|
||||
return
|
||||
if len(ob.data.pose_keys) == 0:
|
||||
if len(obj.data.pose_keys) == 0:
|
||||
return
|
||||
|
||||
return ob.data.pose_keys[ob.data.active_pose_key_index]
|
||||
return obj.data.pose_keys[obj.data.active_pose_key_index]
|
||||
|
||||
|
||||
def verify_pose(context):
|
||||
def poll_correct_pose_key_pose(operator, context):
|
||||
"""To make these operators foolproof, there are a lot of checks to make sure
|
||||
that the user gets to see the effect of the operator. The "Set Pose" operator
|
||||
can be used first to set the correct state and pass all the checks here.
|
||||
"""
|
||||
ob = context.object
|
||||
obj = context.object
|
||||
|
||||
pose_key = get_active_pose_key(ob)
|
||||
if not obj:
|
||||
operator.poll_message_set("There must be an active mesh object.")
|
||||
|
||||
pose_key = get_active_pose_key(obj)
|
||||
if not pose_key:
|
||||
operator.poll_message_set("A Pose Shape Key must be selected.")
|
||||
return False
|
||||
|
||||
arm_ob = get_deforming_armature(ob)
|
||||
arm_ob = get_deforming_armature(obj)
|
||||
|
||||
if not arm_ob:
|
||||
operator.poll_message_set("This mesh object is not deformed by any Armature modifier.")
|
||||
|
||||
# Action must exist and match.
|
||||
if not pose_key.action:
|
||||
return False
|
||||
if not arm_ob.animation_data or arm_ob.animation_data.action != pose_key.action:
|
||||
if not (
|
||||
pose_key.action
|
||||
and arm_ob.animation_data
|
||||
and arm_ob.animation_data.action
|
||||
and arm_ob.animation_data.action == pose_key.action
|
||||
):
|
||||
operator.poll_message_set(
|
||||
"The armature must have the Pose Shape Key's action assigned. Use the Set Pose button."
|
||||
)
|
||||
return False
|
||||
|
||||
if pose_key.frame != context.scene.frame_current:
|
||||
operator.poll_message_set(
|
||||
"The Pose Shape Key's frame must be the same as the current scene frame. Use the Set Pose button."
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class OBJECT_OT_PoseKey_Save(Operator, OperatorWithWarning, SaveAndRestoreState):
|
||||
class OBJECT_OT_pose_key_save(Operator, OperatorWithWarning, SaveAndRestoreState):
|
||||
"""Save the current evaluated mesh vertex positions into the Storage Object"""
|
||||
|
||||
bl_idname = "object.posekey_save"
|
||||
@ -383,31 +444,22 @@ class OBJECT_OT_PoseKey_Save(Operator, OperatorWithWarning, SaveAndRestoreState)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
ob = context.object
|
||||
# We can guess the action and frame number
|
||||
arm_ob = get_deforming_armature(ob)
|
||||
pose_key = get_active_pose_key(ob)
|
||||
if (
|
||||
pose_key
|
||||
and not pose_key.storage_object
|
||||
and not pose_key.action
|
||||
and arm_ob.animation_data
|
||||
and arm_ob.animation_data.action
|
||||
):
|
||||
return True
|
||||
return verify_pose(context)
|
||||
if not poll_correct_pose_key_pose(cls, context):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def invoke(self, context, event):
|
||||
ob = context.object
|
||||
pose_key = ob.data.pose_keys[ob.data.active_pose_key_index]
|
||||
obj = context.object
|
||||
pose_key = obj.data.pose_keys[obj.data.active_pose_key_index]
|
||||
if pose_key.storage_object:
|
||||
return super().invoke(context, event)
|
||||
return self.execute(context)
|
||||
|
||||
def get_warning_text(self, context):
|
||||
ob = context.object
|
||||
pose_key = ob.data.pose_keys[ob.data.active_pose_key_index]
|
||||
return f'This will overwrite "{pose_key.storage_object.name}".\n Are you sure?'
|
||||
obj = context.object
|
||||
pose_key = obj.data.pose_keys[obj.data.active_pose_key_index]
|
||||
return f'Overwrite storage object "{pose_key.storage_object.name}"?'
|
||||
|
||||
def execute(self, context):
|
||||
rigged_ob = context.object
|
||||
@ -490,7 +542,7 @@ class OBJECT_OT_PoseKey_Save(Operator, OperatorWithWarning, SaveAndRestoreState)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class OBJECT_OT_PoseKey_Push(Operator, OperatorWithWarning, SaveAndRestoreState):
|
||||
class OBJECT_OT_pose_key_push(Operator, OperatorWithWarning, SaveAndRestoreState):
|
||||
"""Let the below shape keys blend the current deformed shape into the shape of the Storage Object"""
|
||||
|
||||
bl_idname = "object.posekey_push"
|
||||
@ -499,22 +551,24 @@ class OBJECT_OT_PoseKey_Push(Operator, OperatorWithWarning, SaveAndRestoreState)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
pose_matches = verify_pose(context)
|
||||
if not pose_matches:
|
||||
if not poll_correct_pose_key_pose(cls, context):
|
||||
return False
|
||||
|
||||
# No shape keys to push into
|
||||
ob = context.object
|
||||
pose_key = get_active_pose_key(ob)
|
||||
# No shape keys to push into.
|
||||
obj = context.object
|
||||
pose_key = get_active_pose_key(obj)
|
||||
for target_shape in pose_key.target_shapes:
|
||||
if target_shape.key_block:
|
||||
return True
|
||||
|
||||
cls.poll_message_set(
|
||||
"This Pose Key doesn't have any target shape keys to push into. Add some in the Shape Key Slots list below."
|
||||
)
|
||||
return False
|
||||
|
||||
def get_warning_text(self, context):
|
||||
ob = context.object
|
||||
pose_key = ob.data.pose_keys[ob.data.active_pose_key_index]
|
||||
obj = context.object
|
||||
pose_key = obj.data.pose_keys[obj.data.active_pose_key_index]
|
||||
target_shape_names = [target.name for target in pose_key.target_shapes if target]
|
||||
return (
|
||||
"This will overwrite the following Shape Keys: \n "
|
||||
@ -612,7 +666,7 @@ class OBJECT_OT_PoseKey_Push(Operator, OperatorWithWarning, SaveAndRestoreState)
|
||||
)
|
||||
|
||||
|
||||
class OBJECT_OT_PoseKey_Push_All(Operator, OperatorWithWarning, SaveAndRestoreState):
|
||||
class OBJECT_OT_pose_key_push_all(Operator, OperatorWithWarning, SaveAndRestoreState):
|
||||
"""Go through all Pose Keys, set their pose and overwrite the shape keys to match the storage object shapes"""
|
||||
|
||||
bl_idname = "object.posekey_push_all"
|
||||
@ -621,15 +675,19 @@ class OBJECT_OT_PoseKey_Push_All(Operator, OperatorWithWarning, SaveAndRestoreSt
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
ob = context.object
|
||||
if not ob or ob.type != 'MESH':
|
||||
obj = context.object
|
||||
if not obj or obj.type != 'MESH':
|
||||
cls.poll_message_set("No active mesh object.")
|
||||
return False
|
||||
return len(ob.data.pose_keys) > 0
|
||||
if len(obj.data.pose_keys) == 0:
|
||||
cls.poll_message_set("No Pose Shape Keys to push.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_warning_text(self, context):
|
||||
ob = context.object
|
||||
obj = context.object
|
||||
target_shape_names = []
|
||||
for pk in ob.data.pose_keys:
|
||||
for pk in obj.data.pose_keys:
|
||||
target_shape_names.extend([t.name for t in pk.target_shapes if t])
|
||||
return (
|
||||
"This will overwrite the following Shape Keys: \n "
|
||||
@ -647,7 +705,7 @@ class OBJECT_OT_PoseKey_Push_All(Operator, OperatorWithWarning, SaveAndRestoreSt
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class OBJECT_OT_PoseKey_Clamp_Influence(Operator):
|
||||
class OBJECT_OT_pose_key_clamp_influence(Operator):
|
||||
"""Clamp the influence of this pose key's shape keys to 1.0 for each vertex, by normalizing the vertex weight mask values of vertices where the total influence is greater than 1"""
|
||||
|
||||
bl_idname = "object.posekey_clamp_influence"
|
||||
@ -655,7 +713,7 @@ class OBJECT_OT_PoseKey_Clamp_Influence(Operator):
|
||||
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
||||
|
||||
@staticmethod
|
||||
def get_affected_vertex_group_names(object: Object) -> List[str]:
|
||||
def get_affected_vertex_group_names(object: Object) -> list[str]:
|
||||
pose_key = object.data.pose_keys[object.data.active_pose_key_index]
|
||||
|
||||
vg_names = []
|
||||
@ -670,36 +728,38 @@ class OBJECT_OT_PoseKey_Clamp_Influence(Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return cls.get_affected_vertex_group_names(context.object)
|
||||
if not cls.get_affected_vertex_group_names(context.object):
|
||||
cls.poll_message_set("No shape keys of this pose shape key use vertex masks. There is nothing to clamp.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def normalize_vgroups(self, o, vgroups):
|
||||
def normalize_vgroups(self, obj, vgroups):
|
||||
"""Normalize a set of vertex groups in isolation"""
|
||||
""" Used for creating mask vertex groups for splitting shape keys """
|
||||
for v in o.data.vertices:
|
||||
for vert in obj.data.vertices:
|
||||
# Find sum of weights in specified vgroups
|
||||
# set weight to original/sum
|
||||
sum_weights = 0
|
||||
for vg in vgroups:
|
||||
w = 0
|
||||
for vgroup in vgroups:
|
||||
try:
|
||||
sum_weights += vg.weight(v.index)
|
||||
sum_weights += vgroup.weight(vert.index)
|
||||
except:
|
||||
pass
|
||||
for vg in vgroups:
|
||||
for vgroup in vgroups:
|
||||
if sum_weights > 1.0:
|
||||
try:
|
||||
vg.add([v.index], vg.weight(v.index) / sum_weights, 'REPLACE')
|
||||
vgroup.add([vert.index], vgroup.weight(vert.index) / sum_weights, 'REPLACE')
|
||||
except:
|
||||
pass
|
||||
|
||||
def execute(self, context):
|
||||
ob = context.object
|
||||
vg_names = self.get_affected_vertex_group_names(ob)
|
||||
self.normalize_vgroups(ob, [ob.vertex_groups[vg_name] for vg_name in vg_names])
|
||||
obj = context.object
|
||||
vg_names = self.get_affected_vertex_group_names(obj)
|
||||
self.normalize_vgroups(obj, [obj.vertex_groups[vg_name] for vg_name in vg_names])
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class OBJECT_OT_PoseKey_Place_Objects_In_Grid(Operator):
|
||||
class OBJECT_OT_pose_key_place_objects_in_grid(Operator):
|
||||
"""Place the storage objects in a grid above this object"""
|
||||
|
||||
bl_idname = "object.posekey_object_grid"
|
||||
@ -707,30 +767,35 @@ class OBJECT_OT_PoseKey_Place_Objects_In_Grid(Operator):
|
||||
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
||||
|
||||
@staticmethod
|
||||
def get_storage_objects(context) -> List[Object]:
|
||||
ob = context.object
|
||||
pose_keys = ob.data.pose_keys
|
||||
def get_storage_objects(context) -> list[Object]:
|
||||
obj = context.object
|
||||
pose_keys = obj.data.pose_keys
|
||||
return [pk.storage_object for pk in pose_keys if pk.storage_object]
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
"""Only available if there are any storage objects in any of the pose keys."""
|
||||
return cls.get_storage_objects(context)
|
||||
if not cls.get_storage_objects(context):
|
||||
cls.poll_message_set("This pose key has no storage objects, so there is nothing to sort into a grid.")
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def place_objects_in_grid(context, objs: List[Object]):
|
||||
x = max([o.dimensions.x for o in objs])
|
||||
y = max([o.dimensions.y for o in objs])
|
||||
z = max([o.dimensions.z for o in objs])
|
||||
def place_objects_in_grid(context, objs: list[Object]):
|
||||
if not objs:
|
||||
return
|
||||
x = max([obj.dimensions.x for obj in objs])
|
||||
y = max([obj.dimensions.y for obj in objs])
|
||||
z = max([obj.dimensions.z for obj in objs])
|
||||
scalar = 1.2
|
||||
dimensions = Vector((x * scalar, y * scalar, z * scalar))
|
||||
|
||||
grid_rows = round(sqrt(len(objs)))
|
||||
for i, ob in enumerate(objs):
|
||||
for i, obj in enumerate(objs):
|
||||
col_i = (i % grid_rows) - int(grid_rows / 2)
|
||||
row_i = int(i / grid_rows) + scalar
|
||||
offset = Vector((col_i * dimensions.x, 0, row_i * dimensions.z))
|
||||
ob.location = context.object.location + offset
|
||||
obj.location = context.object.location + offset
|
||||
|
||||
def execute(self, context):
|
||||
storage_objects = self.get_storage_objects(context)
|
||||
@ -739,7 +804,7 @@ class OBJECT_OT_PoseKey_Place_Objects_In_Grid(Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class OBJECT_OT_PoseKey_Jump_To_Shape(Operator):
|
||||
class OBJECT_OT_pose_key_jump_to_shape(Operator):
|
||||
"""Place the storage object next to this object and select it"""
|
||||
|
||||
bl_idname = "object.posekey_jump_to_storage"
|
||||
@ -748,14 +813,17 @@ class OBJECT_OT_PoseKey_Jump_To_Shape(Operator):
|
||||
|
||||
@staticmethod
|
||||
def get_storage_object(context):
|
||||
ob = context.object
|
||||
pose_key = ob.data.pose_keys[ob.data.active_pose_key_index]
|
||||
obj = context.object
|
||||
pose_key = obj.data.pose_keys[obj.data.active_pose_key_index]
|
||||
return pose_key.storage_object
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
"""Only available if there is a storage object in the pose key."""
|
||||
return cls.get_storage_object(context)
|
||||
if not cls.get_storage_object(context):
|
||||
cls.poll_message_set("This pose key doesn't have a storage object to jump to.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
storage_object = self.get_storage_object(context)
|
||||
@ -774,14 +842,14 @@ class OBJECT_OT_PoseKey_Jump_To_Shape(Operator):
|
||||
# Put the other storage objects in a grid
|
||||
prefs = get_addon_prefs(context)
|
||||
if prefs.grid_objects_on_jump:
|
||||
storage_objects = OBJECT_OT_PoseKey_Place_Objects_In_Grid.get_storage_objects(context)
|
||||
storage_objects = OBJECT_OT_pose_key_place_objects_in_grid.get_storage_objects(context)
|
||||
storage_objects.remove(storage_object)
|
||||
OBJECT_OT_PoseKey_Place_Objects_In_Grid.place_objects_in_grid(context, storage_objects)
|
||||
OBJECT_OT_pose_key_place_objects_in_grid.place_objects_in_grid(context, storage_objects)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class OBJECT_OT_PoseKey_Copy_Data(Operator):
|
||||
class OBJECT_OT_pose_key_copy_data(Operator):
|
||||
"""Copy Pose Key data from active object to selected ones"""
|
||||
|
||||
bl_idname = "object.posekey_copy_data"
|
||||
@ -791,16 +859,20 @@ class OBJECT_OT_PoseKey_Copy_Data(Operator):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
"""Only available if there is a selected mesh and the active mesh has pose key data."""
|
||||
selected_meshes = [ob for ob in context.selected_objects if ob.type == 'MESH']
|
||||
selected_meshes = [obj for obj in context.selected_objects if obj.type == 'MESH']
|
||||
if len(selected_meshes) < 2:
|
||||
cls.poll_message_set("No other meshes are selected to copy pose key data to.")
|
||||
return False
|
||||
if context.object.type != 'MESH' or not context.object.data.pose_keys:
|
||||
cls.poll_message_set("No active mesh object with pose keys to copy.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
source_ob = context.object
|
||||
targets = [ob for ob in context.selected_objects if ob.type == 'MESH' and ob != source_ob]
|
||||
targets = [
|
||||
obj for obj in context.selected_objects if obj.type == 'MESH' and obj != source_ob
|
||||
]
|
||||
|
||||
for target_ob in targets:
|
||||
target_ob.data.pose_keys.clear()
|
||||
@ -822,15 +894,15 @@ class OBJECT_OT_PoseKey_Copy_Data(Operator):
|
||||
registry = [
|
||||
PoseShapeKeyTarget,
|
||||
PoseShapeKey,
|
||||
OBJECT_OT_PoseKey_Save,
|
||||
OBJECT_OT_PoseKey_Set_Pose,
|
||||
OBJECT_OT_PoseKey_Push,
|
||||
OBJECT_OT_PoseKey_Push_All,
|
||||
OBJECT_OT_Create_ShapeKey_For_Pose,
|
||||
OBJECT_OT_PoseKey_Clamp_Influence,
|
||||
OBJECT_OT_PoseKey_Place_Objects_In_Grid,
|
||||
OBJECT_OT_PoseKey_Jump_To_Shape,
|
||||
OBJECT_OT_PoseKey_Copy_Data,
|
||||
OBJECT_OT_pose_key_save,
|
||||
OBJECT_OT_pose_key_set_pose,
|
||||
OBJECT_OT_pose_key_push,
|
||||
OBJECT_OT_pose_key_push_all,
|
||||
OBJECT_OT_create_shape_key_for_pose,
|
||||
OBJECT_OT_pose_key_clamp_influence,
|
||||
OBJECT_OT_pose_key_place_objects_in_grid,
|
||||
OBJECT_OT_pose_key_jump_to_shape,
|
||||
OBJECT_OT_pose_key_copy_data,
|
||||
]
|
||||
|
||||
|
||||
|
@ -1,5 +1,16 @@
|
||||
from bpy.types import AddonPreferences
|
||||
from bpy.props import BoolProperty
|
||||
from . import __package__ as base_package
|
||||
|
||||
|
||||
def get_addon_prefs(context=None):
|
||||
if not context:
|
||||
context = bpy.context
|
||||
if base_package.startswith('bl_ext'):
|
||||
# 4.2
|
||||
return context.preferences.addons[base_package].preferences
|
||||
else:
|
||||
return context.preferences.addons[base_package.split(".")[0]].preferences
|
||||
|
||||
|
||||
class PoseShapeKeysPrefs(AddonPreferences):
|
||||
@ -11,7 +22,7 @@ class PoseShapeKeysPrefs(AddonPreferences):
|
||||
default=True,
|
||||
)
|
||||
no_warning: BoolProperty(
|
||||
name="No Warning",
|
||||
name="No Danger Warning",
|
||||
description="Do not show a pop-up warning for dangerous operations",
|
||||
)
|
||||
grid_objects_on_jump: BoolProperty(
|
||||
|
@ -1,76 +0,0 @@
|
||||
import bpy
|
||||
from bpy.props import BoolProperty
|
||||
from .pose_key import get_deforming_armature
|
||||
|
||||
|
||||
class CK_OT_reset_rig(bpy.types.Operator):
|
||||
"""Reset all bone transforms and custom properties to their default values"""
|
||||
|
||||
bl_idname = "object.posekey_reset_rig"
|
||||
bl_label = "Reset Rig"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
reset_transforms: BoolProperty(
|
||||
name="Transforms", default=True, description="Reset bone transforms"
|
||||
)
|
||||
reset_props: BoolProperty(
|
||||
name="Properties", default=True, description="Reset custom properties"
|
||||
)
|
||||
selection_only: BoolProperty(
|
||||
name="Selected Only",
|
||||
default=False,
|
||||
description="Affect selected bones rather than all bones",
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
rigged_ob = context.object
|
||||
rig = get_deforming_armature(rigged_ob)
|
||||
bones = rig.pose.bones
|
||||
if self.selection_only:
|
||||
bones = context.selected_pose_bones
|
||||
for pb in bones:
|
||||
if self.reset_transforms:
|
||||
pb.location = (0, 0, 0)
|
||||
pb.rotation_euler = (0, 0, 0)
|
||||
pb.rotation_quaternion = (1, 0, 0, 0)
|
||||
pb.scale = (1, 1, 1)
|
||||
|
||||
if self.reset_props and len(pb.keys()) > 0:
|
||||
rna_properties = [
|
||||
prop.identifier for prop in pb.bl_rna.properties if prop.is_runtime
|
||||
]
|
||||
|
||||
# Reset custom property values to their default value
|
||||
for key in pb.keys():
|
||||
if key.startswith("$"):
|
||||
continue
|
||||
if key in rna_properties:
|
||||
continue # Addon defined property.
|
||||
|
||||
ui_data = None
|
||||
try:
|
||||
ui_data = pb.id_properties_ui(key)
|
||||
if not ui_data:
|
||||
continue
|
||||
ui_data = ui_data.as_dict()
|
||||
if not 'default' in ui_data:
|
||||
continue
|
||||
except TypeError:
|
||||
# Some properties don't support UI data, and so don't have a default value. (like addon PropertyGroups)
|
||||
pass
|
||||
|
||||
if not ui_data:
|
||||
continue
|
||||
|
||||
if type(pb[key]) not in (float, int):
|
||||
continue
|
||||
pb[key] = ui_data['default']
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
registry = [CK_OT_reset_rig]
|
@ -1,7 +1,4 @@
|
||||
# This script expects a mesh whose base shape is symmetrical, and symmetrize the
|
||||
# active shape key based on the symmetry of the base mesh.
|
||||
|
||||
from typing import List, Tuple
|
||||
from bpy.types import Operator
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, EnumProperty, FloatProperty
|
||||
from mathutils.kdtree import KDTree
|
||||
@ -9,12 +6,12 @@ from mathutils.kdtree import KDTree
|
||||
|
||||
def mirror_mesh(
|
||||
*,
|
||||
reference_verts: List,
|
||||
vertices: List,
|
||||
reference_verts: list,
|
||||
vertices: list,
|
||||
axis: str,
|
||||
symmetrize=False,
|
||||
symmetrize_pos_to_neg=True,
|
||||
) -> Tuple[int, int]:
|
||||
) -> tuple[int, int]:
|
||||
"""
|
||||
Symmetrize vertices around any axis in any direction based on a set of
|
||||
reference vertices which share the same vertex order and are known to be
|
||||
@ -95,8 +92,10 @@ def mirror_mesh(
|
||||
return good_counter, bad_counter
|
||||
|
||||
|
||||
class OBJECT_OT_Symmetrize_Shape_Key(bpy.types.Operator):
|
||||
class OBJECT_OT_symmetrize_shape_key(Operator):
|
||||
"""Symmetrize shape key by matching vertex pairs by proximity in the original mesh"""
|
||||
# NOTE: This script expects a mesh whose base shape is symmetrical, and symmetrize the
|
||||
# active shape key based on the symmetry of the base mesh.
|
||||
|
||||
bl_idname = "object.symmetrize_shape_key"
|
||||
bl_label = "Symmetrize Shape Key"
|
||||
@ -133,8 +132,8 @@ class OBJECT_OT_Symmetrize_Shape_Key(bpy.types.Operator):
|
||||
layout.prop(self, 'threshold', slider=True)
|
||||
|
||||
def execute(self, context):
|
||||
ob = context.object
|
||||
mesh = ob.data
|
||||
obj = context.object
|
||||
mesh = obj.data
|
||||
|
||||
if 'X' in self.direction:
|
||||
axis = 'X'
|
||||
@ -145,10 +144,10 @@ class OBJECT_OT_Symmetrize_Shape_Key(bpy.types.Operator):
|
||||
|
||||
pos_to_neg = not self.direction.startswith('NEG')
|
||||
|
||||
key_blocks = [ob.active_shape_key]
|
||||
key_blocks = [obj.active_shape_key]
|
||||
if self.all_keys:
|
||||
# TODO: This could be more optimized, right now we re-build the kdtree for each key block unneccessarily.
|
||||
key_blocks = ob.data.shape_keys.key_blocks[:]
|
||||
key_blocks = obj.data.shape_keys.key_blocks[:]
|
||||
|
||||
for kb in key_blocks:
|
||||
good_counter, bad_counter = mirror_mesh(
|
||||
@ -175,12 +174,12 @@ class OBJECT_OT_Symmetrize_Shape_Key(bpy.types.Operator):
|
||||
def draw_symmetrize_buttons(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
op = layout.operator(OBJECT_OT_Symmetrize_Shape_Key.bl_idname, text="Symmetrize Active")
|
||||
op = layout.operator(OBJECT_OT_Symmetrize_Shape_Key.bl_idname, text="Symmetrize All")
|
||||
op = layout.operator(OBJECT_OT_symmetrize_shape_key.bl_idname, text="Symmetrize Active")
|
||||
op = layout.operator(OBJECT_OT_symmetrize_shape_key.bl_idname, text="Symmetrize All")
|
||||
op.all_keys = True
|
||||
|
||||
|
||||
registry = [OBJECT_OT_Symmetrize_Shape_Key]
|
||||
registry = [OBJECT_OT_symmetrize_shape_key]
|
||||
|
||||
|
||||
def register():
|
||||
|
@ -1,12 +1,10 @@
|
||||
import bpy
|
||||
from bpy.types import Object, Panel, UIList, Menu
|
||||
from bl_ui.properties_data_mesh import DATA_PT_shape_keys
|
||||
from .ui_list import draw_ui_list
|
||||
from bpy.props import EnumProperty
|
||||
from bl_ui.properties_data_mesh import DATA_PT_shape_keys
|
||||
|
||||
|
||||
def get_addon_prefs(context):
|
||||
return context.preferences.addons[__package__].preferences
|
||||
from .prefs import get_addon_prefs
|
||||
|
||||
|
||||
class CK_UL_pose_keys(UIList):
|
||||
@ -27,7 +25,6 @@ class CK_UL_pose_keys(UIList):
|
||||
class CK_UL_target_keys(UIList):
|
||||
def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname):
|
||||
obj = context.object
|
||||
pose_key = data # I think?
|
||||
pose_key_target = item
|
||||
key_block = pose_key_target.key_block
|
||||
|
||||
@ -58,9 +55,9 @@ class CK_UL_target_keys(UIList):
|
||||
mute_row.prop(key_block, 'mute', emboss=False, text="")
|
||||
|
||||
|
||||
def ob_has_armature_mod(ob: Object) -> bool:
|
||||
for m in ob.modifiers:
|
||||
if m.type == 'ARMATURE':
|
||||
def obj_has_armature_mod(obj: Object) -> bool:
|
||||
for mod in obj.modifiers:
|
||||
if mod.type == 'ARMATURE':
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -77,8 +74,8 @@ class MESH_PT_pose_keys(Panel):
|
||||
return context.object and context.object.type == 'MESH'
|
||||
|
||||
def draw(self, context):
|
||||
ob = context.object
|
||||
mesh = ob.data
|
||||
obj = context.object
|
||||
mesh = obj.data
|
||||
layout = self.layout
|
||||
|
||||
layout.prop(mesh, 'shape_key_ui_type', text="List Type: ", expand=True)
|
||||
@ -86,7 +83,7 @@ class MESH_PT_pose_keys(Panel):
|
||||
if mesh.shape_key_ui_type == 'DEFAULT':
|
||||
return DATA_PT_shape_keys.draw(self, context)
|
||||
|
||||
if not ob_has_armature_mod(ob):
|
||||
if not obj_has_armature_mod(obj):
|
||||
layout.alert = True
|
||||
layout.label(text="Object must have an Armature modifier to use Pose Keys.")
|
||||
return
|
||||
@ -153,17 +150,17 @@ class MESH_PT_shape_key_subpanel(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
ob = context.object
|
||||
obj = context.object
|
||||
return (
|
||||
ob.data.shape_key_ui_type == 'POSE_KEYS'
|
||||
and len(ob.data.pose_keys) > 0
|
||||
and ob.data.pose_keys[ob.data.active_pose_key_index].storage_object
|
||||
and ob_has_armature_mod(ob)
|
||||
obj.data.shape_key_ui_type == 'POSE_KEYS'
|
||||
and len(obj.data.pose_keys) > 0
|
||||
and obj.data.pose_keys[obj.data.active_pose_key_index].storage_object
|
||||
and obj_has_armature_mod(obj)
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
ob = context.object
|
||||
mesh = ob.data
|
||||
obj = context.object
|
||||
mesh = obj.data
|
||||
layout = self.layout
|
||||
|
||||
layout.use_property_split = True
|
||||
@ -206,7 +203,7 @@ class MESH_PT_shape_key_subpanel(Panel):
|
||||
row = col.row(align=True)
|
||||
row.prop(sk, 'slider_min', text="Range")
|
||||
row.prop(sk, 'slider_max', text="")
|
||||
col.prop_search(sk, "vertex_group", ob, "vertex_groups", text="Vertex Mask")
|
||||
col.prop_search(sk, "vertex_group", obj, "vertex_groups", text="Vertex Mask")
|
||||
col.row().prop(sk, 'relative_key')
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user