Anim: add Action Slot selector to Action Constraint

Add slotted Actions support to Action constraints.

The user interface can be improved once #127751 lands.

Ref: #120406
Pull Request: https://projects.blender.org/blender/blender/pulls/127749
This commit is contained in:
Sybren A. Stüvel
2024-09-20 08:07:15 +02:00
parent e37f5616f5
commit 615cb46412
9 changed files with 263 additions and 29 deletions

View File

@@ -731,7 +731,28 @@ class ANIM_OT_slot_unassign_from_id(Operator):
return {'FINISHED'}
class ANIM_OT_slot_unassign_from_nla_strip(Operator):
class generic_slot_unassign_mixin():
context_property_name = ""
"""Which context attribute to use to get the to-be-manipulated data-block."""
@classmethod
def poll(cls, context):
slot_user = getattr(context, cls.context_property_name, None)
if not slot_user:
return False
if not slot_user.action_slot:
cls.poll_message_set("No Action slot is assigned, so there is nothing to un-assign")
return False
return True
def execute(self, context):
slot_user = getattr(context, self.context_property_name, None)
slot_user.action_slot = None
return {'FINISHED'}
class ANIM_OT_slot_unassign_from_nla_strip(generic_slot_unassign_mixin, Operator):
"""Un-assign the assigned Action Slot from an NLA strip.
Note that _which_ NLA strip should get this slot unassigned must be set in
@@ -744,21 +765,23 @@ class ANIM_OT_slot_unassign_from_nla_strip(Operator):
bl_description = "Un-assign the action slot from this NLA strip, effectively making it non-animated"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
nla_strip = getattr(context, "nla_strip", None)
if not nla_strip:
return False
context_property_name = "nla_strip"
if not nla_strip.action or not nla_strip.action_slot:
cls.poll_message_set("This NLA strip has no Action slot assigned")
return False
return True
def execute(self, context):
nla_strip = getattr(context, "nla_strip", None)
nla_strip.action_slot = None
return {'FINISHED'}
class ANIM_OT_slot_unassign_from_constraint(generic_slot_unassign_mixin, Operator):
"""Un-assign the assigned Action Slot from an Action constraint.
Note that _which_ constraint should get this slot unassigned must be set in
the "constraint" context pointer, using:
>>> layout.context_pointer_set("constraint", constraint)
"""
bl_idname = "anim.slot_unassign_from_constraint"
bl_label = "Unassign Slot"
bl_description = "Un-assign the action slot from this constraint"
bl_options = {'REGISTER', 'UNDO'}
context_property_name = "constraint"
classes = (
@@ -773,4 +796,5 @@ classes = (
ANIM_OT_slot_new_for_id,
ANIM_OT_slot_unassign_from_id,
ANIM_OT_slot_unassign_from_nla_strip,
ANIM_OT_slot_unassign_from_constraint,
)

View File

@@ -1126,7 +1126,17 @@ class ConstraintButtonsSubPanel:
layout.use_property_split = True
layout.use_property_decorate = True
layout.prop(con, "action")
col = layout.column(align=True)
col.prop(con, "action")
if context.preferences.experimental.use_animation_baklava and con.action and con.action.is_action_layered:
col.context_pointer_set("animated_id", con.id_data)
col.template_search(
con, "action_slot",
con, "action_slots",
new="", # No use in making a new slot here.
unlink="anim.slot_unassign_from_constraint",
)
layout.prop(con, "use_bone_object_action")
col = layout.column(align=True)

View File

@@ -14,6 +14,8 @@
#include "BKE_anim_data.hh"
#include "BKE_nla.hh"
#include "DNA_constraint_types.h"
namespace blender::animrig {
void action_foreach_fcurve(Action &action,
@@ -73,6 +75,48 @@ bool foreach_action_slot_use(
}
}
/* The rest of the code deals with constraints, so only relevant when this is an Object. */
if (GS(animated_id.name) != ID_OB) {
return true;
}
const Object &object = reinterpret_cast<const Object &>(animated_id);
/**
* Visit a constraint, and call the callback if it's an Action constraint.
*
* \returns whether to continue looping over possible uses of Actions, i.e.
* the return value of the callback.
*/
auto visit_constraint = [&](const bConstraint &constraint) -> bool {
if (constraint.type != CONSTRAINT_TYPE_ACTION) {
return true;
}
bActionConstraint *constraint_data = static_cast<bActionConstraint *>(constraint.data);
if (!constraint_data->act) {
return true;
}
return callback(constraint_data->act->wrap(), constraint_data->action_slot_handle);
};
/* Visit Object constraints. */
LISTBASE_FOREACH (bConstraint *, con, &object.constraints) {
if (!visit_constraint(*con)) {
return false;
}
}
/* Visit Pose Bone constraints. */
if (object.type == OB_ARMATURE) {
LISTBASE_FOREACH (bPoseChannel *, pchan, &object.pose->chanbase) {
LISTBASE_FOREACH (bConstraint *, con, &pchan->constraints) {
if (!visit_constraint(*con)) {
return false;
}
}
}
}
return true;
}

View File

@@ -854,7 +854,7 @@ void animsys_evaluate_action_group(PointerRNA *ptr,
const auto visit_fcurve = [&](FCurve *fcu) {
/* check if this curve should be skipped */
if ((fcu->flag & (FCURVE_MUTED | FCURVE_DISABLED)) == 0 && !BKE_fcurve_is_empty(fcu)) {
if ((fcu->flag & FCURVE_MUTED) == 0 && !BKE_fcurve_is_empty(fcu)) {
PathResolvedRNA anim_rna;
if (BKE_animsys_rna_path_resolve(ptr, fcu->rna_path, fcu->array_index, &anim_rna)) {
const float curval = calculate_fcurve(&anim_rna, fcu, anim_eval_context);

View File

@@ -2982,18 +2982,19 @@ static void actcon_get_tarmat(Depsgraph *depsgraph,
(cob->pchan) ? cob->pchan->name : nullptr);
}
/* TODO: add an action slot selector to the constraint settings. */
const blender::animrig::slot_handle_t slot_handle = blender::animrig::first_slot_handle(
*data->act);
/* Get the appropriate information from the action */
if (cob->type == CONSTRAINT_OBTYPE_OBJECT || (data->flag & ACTCON_BONE_USE_OBJECT_ACTION)) {
Object workob;
/* evaluate using workob */
/* FIXME: we don't have any consistent standards on limiting effects on object... */
what_does_obaction(
cob->ob, &workob, nullptr, data->act, slot_handle, nullptr, &anim_eval_context);
what_does_obaction(cob->ob,
&workob,
nullptr,
data->act,
data->action_slot_handle,
nullptr,
&anim_eval_context);
BKE_object_to_mat4(&workob, ct->matrix);
}
else if (cob->type == CONSTRAINT_OBTYPE_BONE) {
@@ -3010,8 +3011,13 @@ static void actcon_get_tarmat(Depsgraph *depsgraph,
tchan->rotmode = pchan->rotmode;
/* evaluate action using workob (it will only set the PoseChannel in question) */
what_does_obaction(
cob->ob, &workob, &pose, data->act, slot_handle, pchan->name, &anim_eval_context);
what_does_obaction(cob->ob,
&workob,
&pose,
data->act,
data->action_slot_handle,
pchan->name,
&anim_eval_context);
/* convert animation to matrices for use here */
BKE_pchan_calc_mat(tchan);
@@ -6673,3 +6679,11 @@ void BKE_constraint_blend_read_data(BlendDataReader *reader, ID *id_owner, ListB
}
}
}
/* Some static asserts to ensure that the bActionConstraint data is using the expected types for
* some of the fields. This check is done here instead of in DNA_constraint_types.h to avoid the
* inclusion of an DNA_anim_types.h in DNA_constraint_types.h just for this assert. */
static_assert(
std::is_same_v<decltype(ActionSlot::handle), decltype(bActionConstraint::action_slot_handle)>);
static_assert(
std::is_same_v<decltype(ActionSlot::name), decltype(bActionConstraint::action_slot_name)>);

View File

@@ -357,10 +357,23 @@ static void test_constraint(
/* must have action */
con->flag |= CONSTRAINT_DISABLE;
}
else if (data->act->idroot != ID_OB) {
/* only object-rooted actions can be used */
data->act = nullptr;
con->flag |= CONSTRAINT_DISABLE;
else {
animrig::Action &action = data->act->wrap();
if (action.is_action_legacy()) {
if (data->act->idroot != ID_OB) {
/* Only object-rooted actions can be used. */
data->act = nullptr;
con->flag |= CONSTRAINT_DISABLE;
}
}
else {
/* The slot was assigned, so assume that it is suitable to animate the
* owner (only suitable slots appear in the drop-down). */
animrig::Slot *slot = action.slot_for_handle(data->action_slot_handle);
if (!slot) {
con->flag |= CONSTRAINT_DISABLE;
}
}
}
/* Skip target checking if we're not using it */

View File

@@ -335,6 +335,9 @@ typedef struct bActionConstraint {
char _pad[3];
float eval_time; /* Only used when flag ACTCON_USE_EVAL_TIME is set. */
struct bAction *act;
int32_t action_slot_handle;
char action_slot_name[66]; /* MAX_ID_NAME */
char _pad1[2];
/** MAX_ID_NAME-2. */
char subtarget[64];
} bActionConstraint;

View File

@@ -29,6 +29,11 @@
#include "ED_object.hh"
#ifdef WITH_ANIM_BAKLAVA
# include "ANIM_action.hh"
# include "rna_action_tools.hh"
#endif
/* Please keep the names in sync with `constraint.cc`. */
const EnumPropertyItem rna_enum_constraint_type_items[] = {
RNA_ENUM_ITEM_HEADING(N_("Motion Tracking"), nullptr),
@@ -707,6 +712,49 @@ static void rna_ActionConstraint_minmax_range(
}
}
# ifdef WITH_ANIM_BAKLAVA
static void rna_ActionConstraint_action_slot_handle_set(
PointerRNA *ptr, const blender::animrig::slot_handle_t new_slot_handle)
{
bConstraint *con = (bConstraint *)ptr->data;
bActionConstraint *acon = (bActionConstraint *)con->data;
rna_generic_action_slot_handle_set(new_slot_handle,
*ptr->owner_id,
acon->act,
acon->action_slot_handle,
acon->action_slot_name);
}
static PointerRNA rna_ActionConstraint_action_slot_get(PointerRNA *ptr)
{
bConstraint *con = (bConstraint *)ptr->data;
bActionConstraint *acon = (bActionConstraint *)con->data;
return rna_generic_action_slot_get(acon->act, acon->action_slot_handle);
}
static void rna_ActionConstraint_action_slot_set(PointerRNA *ptr,
PointerRNA value,
ReportList *reports)
{
bConstraint *con = (bConstraint *)ptr->data;
bActionConstraint *acon = (bActionConstraint *)con->data;
rna_generic_action_slot_set(
value, *ptr->owner_id, acon->act, acon->action_slot_handle, acon->action_slot_name, reports);
}
static void rna_iterator_ActionConstraint_action_slots_begin(CollectionPropertyIterator *iter,
PointerRNA *ptr)
{
bConstraint *con = (bConstraint *)ptr->data;
bActionConstraint *acon = (bActionConstraint *)con->data;
rna_iterator_generic_action_slots_begin(iter, acon->act);
}
# endif /* WITH_ANIM_BAKLAVA */
static int rna_SplineIKConstraint_joint_bindings_get_length(const PointerRNA *ptr,
int length[RNA_MAX_ARRAY_DIMENSION])
{
@@ -1859,6 +1907,71 @@ static void rna_def_constraint_action(BlenderRNA *brna)
RNA_def_property_flag(prop, PROP_EDITABLE | PROP_ID_REFCOUNT);
RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update");
# ifdef WITH_ANIM_BAKLAVA
/* This property is not necessary for the Python API (that is better off using
* slot references/pointers directly), but it is needed for library overrides
* to work. */
prop = RNA_def_property(srna, "action_slot_handle", PROP_INT, PROP_NONE);
RNA_def_property_int_sdna(prop, nullptr, "action_slot_handle");
RNA_def_property_int_funcs(
prop, nullptr, "rna_ActionConstraint_action_slot_handle_set", nullptr);
RNA_def_property_ui_text(prop,
"Action Slot Handle",
"A number that identifies which sub-set of the Action is considered "
"to be for this Action Constraint");
RNA_def_property_override_flag(prop, PROPOVERRIDE_OVERRIDABLE_LIBRARY);
RNA_def_property_update(prop, NC_ANIMATION | ND_NLA_ACTCHANGE, "rna_Constraint_update");
prop = RNA_def_property(srna, "action_slot_name", PROP_STRING, PROP_NONE);
RNA_def_property_string_sdna(prop, nullptr, "action_slot_name");
RNA_def_property_ui_text(
prop,
"Action Slot Name",
"The name of the action slot. The slot identifies which sub-set of the Action "
"is considered to be for this constraint, and its name is used to find the right slot "
"when assigning an Action.");
prop = RNA_def_property(srna, "action_slot", PROP_POINTER, PROP_NONE);
RNA_def_property_struct_type(prop, "ActionSlot");
RNA_def_property_flag(prop, PROP_EDITABLE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_ui_text(
prop,
"Action Slot",
"The slot identifies which sub-set of the Action is considered to be for this "
"strip, and its name is used to find the right slot when assigning another Action");
RNA_def_property_pointer_funcs(prop,
"rna_ActionConstraint_action_slot_get",
"rna_ActionConstraint_action_slot_set",
nullptr,
nullptr);
RNA_def_property_update(prop, NC_ANIMATION | ND_NLA_ACTCHANGE, "rna_Constraint_update");
/* `strip.action_slot` is exposed to RNA as a pointer for things like the action slot selector in
* the GUI. The ground truth of the assigned slot, however, is `action_slot_handle` declared
* above. That property is used for library override operations, and this pointer property should
* just be ignored.
*
* This needs PROPOVERRIDE_IGNORE; PROPOVERRIDE_NO_COMPARISON is not suitable here. This property
* should act as if it is an overridable property (as from the user's perspective, it is), but an
* override operation should not be created for it. It will be created for `action_slot_handle`,
* and that's enough. */
RNA_def_property_override_flag(prop, PROPOVERRIDE_IGNORE);
prop = RNA_def_property(srna, "action_slots", PROP_COLLECTION, PROP_NONE);
RNA_def_property_struct_type(prop, "ActionSlot");
RNA_def_property_collection_funcs(prop,
"rna_iterator_ActionConstraint_action_slots_begin",
"rna_iterator_array_next",
"rna_iterator_array_end",
"rna_iterator_array_dereference_get",
nullptr,
nullptr,
nullptr,
nullptr);
RNA_def_property_ui_text(
prop, "Action Slots", "The list of action slots suitable for this NLA strip");
# endif /* WITH_ANIM_BAKLAVA */
prop = RNA_def_property(srna, "use_bone_object_action", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "flag", ACTCON_BONE_USE_OBJECT_ACTION);
RNA_def_property_ui_text(prop,

View File

@@ -116,6 +116,10 @@ def check_constraints(self, input_arm, expected_arm, bone, exp_bone):
"Mismatching constraint value types in pose.bones[%s].constraints[%s].%s" % (
bone.name, const_name, var))
if isinstance(value, bpy.types.bpy_prop_collection):
# Don't compare collection properties.
continue
if isinstance(value, str):
self.assertEqual(value, exp_value,
"Mismatching constraint value in pose.bones[%s].constraints[%s].%s" % (
@@ -133,10 +137,19 @@ def check_constraints(self, input_arm, expected_arm, bone, exp_bone):
self.assertEqual(value, exp_value,
"Mismatching constraint boolean in pose.bones[%s].constraints[%s].%s" % (
bone.name, const_name, var))
else:
elif isinstance(value, float):
msg = "Mismatching constraint value in pose.bones[%s].constraints[%s].%s" % (
bone.name, const_name, var)
self.assertAlmostEqual(value, exp_value, places=6, msg=msg)
elif isinstance(value, int):
msg = "Mismatching constraint value in pose.bones[%s].constraints[%s].%s" % (
bone.name, const_name, var)
self.assertEqual(value, exp_value, msg=msg)
elif value is None:
# Since above the types were compared already, if value is none, so is exp_value.
pass
else:
self.fail(f"unexpected value type: {value!r} is of type {type(value)}")
class AbstractAnimationTest: