Fix several Prefab outliner ordering issues (#5747)
* Fix several Prefab outliner ordering issues This change does a few things to address instability in entity order when prefabs are enabled: - Changes ordering behavior on entity add to always be "insert after the last selected sibling of the new entity, or at the end if there is no selected sibling" - Adds some logic to ensure selection stays frozen during prefab propagation to allow this behavior to be used - Alters delta generation in `PrefabPublicHandler::CreateEntity` to use the template instead of reserializing the DOM - this avoids a whole bunch of patching issues caused by EditorEntitySortComponent doing post-hoc order fix-up and should generally be safer/faster as we're producing patches for the actual target for those patches - Because the duplicate action is DOM-driven, and there's some thorniness around making changes that will affect the template during propagation, this adds `PrefabPublicHandler::AddNewEntityToSortOrder` to directly patch the DOM for the duplicate case Two bits of this come from patches from the incredibly helpful @AMZN-daimini - Alters `PrefabPublicHandler::GenerateUndoNodesForEntityChangeAndUpdateCache` behavior for determining what's an override: we now check to see if we're part of the current focused instance but *not* owned by the focus instance directly. This lets entity order for nested prefabs get saved to the owning prefab instead of as an override. I'm putting this up for discussion without a feature flag gating it, but we may wish to make this a toggle or disable it outright for stabilization (in which case entity order won't be saved to the owning prefab) - Adds a custom serializer for EditorEntitySortComponent. This isn't strictly necessary now, but it was very useful for debugging and ended up receiving much more manual testing its migration path and save/load path more than I've tested without using the serializer. Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com> * Add missing serializers Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com> * Address some review feedback Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com> * Generate patch in `PrefabPublicHandler::CreateEntity` using the instance's fully evaluated template in-memory Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com> * Fix up comment Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com> * Fix Linux build Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com> * Try to make test less timing dependent (haven't been able to repro failure locally) Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com> * Fix another build issue Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com> * Fix unit test failures Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com> * Fix a duplicate issue with sanitization that was causing sporadic test failure (thanks, test!) Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com> * One more Linux fix... Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com>
This commit is contained in:
committed by
GitHub
parent
4ad35f424e
commit
4ee2f341dc
+3
-2
@@ -20,6 +20,7 @@
|
||||
#include <AzToolsFramework/Prefab/PrefabPublicNotificationBus.h>
|
||||
#include <AzToolsFramework/Prefab/PrefabSystemComponentInterface.h>
|
||||
#include <AzToolsFramework/Prefab/Template/Template.h>
|
||||
#include <AzToolsFramework/Prefab/PrefabPublicRequestBus.h>
|
||||
|
||||
namespace AzToolsFramework
|
||||
{
|
||||
@@ -244,10 +245,10 @@ namespace AzToolsFramework
|
||||
selectedEntityIds.erase(entityIdIterator--);
|
||||
}
|
||||
}
|
||||
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequests::SetSelectedEntities, selectedEntityIds);
|
||||
|
||||
// Notify Propagation has ended
|
||||
// Notify Propagation has ended, then update selection (which is frozen during propagation, so this order matters)
|
||||
PrefabPublicNotificationBus::Broadcast(&PrefabPublicNotifications::OnPrefabInstancePropagationEnd);
|
||||
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequests::SetSelectedEntities, selectedEntityIds);
|
||||
}
|
||||
|
||||
m_updatingTemplateInstancesInQueue = false;
|
||||
|
||||
@@ -28,6 +28,9 @@ namespace AzToolsFramework
|
||||
inline static const char* EntityIdName = "Id";
|
||||
inline static const char* EntitiesName = "Entities";
|
||||
inline static const char* ContainerEntityName = "ContainerEntity";
|
||||
inline static const char* ComponentsName = "Components";
|
||||
inline static const char* EntityOrderName = "Child Entity Order";
|
||||
inline static const char* TypeName = "$type";
|
||||
|
||||
/**
|
||||
* Find Prefab value from given parent value and target value's name.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <AzCore/Component/TransformBus.h>
|
||||
#include <AzCore/JSON/stringbuffer.h>
|
||||
#include <AzCore/JSON/writer.h>
|
||||
#include <AzCore/Serialization/Json/JsonSerialization.h>
|
||||
#include <AzCore/Utils/TypeHash.h>
|
||||
|
||||
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
|
||||
@@ -29,6 +30,7 @@
|
||||
#include <AzToolsFramework/Prefab/PrefabUndo.h>
|
||||
#include <AzToolsFramework/Prefab/PrefabUndoHelpers.h>
|
||||
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
|
||||
#include <AzToolsFramework/Entity/EditorEntitySortComponent.h>
|
||||
|
||||
#include <QString>
|
||||
|
||||
@@ -595,8 +597,41 @@ namespace AzToolsFramework
|
||||
|
||||
Instance& entityOwningInstance = owningInstanceOfParentEntity->get();
|
||||
|
||||
// Get the template for our owning instance from the root prefab DOM and use that to generate our patch
|
||||
AZStd::vector<InstanceOptionalConstReference> pathOfInstances;
|
||||
|
||||
InstanceOptionalReference rootInstance = owningInstanceOfParentEntity;
|
||||
while (rootInstance->get().GetParentInstance() != AZStd::nullopt)
|
||||
{
|
||||
pathOfInstances.emplace_back(rootInstance);
|
||||
rootInstance = rootInstance->get().GetParentInstance();
|
||||
}
|
||||
|
||||
AZStd::string aliasPathResult = "";
|
||||
for (auto instanceIter = pathOfInstances.rbegin(); instanceIter != pathOfInstances.rend(); ++instanceIter)
|
||||
{
|
||||
aliasPathResult.append("/Instances/");
|
||||
aliasPathResult.append((*instanceIter)->get().GetInstanceAlias());
|
||||
}
|
||||
|
||||
PrefabDomPath rootPrefabDomPath(aliasPathResult.c_str());
|
||||
|
||||
PrefabDom& rootPrefabTemplateDom = m_prefabSystemComponentInterface->FindTemplateDom(rootInstance->get().GetTemplateId());
|
||||
|
||||
auto instanceDomFromRootValue = rootPrefabDomPath.Get(rootPrefabTemplateDom);
|
||||
if (!instanceDomFromRootValue)
|
||||
{
|
||||
return AZ::Failure<AZStd::string>("Could not load Instance DOM from the top level ancestor's DOM.");
|
||||
}
|
||||
|
||||
PrefabDomValueReference instanceDomFromRoot = *instanceDomFromRootValue;
|
||||
if (!instanceDomFromRoot.has_value())
|
||||
{
|
||||
return AZ::Failure<AZStd::string>("Could not load Instance DOM from the top level ancestor's DOM.");
|
||||
}
|
||||
|
||||
PrefabDom instanceDomBeforeUpdate;
|
||||
m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomBeforeUpdate, entityOwningInstance);
|
||||
instanceDomBeforeUpdate.CopyFrom(instanceDomFromRoot.value().get(), instanceDomBeforeUpdate.GetAllocator());
|
||||
|
||||
ScopedUndoBatch undoBatch("Add Entity");
|
||||
|
||||
@@ -674,6 +709,9 @@ namespace AzToolsFramework
|
||||
bool isInstanceContainerEntity = IsInstanceContainerEntity(entityId) && !IsLevelInstanceContainerEntity(entityId);
|
||||
bool isNewParentOwnedByDifferentInstance = false;
|
||||
|
||||
bool isInFocusTree = m_prefabFocusPublicInterface->IsOwningPrefabInFocusHierarchy(entityId);
|
||||
bool isOwnedByFocusedPrefabInstance = m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId);
|
||||
|
||||
if (beforeParentId != afterParentId)
|
||||
{
|
||||
// If the entity parent changed, verify if the owning instance changed too
|
||||
@@ -727,7 +765,7 @@ namespace AzToolsFramework
|
||||
}
|
||||
}
|
||||
|
||||
if (isInstanceContainerEntity)
|
||||
if (isInFocusTree && !isOwnedByFocusedPrefabInstance)
|
||||
{
|
||||
if (isNewParentOwnedByDifferentInstance)
|
||||
{
|
||||
@@ -1653,6 +1691,144 @@ namespace AzToolsFramework
|
||||
return true;
|
||||
}
|
||||
|
||||
void PrefabPublicHandler::AddNewEntityToSortOrder(
|
||||
Instance& owningInstance,
|
||||
PrefabDom& domToAddEntityUnder,
|
||||
const EntityAlias& parentEntityAlias,
|
||||
const EntityAlias& entityToAddAlias)
|
||||
{
|
||||
// Find the parent entity to get its sort order component
|
||||
auto findParentEntity = [&]() -> rapidjson::Value*
|
||||
{
|
||||
if (auto containerEntityIter = domToAddEntityUnder.FindMember(PrefabDomUtils::ContainerEntityName);
|
||||
containerEntityIter != domToAddEntityUnder.MemberEnd())
|
||||
{
|
||||
if (parentEntityAlias == containerEntityIter->value[PrefabDomUtils::EntityIdName].GetString())
|
||||
{
|
||||
return &containerEntityIter->value;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto entitiesIter = domToAddEntityUnder.FindMember(PrefabDomUtils::EntitiesName);
|
||||
entitiesIter != domToAddEntityUnder.MemberEnd())
|
||||
{
|
||||
for (auto entityIter = entitiesIter->value.MemberBegin(); entityIter != entitiesIter->value.MemberEnd(); ++entityIter)
|
||||
{
|
||||
if (parentEntityAlias == entityIter->value[PrefabDomUtils::EntityIdName].GetString())
|
||||
{
|
||||
return &entityIter->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
rapidjson::Value* parentEntityValue = findParentEntity();
|
||||
if (parentEntityValue == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the list of selected entities, we'll insert our duplicated entities after the last selected
|
||||
// sibling in their parent's list, e.g. for:
|
||||
// - Entity1
|
||||
// - Entity2 (selected)
|
||||
// - Entity3
|
||||
// - Entity4 (selected)
|
||||
// - Entity5
|
||||
// Our duplicate selection command would create duplicate Entity2 and Entity4 and insert them after Entity4:
|
||||
// - Entity1
|
||||
// - Entity2
|
||||
// - Entity3
|
||||
// - Entity4
|
||||
// - Entity2 (new, selected after duplicate)
|
||||
// - Entity4 (new, selected after duplicate)
|
||||
// - Entity5
|
||||
AzToolsFramework::EntityIdList selectedEntities;
|
||||
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(
|
||||
selectedEntities, &AzToolsFramework::ToolsApplicationRequests::GetSelectedEntities);
|
||||
|
||||
// Find the EditorEntitySortComponent DOM
|
||||
auto componentsIter = parentEntityValue->FindMember(PrefabDomUtils::ComponentsName);
|
||||
if (componentsIter == parentEntityValue->MemberEnd())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto componentIter = componentsIter->value.MemberBegin(); componentIter != componentIter->value.MemberEnd();
|
||||
++componentIter)
|
||||
{
|
||||
// Check the component type
|
||||
auto typeFieldIter = componentIter->value.FindMember(PrefabDomUtils::TypeName);
|
||||
if (typeFieldIter == componentIter->value.MemberEnd())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AZ::JsonDeserializerSettings jsonDeserializerSettings;
|
||||
AZ::Uuid typeId = AZ::Uuid::CreateNull();
|
||||
AZ::JsonSerialization::LoadTypeId(typeId, typeFieldIter->value);
|
||||
|
||||
if (typeId != azrtti_typeid<Components::EditorEntitySortComponent>())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for the entity order field
|
||||
auto orderMembersIter = componentIter->value.FindMember(PrefabDomUtils::EntityOrderName);
|
||||
if (orderMembersIter == componentIter->value.MemberEnd() || !orderMembersIter->value.IsArray())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Scan for the last selected entity in the list (if any) to determine where to add our entries
|
||||
rapidjson::Value newOrder(rapidjson::kArrayType);
|
||||
auto insertValuesAfter = orderMembersIter->value.End();
|
||||
for (auto orderMemberIter = orderMembersIter->value.Begin(); orderMemberIter != orderMembersIter->value.End();
|
||||
++orderMemberIter)
|
||||
{
|
||||
if (!orderMemberIter->IsString())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const char* value = orderMemberIter->GetString();
|
||||
for (AZ::EntityId selectedEntity : selectedEntities)
|
||||
{
|
||||
auto alias = owningInstance.GetEntityAlias(selectedEntity);
|
||||
if (alias.has_value() && alias.value().get() == value)
|
||||
{
|
||||
insertValuesAfter = orderMemberIter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Construct our new array with the new order - insertion may happen at end, so check for that in the loop itself
|
||||
for (auto orderMemberIter = orderMembersIter->value.Begin();; ++orderMemberIter)
|
||||
{
|
||||
if (orderMemberIter != orderMembersIter->value.End())
|
||||
{
|
||||
newOrder.PushBack(orderMemberIter->Move(), domToAddEntityUnder.GetAllocator());
|
||||
}
|
||||
if (orderMemberIter == insertValuesAfter)
|
||||
{
|
||||
newOrder.PushBack(
|
||||
rapidjson::Value(entityToAddAlias.c_str(), domToAddEntityUnder.GetAllocator()),
|
||||
domToAddEntityUnder.GetAllocator());
|
||||
}
|
||||
if (orderMemberIter == orderMembersIter->value.End())
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace the order with our newly constructed one
|
||||
orderMembersIter->value.Swap(newOrder);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PrefabPublicHandler::DuplicateNestedEntitiesInInstance(Instance& commonOwningInstance,
|
||||
const AZStd::vector<AZ::Entity*>& entities, PrefabDom& domToAddDuplicatedEntitiesUnder,
|
||||
EntityIdList& duplicatedEntityIds, AZStd::unordered_map<EntityAlias, EntityAlias>& oldAliasToNewAliasMap)
|
||||
@@ -1710,6 +1886,73 @@ namespace AzToolsFramework
|
||||
PrefabDom entityDomAfter(&domToAddDuplicatedEntitiesUnder.GetAllocator());
|
||||
entityDomAfter.Parse(newEntityDomString.toUtf8().constData());
|
||||
|
||||
EntityAlias parentEntityAlias;
|
||||
if (auto componentsIter = entityDomAfter.FindMember(PrefabDomUtils::ComponentsName);
|
||||
componentsIter != entityDomAfter.MemberEnd())
|
||||
{
|
||||
auto checkComponent = [&](const rapidjson::Value& value) -> bool
|
||||
{
|
||||
if (!value.IsObject())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the component type
|
||||
auto typeFieldIter = value.FindMember(PrefabDomUtils::TypeName);
|
||||
if (typeFieldIter == value.MemberEnd())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AZ::JsonDeserializerSettings jsonDeserializerSettings;
|
||||
AZ::Uuid typeId = AZ::Uuid::CreateNull();
|
||||
AZ::JsonSerialization::LoadTypeId(typeId, typeFieldIter->value);
|
||||
|
||||
// Prefabs get serialized with the Editor transform component type, check for that
|
||||
if (typeId != azrtti_typeid<Components::TransformComponent>())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (auto parentEntityIter = value.FindMember("Parent Entity");
|
||||
parentEntityIter != value.MemberEnd())
|
||||
{
|
||||
parentEntityAlias = parentEntityIter->value.GetString();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (componentsIter->value.IsObject())
|
||||
{
|
||||
for (auto componentIter = componentsIter->value.MemberBegin(); componentIter != componentsIter->value.MemberEnd();
|
||||
++componentIter)
|
||||
{
|
||||
if (checkComponent(componentIter->value))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (componentsIter->value.IsArray())
|
||||
{
|
||||
for (auto componentIter = componentsIter->value.Begin(); componentIter != componentsIter->value.End();
|
||||
++componentIter)
|
||||
{
|
||||
if (checkComponent(*componentIter))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert our entity into its parent's sort order
|
||||
if (!parentEntityAlias.empty())
|
||||
{
|
||||
AddNewEntityToSortOrder(commonOwningInstance, domToAddDuplicatedEntitiesUnder, parentEntityAlias, newEntityAlias);
|
||||
}
|
||||
|
||||
// Add the new Entity DOM to the Entities member of the instance
|
||||
rapidjson::Value aliasName(newEntityAlias.c_str(), static_cast<rapidjson::SizeType>(newEntityAlias.length()), domToAddDuplicatedEntitiesUnder.GetAllocator());
|
||||
entitiesIter->value.AddMember(AZStd::move(aliasName), entityDomAfter, domToAddDuplicatedEntitiesUnder.GetAllocator());
|
||||
|
||||
@@ -78,6 +78,8 @@ namespace AzToolsFramework
|
||||
|
||||
InstanceOptionalReference GetOwnerInstanceByEntityId(AZ::EntityId entityId) const;
|
||||
bool EntitiesBelongToSameInstance(const EntityIdList& entityIds) const;
|
||||
void AddNewEntityToSortOrder(Instance& owningInstance, PrefabDom& domToAddEntityUnder,
|
||||
const EntityAlias& parentEntityAlias, const EntityAlias& entityToAddAlias);
|
||||
|
||||
/**
|
||||
* Duplicate a list of entities owned by a common owning instance by directly
|
||||
|
||||
Reference in New Issue
Block a user