Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'rigify/operators/action_layers.py')
-rw-r--r--rigify/operators/action_layers.py525
1 files changed, 525 insertions, 0 deletions
diff --git a/rigify/operators/action_layers.py b/rigify/operators/action_layers.py
new file mode 100644
index 00000000..c089722a
--- /dev/null
+++ b/rigify/operators/action_layers.py
@@ -0,0 +1,525 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import bpy
+
+from typing import Tuple, Optional, Sequence
+
+from bpy.types import PropertyGroup, Action, UIList, UILayout, Context, Panel, Operator, Armature
+from bpy.props import (EnumProperty, IntProperty, BoolProperty, StringProperty, FloatProperty,
+ PointerProperty, CollectionProperty)
+
+from .generic_ui_list import draw_ui_list
+
+from ..utils.naming import mirror_name
+from ..utils.action_layers import ActionSlotBase
+
+
+def get_action_slots(arm: Armature) -> Sequence['ActionSlot']:
+ # noinspection PyUnresolvedReferences
+ return arm.rigify_action_slots
+
+
+def get_action_slots_active(arm: Armature) -> tuple[Sequence['ActionSlot'], int]:
+ # noinspection PyUnresolvedReferences
+ return arm.rigify_action_slots, arm.rigify_active_action_slot
+
+
+def poll_trigger_action(_self, action):
+ """Whether an action can be used as a corrective action's trigger or not."""
+ armature_id_store = bpy.context.object.data
+ assert isinstance(armature_id_store, Armature)
+
+ slots, idx = get_action_slots_active(armature_id_store)
+
+ active_slot = slots[idx] if 0 <= idx < len(slots) else None
+
+ # If this action is the same as the active slot's action, don't show it.
+ if active_slot and action == active_slot.action:
+ return False
+
+ # If this action is used by any other action slot, show it.
+ for slot in slots:
+ if slot.action == action and not slot.is_corrective:
+ return True
+
+ return False
+
+
+class ActionSlot(PropertyGroup, ActionSlotBase):
+ action: PointerProperty(
+ name="Action",
+ type=Action,
+ description="Action to apply to the rig via constraints"
+ )
+
+ enabled: BoolProperty(
+ name="Enabled",
+ description="Create constraints for this action on the generated rig",
+ default=True
+ )
+
+ symmetrical: BoolProperty(
+ name="Symmetrical",
+ description="Apply the same setup but mirrored to the opposite side control, shown in "
+ "parentheses. Bones will only be affected by the control with the same side "
+ "(eg., .L bones will only be affected by the .L control). Bones without a "
+ "side in their name (so no .L or .R) will be affected by both controls "
+ "with 0.5 influence each",
+ default=True
+ )
+
+ subtarget: StringProperty(
+ name="Control Bone",
+ description="Select a bone on the generated rig which will drive this action"
+ )
+
+ transform_channel: EnumProperty(name="Transform Channel",
+ items=[("LOCATION_X", "X Location", "X Location"),
+ ("LOCATION_Y", "Y Location", "Y Location"),
+ ("LOCATION_Z", "Z Location", "Z Location"),
+ ("ROTATION_X", "X Rotation", "X Rotation"),
+ ("ROTATION_Y", "Y Rotation", "Y Rotation"),
+ ("ROTATION_Z", "Z Rotation", "Z Rotation"),
+ ("SCALE_X", "X Scale", "X Scale"),
+ ("SCALE_Y", "Y Scale", "Y Scale"),
+ ("SCALE_Z", "Z Scale", "Z Scale")],
+ description="Transform channel",
+ default="LOCATION_X")
+
+ target_space: EnumProperty(
+ name="Transform Space",
+ items=[("WORLD", "World Space", "World Space"),
+ ("POSE", "Pose Space", "Pose Space"),
+ ("LOCAL_WITH_PARENT", "Local With Parent", "Local With Parent"),
+ ("LOCAL", "Local Space", "Local Space")],
+ default="LOCAL"
+ )
+
+ def update_frame_start(self, _context):
+ if self.frame_start > self.frame_end:
+ self.frame_end = self.frame_start
+
+ frame_start: IntProperty(
+ name="Start Frame",
+ description="First frame of the action's timeline",
+ update=update_frame_start
+ )
+
+ def update_frame_end(self, _context):
+ if self.frame_end < self.frame_start:
+ self.frame_start = self.frame_end
+
+ frame_end: IntProperty(
+ name="End Frame",
+ default=2,
+ description="Last frame of the action's timeline",
+ update=update_frame_end
+ )
+
+ trans_min: FloatProperty(
+ name="Min",
+ default=-0.05,
+ description="Value that the transformation value must reach to put the action's timeline"
+ "to the first frame. Rotations are in degrees"
+ )
+
+ trans_max: FloatProperty(
+ name="Max",
+ default=0.05,
+ description="Value that the transformation value must reach to put the action's timeline"
+ "to the last frame. Rotations are in degrees"
+ )
+
+ is_corrective: BoolProperty(
+ name="Corrective",
+ description="Indicate that this is a corrective action. Corrective actions will activate"
+ "based on the activation of two other actions (using End Frame if both inputs"
+ "are at their End Frame, and Start Frame if either is at Start Frame)"
+ )
+
+ trigger_action_a: PointerProperty(
+ name="Trigger A",
+ type=Action,
+ description="Action whose activation will trigger the corrective action",
+ poll=poll_trigger_action
+ )
+
+ trigger_action_b: PointerProperty(
+ name="Trigger B",
+ description="Action whose activation will trigger the corrective action",
+ type=Action,
+ poll=poll_trigger_action
+ )
+
+ show_action_a: BoolProperty(name="Show Settings")
+ show_action_b: BoolProperty(name="Show Settings")
+
+
+def find_slot_by_action(metarig_data: Armature, action) -> Tuple[Optional[ActionSlot], int]:
+ """Find the ActionSlot in the rig which targets this action."""
+ if not action:
+ return None, -1
+
+ for i, slot in enumerate(get_action_slots(metarig_data)):
+ if slot.action == action:
+ return slot, i
+ else:
+ return None, -1
+
+
+def find_duplicate_slot(metarig_data: Armature, action_slot: ActionSlot) -> Optional[ActionSlot]:
+ """Find a different ActionSlot in the rig which has the same action."""
+
+ for slot in get_action_slots(metarig_data):
+ if slot.action == action_slot.action and slot != action_slot:
+ return slot
+
+ return None
+
+# =============================================
+# Operators
+
+
+class RIGIFY_OT_action_create(Operator):
+ """Create new Action"""
+ # This is needed because bpy.ops.action.new() has a poll function that blocks
+ # the operator unless it's drawn in an animation UI panel.
+
+ bl_idname = "object.rigify_action_create"
+ bl_label = "New"
+ bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
+
+ def execute(self, context):
+ armature_id_store = context.object.data
+ assert isinstance(armature_id_store, Armature)
+ action_slots, action_slot_idx = get_action_slots_active(armature_id_store)
+ action_slot = action_slots[action_slot_idx]
+ action = bpy.data.actions.new(name="Action")
+ action_slot.action = action
+ return {'FINISHED'}
+
+
+class RIGIFY_OT_jump_to_action_slot(Operator):
+ """Set Active Action Slot Index"""
+
+ bl_idname = "object.rigify_jump_to_action_slot"
+ bl_label = "Jump to Action Slot"
+ bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
+
+ to_index: IntProperty()
+
+ def execute(self, context):
+ armature_id_store = context.object.data
+ armature_id_store.rigify_active_action_slot = self.to_index
+ return {'FINISHED'}
+
+
+# =============================================
+# UI Panel
+
+class RIGIFY_UL_action_slots(UIList):
+ def draw_item(self, context: Context, layout: UILayout, data: Armature,
+ action_slot: ActionSlot, icon, active_data, active_propname: str,
+ slot_index: int = 0, flt_flag: int = 0):
+ action_slots, action_slot_idx = get_action_slots_active(data)
+ active_action = action_slots[action_slot_idx]
+
+ if self.layout_type in {'DEFAULT', 'COMPACT'}:
+ if action_slot.action:
+ row = layout.row()
+ icon = 'ACTION'
+
+ # Check if this action is a trigger for the active corrective action
+ if active_action.is_corrective and \
+ action_slot.action in [active_action.trigger_action_a,
+ active_action.trigger_action_b]:
+ icon = 'RESTRICT_INSTANCED_OFF'
+
+ # Check if the active action is a trigger for this corrective action.
+ if action_slot.is_corrective and \
+ active_action.action in [action_slot.trigger_action_a,
+ action_slot.trigger_action_b]:
+ icon = 'RESTRICT_INSTANCED_OFF'
+
+ row.prop(action_slot.action, 'name', text="", emboss=False, icon=icon)
+
+ # Highlight various errors
+
+ if find_duplicate_slot(data, action_slot):
+ # Multiple entries for the same action
+ row.alert = True
+ row.label(text="Duplicate", icon='ERROR')
+
+ elif action_slot.is_corrective:
+ text = "Corrective"
+ icon = 'RESTRICT_INSTANCED_OFF'
+
+ for trigger in [action_slot.trigger_action_a,
+ action_slot.trigger_action_b]:
+ trigger_slot, trigger_idx = find_slot_by_action(data, trigger)
+
+ # No trigger action set, no slot or invalid slot
+ if not trigger_slot or trigger_slot.is_corrective:
+ row.alert = True
+ text = "No Trigger Action"
+ icon = 'ERROR'
+ break
+
+ row.label(text=text, icon=icon)
+
+ else:
+ text = action_slot.subtarget
+ icon = 'BONE_DATA'
+
+ # noinspection PyUnresolvedReferences
+ target_rig: Object = data.rigify_target_rig
+
+ if not action_slot.subtarget:
+ row.alert = True
+ text = 'No Control Bone'
+ icon = 'ERROR'
+
+ elif target_rig:
+ # Check for bones not actually present in the generated rig
+ bones = target_rig.pose.bones
+
+ if action_slot.subtarget not in bones:
+ row.alert = True
+ text = 'Bad Control Bone'
+ icon = 'ERROR'
+ elif (action_slot.symmetrical
+ and mirror_name(action_slot.subtarget) not in bones):
+ row.alert = True
+ text = 'Bad Control Bone'
+ icon = 'ERROR'
+
+ row.label(text=text, icon=icon)
+
+ # noinspection SpellCheckingInspection
+ icon = 'CHECKBOX_HLT' if action_slot.enabled else 'CHECKBOX_DEHLT'
+ row.enabled = action_slot.enabled
+
+ layout.prop(action_slot, 'enabled', text="", icon=icon, emboss=False)
+
+ # No action
+ else:
+ layout.label(text="", translate=False, icon='ACTION')
+
+ elif self.layout_type in {'GRID'}:
+ layout.alignment = 'CENTER'
+ layout.label(text="", icon_value=icon)
+
+
+class DATA_PT_rigify_actions(Panel):
+ bl_space_type = 'PROPERTIES'
+ bl_region_type = 'WINDOW'
+ bl_context = 'data'
+ bl_label = "Actions"
+ bl_parent_id = "DATA_PT_rigify"
+ bl_options = {'DEFAULT_CLOSED'}
+
+ @classmethod
+ def poll(cls, context):
+ return context.object.mode in ('POSE', 'OBJECT')
+
+ def draw(self, context: Context):
+ armature_id_store = context.object.data
+ assert isinstance(armature_id_store, Armature)
+ action_slots, active_idx = get_action_slots_active(armature_id_store)
+
+ layout = self.layout
+ layout.use_property_split = True
+ layout.use_property_decorate = False
+
+ draw_ui_list(
+ layout, context,
+ class_name='RIGIFY_UL_action_slots',
+ list_context_path='object.data.rigify_action_slots',
+ active_idx_context_path='object.data.rigify_active_action_slot',
+ )
+
+ if len(action_slots) == 0:
+ return
+
+ active_slot = action_slots[active_idx]
+
+ layout.template_ID(active_slot, 'action', new=RIGIFY_OT_action_create.bl_idname)
+
+ if not active_slot.action:
+ return
+
+ layout = layout.column()
+ layout.prop(active_slot, 'is_corrective')
+
+ if active_slot.is_corrective:
+ self.draw_ui_corrective(context, active_slot)
+ else:
+ # noinspection PyUnresolvedReferences
+ target_rig = armature_id_store.rigify_target_rig
+ self.draw_slot_ui(layout, active_slot, target_rig)
+ self.draw_status(active_slot)
+
+ def draw_ui_corrective(self, context: Context, slot):
+ layout = self.layout
+
+ layout.prop(slot, 'frame_start', text="Frame Start")
+ layout.prop(slot, 'frame_end', text="End")
+ layout.separator()
+
+ for trigger_prop in ['trigger_action_a', 'trigger_action_b']:
+ self.draw_ui_trigger(context, slot, trigger_prop)
+
+ def draw_ui_trigger(self, context: Context, slot, trigger_prop: str):
+ layout = self.layout
+ metarig = context.object
+ assert isinstance(metarig.data, Armature)
+
+ trigger = getattr(slot, trigger_prop)
+ icon = 'ACTION' if trigger else 'ERROR'
+
+ row = layout.row()
+ row.prop(slot, trigger_prop, icon=icon)
+
+ if not trigger:
+ return
+
+ trigger_slot, slot_index = find_slot_by_action(metarig.data, trigger)
+
+ if not trigger_slot:
+ row = layout.split(factor=0.4)
+ row.separator()
+ row.alert = True
+ row.label(text="Action not in list", icon='ERROR')
+ return
+
+ show_prop_name = 'show_action_' + trigger_prop[-1]
+ show = getattr(slot, show_prop_name)
+ icon = 'HIDE_OFF' if show else 'HIDE_ON'
+
+ row.prop(slot, show_prop_name, icon=icon, text="")
+
+ op = row.operator(RIGIFY_OT_jump_to_action_slot.bl_idname, text="", icon='LOOP_FORWARDS')
+ op.to_index = slot_index
+
+ if show:
+ col = layout.column(align=True)
+ col.enabled = False
+ # noinspection PyUnresolvedReferences
+ target_rig = metarig.data.rigify_target_rig
+ self.draw_slot_ui(col, trigger_slot, target_rig)
+ col.separator()
+
+ @staticmethod
+ def draw_slot_ui(layout, slot, target_rig):
+ if not target_rig:
+ row = layout.row()
+ row.alert = True
+ row.label(text="Cannot verify bone name without a generated rig", icon='ERROR')
+
+ row = layout.row()
+
+ bone_icon = 'BONE_DATA' if slot.subtarget else 'ERROR'
+
+ if target_rig:
+ subtarget_exists = slot.subtarget in target_rig.pose.bones
+ row.prop_search(slot, 'subtarget', target_rig.pose, 'bones', icon=bone_icon)
+ row.alert = not subtarget_exists
+
+ if slot.subtarget and not subtarget_exists:
+ row = layout.split(factor=0.4)
+ row.column()
+ row.alert = True
+ row.label(text=f"Bone not found: {slot.subtarget}", icon='ERROR')
+ else:
+ row.prop(slot, 'subtarget', icon=bone_icon)
+
+ flipped_subtarget = mirror_name(slot.subtarget)
+
+ if flipped_subtarget != slot.subtarget:
+ flipped_subtarget_exists = not target_rig or flipped_subtarget in target_rig.pose.bones
+
+ row = layout.row()
+ row.use_property_split = True
+ row.prop(slot, 'symmetrical', text=f"Symmetrical ({flipped_subtarget})")
+
+ if slot.symmetrical and not flipped_subtarget_exists:
+ row.alert = True
+
+ row = layout.split(factor=0.4)
+ row.column()
+ row.alert = True
+ row.label(text=f"Bone not found: {flipped_subtarget}", icon='ERROR')
+
+ layout.prop(slot, 'frame_start', text="Frame Start")
+ layout.prop(slot, 'frame_end', text="End")
+
+ layout.prop(slot, 'target_space', text="Target Space")
+ layout.prop(slot, 'transform_channel', text="Transform Channel")
+
+ layout.prop(slot, 'trans_min')
+ layout.prop(slot, 'trans_max')
+
+ def draw_status(self, slot):
+ """
+ There are a lot of ways to create incorrect Action setups, so give
+ the user a warning in those cases.
+ """
+ layout = self.layout
+
+ split = layout.split(factor=0.4)
+ heading = split.row()
+ heading.alignment = 'RIGHT'
+ heading.label(text="Status:")
+
+ if slot.trans_min == slot.trans_max:
+ col = split.column(align=True)
+ col.alert = True
+ col.label(text="Min and max value are the same!")
+ col.label(text=f"Will be stuck reading frame {slot.frame_start}!")
+ return
+
+ if slot.frame_start == slot.frame_end:
+ col = split.column(align=True)
+ col.alert = True
+ col.label(text="Start and end frame cannot be the same!")
+
+ default_frame = slot.get_default_frame()
+
+ if slot.is_default_frame_integer():
+ split.label(text=f"Default Frame: {round(default_frame)}")
+ else:
+ split.alert = True
+ split.label(text=f"Default Frame: {round(default_frame, 2)} "
+ "(Should be a whole number!)")
+
+
+# =============================================
+# Registration
+
+classes = (
+ ActionSlot,
+ RIGIFY_OT_action_create,
+ RIGIFY_OT_jump_to_action_slot,
+ RIGIFY_UL_action_slots,
+ DATA_PT_rigify_actions,
+)
+
+
+def register():
+ from bpy.utils import register_class
+ for cls in classes:
+ register_class(cls)
+
+ bpy.types.Armature.rigify_action_slots = CollectionProperty(type=ActionSlot)
+ bpy.types.Armature.rigify_active_action_slot = IntProperty(min=0, default=0)
+
+
+def unregister():
+ from bpy.utils import unregister_class
+ for cls in classes:
+ unregister_class(cls)
+
+ # noinspection PyUnresolvedReferences
+ del bpy.types.Armature.rigify_action_slots
+ # noinspection PyUnresolvedReferences
+ del bpy.types.Armature.rigify_active_action_slot