Anim: make RNA Slot.target_id_type writable when not yet specified

When loading old blend files, versioned Actions can end up having a slot with an
'UNSPECIFIED' `target_id_type`. Assigning the slot to an ID will then set the
slot's `target_id_type` to match the type of the ID, but there was previously no
way to directly set it via Python if needed.

This PR changes the writability of `target_id_type` to match the extent of its
mutability when assigning a slot to an IDs. Which is to say, it can be set if
it's 'UNSPECIFIED', but not otherwise.

RNA doesn't have a good way to represent this, so we accomplish this with a
custom set function that simply ignores the write if the slot's `target_id_type`
isn't 'UNSPECIFIED'. This isn't ideal, but is the least-bad solution at the
moment.

Pull Request: https://projects.blender.org/blender/blender/pulls/133883
This commit is contained in:
Nathan Vegdahl
2025-02-03 13:01:47 +01:00
committed by Nathan Vegdahl
parent fa2b131395
commit b5005cd99c
4 changed files with 100 additions and 4 deletions

View File

@@ -220,6 +220,20 @@ class Action : public ::bAction {
*/
void slot_display_name_set(Main &bmain, Slot &slot, StringRefNull new_display_name);
/**
* Set the slot's target ID type, updating the identifier prefix to match and
* ensuring that the resulting identifier is unique.
*
* This has to be done on the Action level to ensure each slot has a unique
* identifier within the Action.
*
* \note This does NOT propagate the identifier to the slot's users. That is
* the caller's responsibility.
*
* \see #Action::slot_identifier_propagate
*/
void slot_idtype_define(Slot &slot, ID_Type idtype);
/**
* Set the slot identifier, ensure it is unique, and propagate the new identifier to
* all data-blocks that use it.

View File

@@ -410,6 +410,13 @@ void Action::slot_display_name_set(Main &bmain, Slot &slot, StringRefNull new_di
this->slot_identifier_propagate(bmain, slot);
}
void Action::slot_idtype_define(Slot &slot, ID_Type idtype)
{
slot.idtype = idtype;
slot.identifier_ensure_prefix();
slot_identifier_ensure_unique(*this, slot);
}
void Action::slot_identifier_set(Main &bmain, Slot &slot, const StringRefNull new_identifier)
{
/* TODO: maybe this function should only set the 'identifier without prefix' aka the 'display

View File

@@ -1554,6 +1554,24 @@ static const EnumPropertyItem *rna_ActionSlot_target_id_type_itemf(bContext * /*
return items;
}
static void rna_ActionSlot_target_id_type_set(PointerRNA *ptr, int value)
{
animrig::Action &action = reinterpret_cast<bAction *>(ptr->owner_id)->wrap();
animrig::Slot &slot = reinterpret_cast<ActionSlot *>(ptr->data)->wrap();
if (slot.idtype != 0) {
/* Ignore the assignment. */
printf(
"WARNING: ignoring assignment to target_id_type of Slot '%s' in Action '%s'. A Slot's "
"target_id_type can only be changed when currently 'UNSPECIFIED'.\n",
slot.identifier,
action.id.name);
return;
}
action.slot_idtype_define(slot, ID_Type(value));
}
#else
static void rna_def_dopesheet(BlenderRNA *brna)
@@ -2007,11 +2025,14 @@ static void rna_def_action_slot(BlenderRNA *brna)
prop = RNA_def_property(srna, "target_id_type", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_sdna(prop, nullptr, "idtype");
RNA_def_property_enum_items(prop, default_ActionSlot_target_id_type_items);
RNA_def_property_enum_funcs(prop, nullptr, nullptr, "rna_ActionSlot_target_id_type_itemf");
RNA_def_property_enum_funcs(
prop, nullptr, "rna_ActionSlot_target_id_type_set", "rna_ActionSlot_target_id_type_itemf");
RNA_def_property_update(prop, NC_ANIMATION | ND_ANIMCHAN, "rna_ActionSlot_identifier_update");
RNA_def_property_flag(prop, PROP_ENUM_NO_CONTEXT);
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
RNA_def_property_ui_text(
prop, "Target ID Type", "Type of data-block that this slot is intended to animate");
RNA_def_property_ui_text(prop,
"Target ID Type",
"Type of data-block that this slot is intended to animate; can be set "
"when 'UNSPECIFIED' but is otherwise read-only");
RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_ID_ID);
prop = RNA_def_property(srna, "target_id_type_icon", PROP_INT, PROP_NONE);

View File

@@ -191,6 +191,60 @@ class ActionSlotAssignmentTest(unittest.TestCase):
"After assignment, the ID type should remain UNSPECIFIED when the Action is linked.")
self.assertEqual("XXLegacy Slot", slot.identifier)
def test_untyped_slot_target_id_writing(self):
"""Test writing to the target id type of an untyped slot."""
action = self._load_legacy_action(link=False)
slot = action.slots[0]
self.assertEqual('UNSPECIFIED', slot.target_id_type)
self.assertEqual("XXLegacy Slot", slot.identifier)
slot.target_id_type = 'OBJECT'
self.assertEqual(
'OBJECT',
slot.target_id_type,
"Should be able to write to target_id_type of a slot when not yet specified.")
self.assertEqual("OBLegacy Slot", slot.identifier)
slot.target_id_type = 'MATERIAL'
self.assertEqual(
'OBJECT',
slot.target_id_type,
"Should NOT be able to write to target_id_type of a slot when already specified.")
self.assertEqual("OBLegacy Slot", slot.identifier)
def test_untyped_slot_target_id_writing_with_duplicate_identifier(self):
"""Test that writing to the target id type a slot appropriately renames
it when that would otherwise cause its identifier to collide with an
already existing slot."""
action = self._load_legacy_action(link=False)
slot = action.slots[0]
# Create soon-to-collide slot.
other_slot = action.slots.new('OBJECT', "Legacy Slot")
# Ensure the setup is correct.
self.assertEqual('UNSPECIFIED', slot.target_id_type)
self.assertEqual("XXLegacy Slot", slot.identifier)
self.assertEqual('OBJECT', other_slot.target_id_type)
self.assertEqual("OBLegacy Slot", other_slot.identifier)
# Assign the colliding target id type.
slot.target_id_type = 'OBJECT'
self.assertEqual('OBJECT', slot.target_id_type)
self.assertEqual(
"OBLegacy Slot.001",
slot.identifier,
"Should get renamed to not conflict with existing slots.")
self.assertEqual('OBJECT', other_slot.target_id_type)
self.assertEqual("OBLegacy Slot", other_slot.identifier)
@staticmethod
def _load_legacy_action(*, link: bool) -> bpy.types.Action:
# At the moment of writing, the only way to create an untyped slot is to