From 0959756f738598ef42ff87ac3d311c181ef8b513 Mon Sep 17 00:00:00 2001 From: Chris Galvan Date: Tue, 18 May 2021 15:05:12 -0500 Subject: [PATCH] [LYN-2255] Implemented duplication of entities with prefabs. --- .../AzToolsFramework/API/EditorEntityAPI.h | 15 ++ .../Application/EditorEntityManager.cpp | 22 ++- .../Application/EditorEntityManager.h | 3 + .../AzToolsFramework/Prefab/PrefabDomUtils.h | 1 + .../Prefab/PrefabPublicHandler.cpp | 173 +++++++++++++++++- .../Prefab/PrefabPublicHandler.h | 3 +- .../Prefab/PrefabPublicInterface.h | 7 + .../SandboxIntegration.cpp | 21 ++- .../SandboxIntegration.h | 2 + 9 files changed, 238 insertions(+), 9 deletions(-) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorEntityAPI.h b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorEntityAPI.h index f51af58c51..6c230ff5c7 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorEntityAPI.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorEntityAPI.h @@ -52,6 +52,21 @@ namespace AzToolsFramework * Deletes all entities in the provided list, as well as their transform descendants. */ virtual void DeleteEntitiesAndAllDescendants(const EntityIdList& entities) = 0; + + /** + * Duplicate all currently-selected entities. + */ + virtual void DuplicateSelected() = 0; + + /** + * Duplicates the specified entity. + */ + virtual void DuplicateEntityById(AZ::EntityId entityId) = 0; + + /** + * Duplicates all specified entities. + */ + virtual void DuplicateEntities(const EntityIdList& entities) = 0; }; } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp index 80d7fc7c5a..880217e807 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp @@ -43,7 +43,7 @@ namespace AzToolsFramework void EditorEntityManager::DeleteEntityById(AZ::EntityId entityId) { - DeleteEntities({entityId}); + DeleteEntities(EntityIdList{ entityId }); } void EditorEntityManager::DeleteEntities(const EntityIdList& entities) @@ -53,12 +53,30 @@ namespace AzToolsFramework void EditorEntityManager::DeleteEntityAndAllDescendants(AZ::EntityId entityId) { - DeleteEntitiesAndAllDescendants({entityId}); + DeleteEntitiesAndAllDescendants(EntityIdList{ entityId }); } void EditorEntityManager::DeleteEntitiesAndAllDescendants(const EntityIdList& entities) { m_prefabPublicInterface->DeleteEntitiesAndAllDescendantsInInstance(entities); } + + void EditorEntityManager::DuplicateSelected() + { + EntityIdList selectedEntities; + ToolsApplicationRequestBus::BroadcastResult(selectedEntities, &ToolsApplicationRequests::GetSelectedEntities); + + m_prefabPublicInterface->DuplicateEntitiesInInstance(selectedEntities); + } + + void EditorEntityManager::DuplicateEntityById(AZ::EntityId entityId) + { + DuplicateEntities(EntityIdList{ entityId }); + } + + void EditorEntityManager::DuplicateEntities(const EntityIdList& entities) + { + m_prefabPublicInterface->DuplicateEntitiesInInstance(entities); + } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h index 580ad22bda..939f73729e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h @@ -31,6 +31,9 @@ namespace AzToolsFramework void DeleteEntities(const EntityIdList& entities) override; void DeleteEntityAndAllDescendants(AZ::EntityId entityId) override; void DeleteEntitiesAndAllDescendants(const EntityIdList& entities) override; + void DuplicateSelected() override; + void DuplicateEntityById(AZ::EntityId entityId) override; + void DuplicateEntities(const EntityIdList& entities) override; private: Prefab::PrefabPublicInterface* m_prefabPublicInterface = nullptr; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomUtils.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomUtils.h index 5ee91c85ae..4feecb9da3 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomUtils.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomUtils.h @@ -28,6 +28,7 @@ namespace AzToolsFramework inline static const char* PatchesName = "Patches"; inline static const char* SourceName = "Source"; inline static const char* LinkIdName = "LinkId"; + inline static const char* EntityIdName = "Id"; inline static const char* EntitiesName = "Entities"; inline static const char* ContainerEntityName = "ContainerEntity"; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index 200a5c96fb..549830d24e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -13,6 +13,8 @@ #include #include +#include +#include #include #include @@ -31,6 +33,8 @@ #include #include +#include + namespace AzToolsFramework { namespace Prefab @@ -631,6 +635,166 @@ namespace AzToolsFramework return DeleteFromInstance(entityIds, true); } + PrefabOperationResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) + { + if (entityIds.empty()) + { + return AZ::Success(); + } + + if (!EntitiesBelongToSameInstance(entityIds)) + { + return AZ::Failure(AZStd::string("DuplicateEntitiesInInstance - Duplication Error. Cannot duplicate multiple " + "entities belonging to different instances with one operation.")); + } + + // We've already verified the entities are all owned by the same instance, + // so we can just retrieve our instance from the first entity in the list. + InstanceOptionalReference instance = GetOwnerInstanceByEntityId(entityIds[0]); + + // This will cull out any entities that have ancestors in the list, since we will end up duplicating + // the full nested hierarchy with what is returned from RetrieveAndSortPrefabEntitiesAndInstances + AzToolsFramework::EntityIdSet duplicationSet = AzToolsFramework::GetCulledEntityHierarchy(entityIds); + + AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework); + + UndoSystem::URSequencePoint* currentUndoBatch = nullptr; + ToolsApplicationRequests::Bus::BroadcastResult(currentUndoBatch, &ToolsApplicationRequests::Bus::Events::GetCurrentUndoBatch); + + bool createdUndo = false; + if (!currentUndoBatch) + { + createdUndo = true; + ToolsApplicationRequests::Bus::BroadcastResult( + currentUndoBatch, &ToolsApplicationRequests::Bus::Events::BeginUndoBatch, "Duplicate Entities"); + AZ_Assert(currentUndoBatch, "Failed to create new undo batch."); + } + + // In order to undo DuplicateEntitiesInInstance, we have to create a selection command which selects the current selection + // and then add the duplication as children. + // Commands always execute themselves first and then their children (when going forwards) + // and do the opposite when going backwards. + EntityIdList selectedEntities; + ToolsApplicationRequestBus::BroadcastResult(selectedEntities, &ToolsApplicationRequests::GetSelectedEntities); + SelectionCommand* selCommand = aznew SelectionCommand(selectedEntities, "Duplicate Entities"); + + // We insert a "deselect all" command before we duplicate the entities. This ensures the duplicate operations aren't changing + // selection state, which triggers expensive UI updates. By deselecting up front, we are able to do those expensive + // UI updates once at the start instead of once for each entity. + { + EntityIdList deselection; + SelectionCommand* deselectAllCommand = aznew SelectionCommand(deselection, "Deselect Entities"); + deselectAllCommand->SetParent(selCommand); + } + + { + AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "DuplicateEntitiesInInstance::UndoCaptureAndDuplicateEntities"); + + // Take a snapshot of the instance DOM before we manipulate it + Prefab::PrefabDom instanceDomBefore; + m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomBefore, instance->get()); + + AZStd::vector entities; + AZStd::vector> instances; + + // Gather all entities/instances in the hierarchy, but don't detach them because we are duplicating not deleting. + EntityList inputEntityList = EntityIdSetToEntityList(duplicationSet); + bool success = RetrieveAndSortPrefabEntitiesAndInstances(inputEntityList, instance->get(), entities, instances, false); + + if (!success) + { + return AZ::Failure(AZStd::string("DuplicateEntitiesInInstance")); + } + + // Make a copy of our before instance DOM where we will add our duplicated entities + Prefab::PrefabDom instanceDomAfter; + instanceDomAfter.CopyFrom(instanceDomBefore, instanceDomAfter.GetAllocator()); + + AZStd::unordered_map oldAliasToNewAliasMap; + AZStd::unordered_map aliasToEntityDomMap; + + for (AZ::Entity* entity : entities) + { + EntityAliasOptionalReference oldAliasRef = instance->get().GetEntityAlias(entity->GetId()); + AZ_Assert(oldAliasRef.has_value(), "No alias found for Entity in the DOM"); + EntityAlias oldAlias = oldAliasRef.value(); + + // Give this the outer allocator so that the memory reference will be valid when + // it gets used for AddMember + Prefab::PrefabDom entityDomBefore(&instanceDomAfter.GetAllocator()); + m_instanceToTemplateInterface->GenerateDomForEntity(entityDomBefore, *entity); + + // Keep track of the old alias <-> new alias mapping for this duplicated entity + // so we can fixup references later + EntityAlias newEntityAlias = Instance::GenerateEntityAlias(); + oldAliasToNewAliasMap.insert(AZStd::make_pair(oldAlias, newEntityAlias)); + + // Update the Entity Id in the Entity DOM for the duplicated Entity + auto entityIdIter = entityDomBefore.FindMember(PrefabDomUtils::EntityIdName); + if (entityIdIter != entityDomBefore.MemberEnd()) + { + entityIdIter->value.SetString(newEntityAlias.c_str(), newEntityAlias.length(), entityDomBefore.GetAllocator()); + } + + rapidjson::StringBuffer buffer; + buffer.Clear(); + + rapidjson::Writer writer(buffer); + entityDomBefore.Accept(writer); + + // Store our duplicated Entity DOM with its new alias as a string + // so that we can fixup entity alias references before adding it + // to the Entities member of our instance DOM + QString entityDomString(buffer.GetString()); + aliasToEntityDomMap.insert(AZStd::make_pair(newEntityAlias, entityDomString)); + } + + auto entitiesIter = instanceDomAfter.FindMember(PrefabDomUtils::EntitiesName); + AZ_Assert(entitiesIter != instanceDomAfter.MemberEnd(), "Instance DOM missing the Entities member."); + + // Now that all the duplicated Entity DOMs have been created, we need to iterate + // through them and replace any previous EntityAlias references with the new ones. + // These are more than just parent entity references for nested entities, this will + // also cover any EntityId references that were made in the components between them. + for (auto aliasEntityPair : aliasToEntityDomMap) + { + EntityAlias newEntityAlias = aliasEntityPair.first; + QString newEntityDomString = aliasEntityPair.second; + + // Replace all of the old alias references with the new ones + for (auto aliasMapIter : oldAliasToNewAliasMap) + { + newEntityDomString.replace(aliasMapIter.first.c_str(), aliasMapIter.second.c_str()); + } + + // Create the new Entity DOM from parsing the JSON string + Prefab::PrefabDom entityDomAfter(&instanceDomAfter.GetAllocator()); + entityDomAfter.Parse(newEntityDomString.toUtf8().constData()); + + // Add the new Entity DOM to the Entities member of the instance + rapidjson::Value aliasName(newEntityAlias.c_str(), newEntityAlias.length(), instanceDomAfter.GetAllocator()); + entitiesIter->value.AddMember(AZStd::move(aliasName), entityDomAfter, instanceDomAfter.GetAllocator()); + } + + PrefabUndoInstance* command = aznew PrefabUndoInstance("Instance duplication"); + command->Capture(instanceDomBefore, instanceDomAfter, instance->get().GetTemplateId()); + command->SetParent(selCommand); + } + + selCommand->SetParent(currentUndoBatch); + { + AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "DuplicateEntitiesInInstance:RunRedo"); + selCommand->RunRedo(); + } + + if (createdUndo) + { + ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequests::EndUndoBatch); + } + + return AZ::Success(); + } + PrefabOperationResult PrefabPublicHandler::DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants) { if (entityIds.empty()) @@ -865,7 +1029,8 @@ namespace AzToolsFramework bool PrefabPublicHandler::RetrieveAndSortPrefabEntitiesAndInstances( const EntityList& inputEntities, Instance& commonRootEntityOwningInstance, - EntityList& outEntities, AZStd::vector>& outInstances) const + EntityList& outEntities, AZStd::vector>& outInstances, + bool shouldDetach) const { if (inputEntities.size() == 0) { @@ -949,14 +1114,16 @@ namespace AzToolsFramework for (AZ::Entity* entity : entities) { - outEntities.emplace_back(commonRootEntityOwningInstance.DetachEntity(entity->GetId()).release()); + AZ::Entity* outEntity = (shouldDetach) ? commonRootEntityOwningInstance.DetachEntity(entity->GetId()).release() : entity; + outEntities.emplace_back(outEntity); } outInstances.clear(); outInstances.reserve(instances.size()); for (Instance* instancePtr : instances) { - outInstances.push_back(AZStd::move(commonRootEntityOwningInstance.DetachNestedInstance(instancePtr->GetInstanceAlias()))); + AZStd::unique_ptr outInstance = (shouldDetach) ? commonRootEntityOwningInstance.DetachNestedInstance(instancePtr->GetInstanceAlias()) : AZStd::unique_ptr(instancePtr); + outInstances.push_back(AZStd::move(outInstance)); } return (outEntities.size() + outInstances.size()) > 0; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h index d88086dda9..b6128f0ab9 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h @@ -60,11 +60,12 @@ namespace AzToolsFramework PrefabOperationResult DeleteEntitiesInInstance(const EntityIdList& entityIds) override; PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override; + PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; private: PrefabOperationResult DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants); bool RetrieveAndSortPrefabEntitiesAndInstances(const EntityList& inputEntities, Instance& commonRootEntityOwningInstance, - EntityList& outEntities, AZStd::vector>& outInstances) const; + EntityList& outEntities, AZStd::vector>& outInstances, bool shouldDetach = true) const; InstanceOptionalReference GetOwnerInstanceByEntityId(AZ::EntityId entityId) const; bool EntitiesBelongToSameInstance(const EntityIdList& entityIds) const; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h index 1a8da0dfe0..0750c4d264 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h @@ -143,6 +143,13 @@ namespace AzToolsFramework * @return An outcome object; on failure, it comes with an error message detailing the cause of the error. */ virtual PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) = 0; + + /** + * Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance. + * @param entities The entities to duplicate. + * @return An outcome object; on failure, it comes with an error message detailing the cause of the error. + */ + virtual PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; }; } // namespace Prefab diff --git a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp index 027487a435..d36c20c56a 100644 --- a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp +++ b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -192,6 +193,9 @@ void SandboxIntegrationManager::Setup() (m_prefabIntegrationInterface != nullptr), "SandboxIntegrationManager requires a PrefabIntegrationInterface instance to be present on Setup()."); + m_editorEntityAPI = AZ::Interface::Get(); + AZ_Assert(m_editorEntityAPI, "SandboxIntegrationManager requires an EditorEntityAPI instance to be present on Setup()."); + AzToolsFramework::Layers::EditorLayerComponentNotificationBus::Handler::BusConnect(); } @@ -1215,9 +1219,20 @@ void SandboxIntegrationManager::CloneSelection(bool& handled) if (!duplicationSet.empty()) { - AZStd::unordered_set clonedEntities; - handled = AzToolsFramework::CloneInstantiatedEntities(duplicationSet, clonedEntities); - m_unsavedEntities.insert(clonedEntities.begin(), clonedEntities.end()); + bool prefabSystemEnabled = false; + AzFramework::ApplicationRequests::Bus::BroadcastResult(prefabSystemEnabled, &AzFramework::ApplicationRequests::IsPrefabSystemEnabled); + + if (prefabSystemEnabled) + { + m_editorEntityAPI->DuplicateSelected(); + handled = true; + } + else + { + AZStd::unordered_set clonedEntities; + handled = AzToolsFramework::CloneInstantiatedEntities(duplicationSet, clonedEntities); + m_unsavedEntities.insert(clonedEntities.begin(), clonedEntities.end()); + } } else { diff --git a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h index 14f52591a4..528b93e44e 100644 --- a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h +++ b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h @@ -77,6 +77,7 @@ class CHyperGraph; namespace AzToolsFramework { + class EditorEntityAPI; class EditorEntityUiInterface; namespace AssetBrowser @@ -371,6 +372,7 @@ private: AzToolsFramework::EditorEntityUiInterface* m_editorEntityUiInterface = nullptr; AzToolsFramework::Prefab::PrefabIntegrationInterface* m_prefabIntegrationInterface = nullptr; + AzToolsFramework::EditorEntityAPI* m_editorEntityAPI = nullptr; // Overrides UI styling and behavior for Layer Entities AzToolsFramework::LayerUiHandler m_layerUiOverrideHandler;