Rigify: Full Action Slot Support in Blender 5.0
This commit tries to make the bare minimum changes to add a meaningful level of support for Action Slots in Rigify: - You can now select an Action Slot in each Action Set-up, and that will be assigned to the generated Action Constraints. - For this to be meaningful, we have to support selecting the same Action in multiple Action Set-ups, which would previously throw an error. - For that to be possible however, it was necessary to make the trigger selectors of Corrective Action Set-ups select one of the other set-ups directly, rather than selecting an Action (datablock), and then making an association to one of the action set-ups based on that action pointer. (The necessity to allow users to point at another action set-up was the reason behind not allowing user to use the same action datablock multiple times.) Pull Request: https://projects.blender.org/blender/blender/pulls/146182
This commit is contained in:
committed by
Sybren A. Stüvel
parent
76c03744a8
commit
f5428c51e0
@@ -859,6 +859,10 @@ def register_rna_properties() -> None:
|
||||
name="Rigify Owner Rig",
|
||||
description="Rig that owns this object and may delete or overwrite it upon re-generation")
|
||||
|
||||
# 5.0: Version metarigs to new Action Slot selector properties on file load.
|
||||
from .utils.action_layers import versioning_5_0
|
||||
bpy.app.handlers.load_post.append(versioning_5_0)
|
||||
|
||||
|
||||
def unregister_rna_properties() -> None:
|
||||
# Properties on PoseBones and Armature. (Annotated to suppress unknown attribute warnings.)
|
||||
|
||||
@@ -291,6 +291,10 @@ class Generator(base_generate.BaseGenerator):
|
||||
tar.data_path = "RIGIFY-" + tar.data_path
|
||||
|
||||
def __rename_org_bones(self, obj: ArmatureObject):
|
||||
# Clear any assigned Action, so we don't rename fcurves when renaming ORG- bones.
|
||||
if obj.animation_data:
|
||||
obj.animation_data.action = None
|
||||
|
||||
# Make a list of the original bones, so we can keep track of them.
|
||||
original_bones = [bone.name for bone in obj.data.bones]
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
import random
|
||||
|
||||
from typing import Tuple, Optional, Sequence, Any
|
||||
from typing import Sequence, Any
|
||||
|
||||
from bpy.types import PropertyGroup, Action, UIList, UILayout, Context, Panel, Operator, Armature
|
||||
from bpy.types import PropertyGroup, Action, UIList, UILayout, Context, Panel, Operator, Armature, ActionSlot
|
||||
from bpy.props import (EnumProperty, IntProperty, BoolProperty, StringProperty, FloatProperty,
|
||||
PointerProperty, CollectionProperty)
|
||||
from bpy.app.translations import (
|
||||
@@ -50,13 +51,115 @@ def poll_trigger_action(_self, action):
|
||||
return False
|
||||
|
||||
|
||||
def get_first_compatible_action_slot(action: Action, id_type: str) -> ActionSlot | None:
|
||||
for slot in action.slots:
|
||||
if slot.target_id_type in ('UNSPECIFIED', id_type):
|
||||
return slot
|
||||
return None
|
||||
|
||||
|
||||
class ActionSlot(PropertyGroup, ActionSlotBase):
|
||||
def on_action_update(self, context):
|
||||
if not self.action:
|
||||
return
|
||||
|
||||
# We must trigger the lazy-initialization of the unique_id before
|
||||
# any UI code tries to access it, since if it tried to lazy-initialize
|
||||
# during UI drawing, that would result in an error.
|
||||
self.ensure_unique_id()
|
||||
|
||||
if self.action_slot:
|
||||
# Nothing else to do here.
|
||||
return
|
||||
|
||||
# Set the first compatible slot if none already set. However, be careful
|
||||
# to prevent infinite loops, as this will call this function again.
|
||||
first_slot = get_first_compatible_action_slot(self.action, 'OBJECT')
|
||||
if first_slot:
|
||||
# Only write when not None, to prevent looping infinitely.
|
||||
self.action_slot = first_slot
|
||||
|
||||
action: PointerProperty(
|
||||
name="Action",
|
||||
type=Action,
|
||||
description="Action to apply to the rig via constraints"
|
||||
description="Action to apply to the rig via constraints",
|
||||
update=on_action_update,
|
||||
)
|
||||
|
||||
def slot_name_from_handle(self, slot_handle_as_str: str, _is_set: bool) -> str:
|
||||
"""This is a get_transform callback function, see Blender 5.0 PyAPI docs."""
|
||||
if not slot_handle_as_str:
|
||||
return ""
|
||||
slot_handle = int(slot_handle_as_str)
|
||||
action_slot = next((s for s in self.action.slots if s.handle == slot_handle), None)
|
||||
if not action_slot:
|
||||
return ""
|
||||
# We use the display name rather than the identifier because in Rigify's context,
|
||||
# we don't care about the datablock type prefix found in the identifier,
|
||||
# since our action slots are always for Objects.
|
||||
return action_slot.name_display
|
||||
|
||||
def slot_name_to_handle(self, new_name: str, _current_name: str, _is_set: bool) -> str:
|
||||
"""This is a set_transform callback function, see Blender 5.0 PyAPI docs."""
|
||||
action_slot = self.action.slots.get("OB" + new_name)
|
||||
if not action_slot:
|
||||
return ""
|
||||
return str(action_slot.handle)
|
||||
|
||||
action_slot_ui: StringProperty(
|
||||
name="Acion Slot",
|
||||
description="Slot of the Action to use for the Action Constraints",
|
||||
# These callbacks let us store the action slot's `handle` property
|
||||
# under the hood (which is unique and never changes), while acting
|
||||
# as a user-friendly display name in the UI.
|
||||
get_transform=slot_name_from_handle,
|
||||
set_transform=slot_name_to_handle,
|
||||
update=on_action_update,
|
||||
)
|
||||
|
||||
unique_id: IntProperty(default=0)
|
||||
|
||||
def ensure_unique_id(self) -> int:
|
||||
if self.unique_id:
|
||||
return self.unique_id
|
||||
|
||||
# IDProperties only support signed 32-bit integers, so this is the
|
||||
# biggest pool of random numbers we can pick from.
|
||||
unique_id = random.randint(0, 2**31 - 1)
|
||||
self.unique_id = unique_id
|
||||
return unique_id
|
||||
|
||||
@property
|
||||
def action_slot(self) -> ActionSlot | None:
|
||||
return self.action.slots.get("OB" + self.action_slot_ui)
|
||||
|
||||
@action_slot.setter
|
||||
def action_slot(self, slot: ActionSlot):
|
||||
"""For convenience, caller can assign an Action Slot,
|
||||
even though under the hood we'll actually be storing the slot handle.
|
||||
"""
|
||||
# We don't actually assign the handle directly, since we have
|
||||
# the action_slot_ui wrapper property, which masks the handle for us.
|
||||
self.action_slot_ui = slot.name_display if slot else ""
|
||||
|
||||
def get_name_transform(self) -> str:
|
||||
"""Return a useful display name for this Rigify action set-up,
|
||||
consisting of the Action name and the slot name, with a little arrow inbetween.
|
||||
|
||||
The latter is omitted when the Action has only a single slot, to be less cluttered
|
||||
for users who prefer to use separate Actions,
|
||||
and for legacy rigs where all slots are named "Legacy Slot".
|
||||
"""
|
||||
if not self.action:
|
||||
return str(self.unique_id)
|
||||
|
||||
if self.action_slot and len(self.action.slots) > 1:
|
||||
return f"{self.action.name} ➔ {self.action_slot.name_display}"
|
||||
|
||||
return self.action.name
|
||||
|
||||
name: StringProperty(get=get_name_transform)
|
||||
|
||||
enabled: BoolProperty(
|
||||
name="Enabled",
|
||||
description="Create constraints for this action on the generated rig",
|
||||
@@ -135,6 +238,8 @@ class ActionSlot(PropertyGroup, ActionSlotBase):
|
||||
"to the last frame. Rotations are in degrees"
|
||||
)
|
||||
|
||||
# Corrective Action properties
|
||||
|
||||
is_corrective: BoolProperty(
|
||||
name="Corrective",
|
||||
description="Indicate that this is a corrective action. Corrective actions will activate "
|
||||
@@ -142,46 +247,63 @@ class ActionSlot(PropertyGroup, ActionSlotBase):
|
||||
"are at their End Frame, and Start Frame if either is at Start Frame)"
|
||||
)
|
||||
|
||||
trigger_action_a: PointerProperty(
|
||||
def setup_id_to_str(self, unique_id_as_str: str, _is_set: bool) -> str:
|
||||
"""This is a get_transform callback function, see Blender 5.0 PyAPI docs."""
|
||||
if not unique_id_as_str:
|
||||
return ""
|
||||
unique_id = int(unique_id_as_str)
|
||||
action_setups = self.id_data.rigify_action_slots
|
||||
action_setup = next((setup for setup in action_setups if setup.unique_id == unique_id), None)
|
||||
if not action_setup:
|
||||
return ""
|
||||
return action_setup.name
|
||||
|
||||
def setup_name_to_id(self, name: str, _curr_value: str, _is_set: bool) -> str:
|
||||
"""This is a set_transform callback function, see Blender 5.0 PyAPI docs."""
|
||||
action_setups = self.id_data.rigify_action_slots
|
||||
action_setup = next((setup for setup in action_setups if setup.name == name), None)
|
||||
if not action_setup:
|
||||
return ""
|
||||
return str(action_setup.unique_id)
|
||||
|
||||
trigger_select_a: StringProperty(
|
||||
name="Trigger A",
|
||||
type=Action,
|
||||
description="Action whose activation will trigger the corrective action",
|
||||
poll=poll_trigger_action
|
||||
description="Action Set-up whose activation will trigger this set-up as a corrective",
|
||||
get_transform=setup_id_to_str,
|
||||
set_transform=setup_name_to_id,
|
||||
)
|
||||
trigger_select_b: StringProperty(
|
||||
name="Trigger B",
|
||||
description="Action Set-up whose activation will trigger this set-up as a corrective",
|
||||
# These callbacks let us store the trigger action setups' `unique_id` property
|
||||
# under the hood (which is unique and never changes), while acting as
|
||||
# a user-friendly display name in the UI.
|
||||
get_transform=setup_id_to_str,
|
||||
set_transform=setup_name_to_id,
|
||||
)
|
||||
|
||||
trigger_action_b: PointerProperty(
|
||||
name="Trigger B",
|
||||
description="Action whose activation will trigger the corrective action",
|
||||
type=Action,
|
||||
poll=poll_trigger_action
|
||||
)
|
||||
@property
|
||||
def trigger_a(self):
|
||||
action_setups = self.id_data.rigify_action_slots
|
||||
return next((setup for setup in action_setups if setup.name == self.trigger_select_a), None)
|
||||
|
||||
@trigger_a.setter
|
||||
def trigger_a(self, action_setup):
|
||||
self.trigger_select_a = action_setup.name if action_setup else ""
|
||||
|
||||
@property
|
||||
def trigger_b(self):
|
||||
action_setups = self.id_data.rigify_action_slots
|
||||
return next((setup for setup in action_setups if setup.name == self.trigger_select_b), None)
|
||||
|
||||
@trigger_b.setter
|
||||
def trigger_b(self, action_setup):
|
||||
self.trigger_select_b = action_setup.name if action_setup else ""
|
||||
|
||||
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
|
||||
|
||||
@@ -213,11 +335,19 @@ class RIGIFY_OT_jump_to_action_slot(Operator):
|
||||
bl_label = "Jump to Action Slot"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
to_index: IntProperty()
|
||||
to_unique_id: IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
armature_id_store = context.object.data
|
||||
armature_id_store.rigify_active_action_slot = self.to_index
|
||||
for i, action_setup in enumerate(armature_id_store.rigify_action_slots):
|
||||
if action_setup.unique_id == self.to_unique_id:
|
||||
armature_id_store.rigify_active_action_slot = i
|
||||
break
|
||||
else:
|
||||
self.report({'ERROR'}, "Failed to find Action Slot.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, f'Set active action set-up index to {i}.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
@@ -238,35 +368,26 @@ class RIGIFY_UL_action_slots(UIList):
|
||||
|
||||
# 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]:
|
||||
action_slot in (active_action.trigger_a,
|
||||
active_action.trigger_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]:
|
||||
active_action in [action_slot.trigger_a,
|
||||
action_slot.trigger_b]:
|
||||
icon = 'RESTRICT_INSTANCED_OFF'
|
||||
|
||||
row.prop(action_slot.action, 'name', text="", emboss=False, icon=icon)
|
||||
row.label(text=action_slot.name, 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:
|
||||
if 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)
|
||||
|
||||
for trigger in (action_slot.trigger_a, action_slot.trigger_b):
|
||||
# No trigger action set, no slot or invalid slot
|
||||
if not trigger_slot or trigger_slot.is_corrective:
|
||||
if not trigger or trigger.is_corrective:
|
||||
row.alert = True
|
||||
text = "No Trigger Action"
|
||||
icon = 'ERROR'
|
||||
@@ -311,7 +432,6 @@ class RIGIFY_UL_action_slots(UIList):
|
||||
layout.label(text="", translate=False, icon='ACTION')
|
||||
|
||||
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
class DATA_PT_rigify_actions(Panel):
|
||||
bl_space_type = 'PROPERTIES'
|
||||
@@ -344,22 +464,28 @@ class DATA_PT_rigify_actions(Panel):
|
||||
if len(action_slots) == 0:
|
||||
return
|
||||
|
||||
active_slot = action_slots[active_idx]
|
||||
active_action_setup = action_slots[active_idx]
|
||||
|
||||
layout.template_ID(active_slot, 'action', new=RIGIFY_OT_action_create.bl_idname)
|
||||
|
||||
if not active_slot.action:
|
||||
col = layout.column(align=True)
|
||||
col.template_ID(active_action_setup, 'action', new=RIGIFY_OT_action_create.bl_idname)
|
||||
if not active_action_setup.action:
|
||||
return
|
||||
if not active_action_setup.action.slots:
|
||||
layout.alert = True
|
||||
layout.label(text="No slots in this Action.")
|
||||
return
|
||||
|
||||
layout = layout.column()
|
||||
layout.prop(active_slot, 'is_corrective')
|
||||
col.prop_search(active_action_setup, "action_slot_ui", active_action_setup.action, 'slots', text="")
|
||||
|
||||
if active_slot.is_corrective:
|
||||
self.draw_ui_corrective(context, active_slot)
|
||||
layout = layout.column()
|
||||
layout.prop(active_action_setup, 'is_corrective')
|
||||
|
||||
if active_action_setup.is_corrective:
|
||||
self.draw_ui_corrective(context, active_action_setup)
|
||||
else:
|
||||
target_rig = get_rigify_target_rig(armature_id_store)
|
||||
self.draw_slot_ui(layout, active_slot, target_rig)
|
||||
self.draw_status(active_slot)
|
||||
self.draw_slot_ui(layout, active_action_setup, target_rig)
|
||||
self.draw_status(active_action_setup)
|
||||
|
||||
def draw_ui_corrective(self, context: Context, slot):
|
||||
layout = self.layout
|
||||
@@ -368,7 +494,7 @@ class DATA_PT_rigify_actions(Panel):
|
||||
layout.prop(slot, 'frame_end', text="End")
|
||||
layout.separator()
|
||||
|
||||
for trigger_prop in ['trigger_action_a', 'trigger_action_b']:
|
||||
for trigger_prop in ['trigger_select_a', 'trigger_select_b']:
|
||||
self.draw_ui_trigger(context, slot, trigger_prop)
|
||||
|
||||
def draw_ui_trigger(self, context: Context, slot, trigger_prop: str):
|
||||
@@ -376,18 +502,17 @@ class DATA_PT_rigify_actions(Panel):
|
||||
metarig = context.object
|
||||
assert isinstance(metarig.data, Armature)
|
||||
|
||||
trigger = getattr(slot, trigger_prop)
|
||||
trigger = getattr(slot, trigger_prop.replace("select_", ""))
|
||||
icon = 'ACTION' if trigger else 'ERROR'
|
||||
|
||||
row = layout.row()
|
||||
row.prop(slot, trigger_prop, icon=icon)
|
||||
try:
|
||||
active_slot = metarig.data.rigify_action_slots[metarig.data.rigify_active_action_slot]
|
||||
except IndexError:
|
||||
return
|
||||
row.prop_search(active_slot, trigger_prop, metarig.data, 'rigify_action_slots', 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
|
||||
@@ -401,13 +526,13 @@ class DATA_PT_rigify_actions(Panel):
|
||||
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
|
||||
op.to_unique_id = trigger.unique_id
|
||||
|
||||
if show:
|
||||
col = layout.column(align=True)
|
||||
col.enabled = False
|
||||
target_rig = get_rigify_target_rig(metarig.data)
|
||||
self.draw_slot_ui(col, trigger_slot, target_rig)
|
||||
self.draw_slot_ui(col, trigger, target_rig)
|
||||
col.separator()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
from typing import Optional, List, Dict, Tuple, TYPE_CHECKING
|
||||
from bpy.types import Action, Mesh, Armature
|
||||
from bpy.types import Action, Mesh, Armature, ActionChannelbag
|
||||
from bpy.types import ActionSlot as BlenderActionSlot
|
||||
|
||||
from bl_math import clamp
|
||||
from bpy_extras import anim_utils
|
||||
|
||||
@@ -28,6 +32,7 @@ class ActionSlotBase:
|
||||
"""Abstract non-RNA base for the action list slots."""
|
||||
|
||||
action: Optional[Action]
|
||||
action_slot: Optional[BlenderActionSlot]
|
||||
enabled: bool
|
||||
symmetrical: bool
|
||||
subtarget: str
|
||||
@@ -38,8 +43,8 @@ class ActionSlotBase:
|
||||
trans_min: float
|
||||
trans_max: float
|
||||
is_corrective: bool
|
||||
trigger_action_a: Optional[Action]
|
||||
trigger_action_b: Optional[Action]
|
||||
trigger_a: Optional[ActionSlotBase]
|
||||
trigger_b: Optional[ActionSlotBase]
|
||||
|
||||
############################################
|
||||
# Action Constraint Setup
|
||||
@@ -49,10 +54,7 @@ class ActionSlotBase:
|
||||
"""Return a list of bone names that have keyframes in the Action of this Slot."""
|
||||
keyed_bones = []
|
||||
|
||||
# This will get updated to actually support slots properly in #146182. For
|
||||
# now, just pick the first suitable action slot.
|
||||
action_slot = anim_utils.action_get_first_suitable_slot(self.action, 'OBJECT')
|
||||
channelbag = anim_utils.action_get_channelbag_for_slot(self.action, action_slot)
|
||||
channelbag = anim_utils.action_get_channelbag_for_slot(self.action, self.action_slot)
|
||||
if not channelbag:
|
||||
return []
|
||||
|
||||
@@ -150,7 +152,7 @@ class GeneratedActionSlot(ActionSlotBase):
|
||||
def __init__(self, action, *, enabled=True, symmetrical=True, subtarget='',
|
||||
transform_channel='LOCATION_X', target_space='LOCAL', frame_start=0,
|
||||
frame_end=2, trans_min=-0.05, trans_max=0.05, is_corrective=False,
|
||||
trigger_action_a=None, trigger_action_b=None):
|
||||
trigger_a=None, trigger_b=None):
|
||||
self.action = action
|
||||
self.enabled = enabled
|
||||
self.symmetrical = symmetrical
|
||||
@@ -162,8 +164,8 @@ class GeneratedActionSlot(ActionSlotBase):
|
||||
self.trans_min = trans_min
|
||||
self.trans_max = trans_max
|
||||
self.is_corrective = is_corrective
|
||||
self.trigger_action_a = trigger_action_a
|
||||
self.trigger_action_b = trigger_action_b
|
||||
self.trigger_a = trigger_a
|
||||
self.trigger_b = trigger_b
|
||||
|
||||
|
||||
class ActionLayer(RigComponent):
|
||||
@@ -186,8 +188,8 @@ class ActionLayer(RigComponent):
|
||||
self.use_trigger = False
|
||||
|
||||
if slot.is_corrective:
|
||||
trigger_a = self.owner.action_map[slot.trigger_action_a.name]
|
||||
trigger_b = self.owner.action_map[slot.trigger_action_b.name]
|
||||
trigger_a = self.owner.action_map[slot.trigger_a.name]
|
||||
trigger_b = self.owner.action_map[slot.trigger_b.name]
|
||||
|
||||
self.trigger_a = trigger_a.get(side) or trigger_a.get(Side.MIDDLE)
|
||||
self.trigger_b = trigger_b.get(side) or trigger_b.get(Side.MIDDLE)
|
||||
@@ -207,7 +209,7 @@ class ActionLayer(RigComponent):
|
||||
return self.slot.is_corrective or self.use_trigger
|
||||
|
||||
def _get_name(self):
|
||||
name = self.slot.action.name
|
||||
name = self.slot.name
|
||||
|
||||
if self.side == Side.LEFT:
|
||||
name += ".L"
|
||||
@@ -241,7 +243,7 @@ class ActionLayer(RigComponent):
|
||||
|
||||
def rig_bones(self):
|
||||
if self.slot.is_corrective and self.use_trigger:
|
||||
raise MetarigError(f"Corrective action used as trigger: {self.slot.action.name}")
|
||||
raise MetarigError(f"Corrective action used as trigger: {self.slot.name}")
|
||||
|
||||
if self.use_property:
|
||||
self.rig_input_driver(self.owner.property_bone, quote_property(self.name))
|
||||
@@ -265,6 +267,7 @@ class ActionLayer(RigComponent):
|
||||
insert_index=0,
|
||||
use_eval_time=True,
|
||||
action=self.slot.action,
|
||||
action_slot=self.slot.action_slot,
|
||||
frame_start=self.slot.frame_start,
|
||||
frame_end=self.slot.frame_end,
|
||||
mix_mode='BEFORE_SPLIT',
|
||||
@@ -303,7 +306,7 @@ class ActionLayer(RigComponent):
|
||||
|
||||
if control_name not in self.obj.pose.bones:
|
||||
raise MetarigError(
|
||||
f"Control bone '{control_name}' for action '{self.slot.action.name}' not found")
|
||||
f"Control bone '{control_name}' for action '{self.slot.name}' not found")
|
||||
|
||||
channel = self.slot.transform_channel\
|
||||
.replace("LOCATION", "LOC").replace("ROTATION", "ROT")
|
||||
@@ -364,39 +367,38 @@ class ActionLayerBuilder(GeneratorPlugin, BoneUtilityMixin, MechanismUtilityMixi
|
||||
# Constraints will be added in reverse order because each one is added to the top
|
||||
# of the stack when created. However, Before Original reverses the effective
|
||||
# order of transformations again, restoring the original sequence.
|
||||
for act_slot in self.sort_slots(action_slots):
|
||||
for act_slot in self.sort_action_setups(action_slots):
|
||||
self.spawn_slot_layers(act_slot)
|
||||
|
||||
@staticmethod
|
||||
def sort_slots(slots: List[ActionSlotBase]):
|
||||
indices = {slot.action.name: i for i, slot in enumerate(slots)}
|
||||
def sort_action_setups(action_setups: list[ActionSlotBase]):
|
||||
indices = {action_setup.unique_id: i for i, action_setup in enumerate(action_setups)}
|
||||
|
||||
def action_key(action: Action):
|
||||
return indices.get(action.name, -1) if action else -1
|
||||
def action_key(action_setup: ActionSlotBase) -> int:
|
||||
return indices.get(action_setup.unique_id, -1)
|
||||
|
||||
def slot_key(slot: ActionSlotBase):
|
||||
# Ensure corrective actions are added after their triggers.
|
||||
if slot.is_corrective:
|
||||
return max(action_key(slot.action),
|
||||
action_key(slot.trigger_action_a) + 0.5,
|
||||
action_key(slot.trigger_action_b) + 0.5)
|
||||
def action_setup_key(action_setup: ActionSlotBase) -> float:
|
||||
# Ensure corrective actions are added AFTER their triggers.
|
||||
if action_setup.is_corrective:
|
||||
return max(
|
||||
action_key(action_setup),
|
||||
action_key(action_setup.trigger_a) + 0.5,
|
||||
action_key(action_setup.trigger_b) + 0.5,
|
||||
)
|
||||
else:
|
||||
return action_key(slot.action)
|
||||
return action_key(action_setup)
|
||||
|
||||
return sorted(slots, key=slot_key)
|
||||
return sorted(action_setups, key=action_setup_key)
|
||||
|
||||
def spawn_slot_layers(self, act_slot):
|
||||
name = act_slot.action.name
|
||||
|
||||
if name in self.action_map:
|
||||
raise MetarigError(f"Action slot with duplicate action: {name}")
|
||||
name = act_slot.name
|
||||
|
||||
if act_slot.is_corrective:
|
||||
if not act_slot.trigger_action_a or not act_slot.trigger_action_b:
|
||||
if not act_slot.trigger_a or not act_slot.trigger_b:
|
||||
raise MetarigError(f"Action slot has missing triggers: {name}")
|
||||
|
||||
trigger_a = self.action_map.get(act_slot.trigger_action_a.name)
|
||||
trigger_b = self.action_map.get(act_slot.trigger_action_b.name)
|
||||
trigger_a = self.action_map.get(act_slot.trigger_a.name)
|
||||
trigger_b = self.action_map.get(act_slot.trigger_b.name)
|
||||
|
||||
if not trigger_a or not trigger_b:
|
||||
raise MetarigError(f"Action slot references missing trigger slot(s): {name}")
|
||||
@@ -423,7 +425,30 @@ class ActionLayerBuilder(GeneratorPlugin, BoneUtilityMixin, MechanismUtilityMixi
|
||||
def rig_bones(self):
|
||||
if self.layers:
|
||||
self.child_meshes = [
|
||||
verify_mesh_obj(child)
|
||||
for child in self.generator.obj.children_recursive
|
||||
if child.type == 'MESH'
|
||||
verify_mesh_obj(child) for child in self.generator.obj.children_recursive if child.type == 'MESH'
|
||||
]
|
||||
|
||||
|
||||
@bpy.app.handlers.persistent
|
||||
def versioning_5_0(_):
|
||||
"""This is a load_post handler, registered in the top-most level __init__.py."""
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != 'ARMATURE' or obj.library:
|
||||
# We only care about armatures, which are local to this file.
|
||||
continue
|
||||
for action_setup in obj.data.rigify_action_slots:
|
||||
if not action_setup.action:
|
||||
continue
|
||||
action_setup.action_slot = next(
|
||||
(s for s in action_setup.action.slots if s.target_id_type in ('UNSPECIFIED', 'OBJECT')), None
|
||||
)
|
||||
sys_props = action_setup.bl_system_properties_get()
|
||||
for prop_name in ('trigger_action_a', 'trigger_action_b'):
|
||||
trigger_action = sys_props.get(prop_name, None)
|
||||
if not trigger_action:
|
||||
continue
|
||||
trigger_action_setup = next(
|
||||
(setup for setup in obj.data.rigify_action_slots if setup.action == trigger_action)
|
||||
)
|
||||
setattr(action_setup, prop_name.replace("_action", ""), trigger_action_setup)
|
||||
del sys_props[prop_name]
|
||||
|
||||
Reference in New Issue
Block a user