Merge pull request #809 from aws-lumberyard-dev/cgalvan/DuplicateEntities
[LYN-2255] Implemented duplication of entities with prefabs.
This commit is contained in:
@@ -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
|
||||
|
||||
+20
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <AzToolsFramework/Prefab/PrefabPublicHandler.h>
|
||||
|
||||
#include <AzCore/Component/TransformBus.h>
|
||||
#include <AzCore/JSON/stringbuffer.h>
|
||||
#include <AzCore/JSON/writer.h>
|
||||
#include <AzCore/Utils/TypeHash.h>
|
||||
|
||||
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
|
||||
@@ -31,6 +33,8 @@
|
||||
#include <AzToolsFramework/Prefab/PrefabUndoHelpers.h>
|
||||
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace AzToolsFramework
|
||||
{
|
||||
namespace Prefab
|
||||
@@ -83,7 +87,8 @@ namespace AzToolsFramework
|
||||
commonRootInstanceDomBeforeCreate, commonRootEntityOwningInstance->get());
|
||||
|
||||
AZStd::vector<AZ::Entity*> entities;
|
||||
AZStd::vector<AZStd::unique_ptr<Instance>> instances;
|
||||
AZStd::vector<AZStd::unique_ptr<Instance>> instancePtrs;
|
||||
AZStd::vector<Instance*> instances;
|
||||
AZStd::unordered_map<Instance*, PrefabDom> nestedInstanceLinkPatchesMap;
|
||||
|
||||
// Retrieve all entities affected and identify Instances
|
||||
@@ -93,10 +98,18 @@ namespace AzToolsFramework
|
||||
AZStd::string("Could not create a new prefab out of the entities provided - invalid selection."));
|
||||
}
|
||||
|
||||
// Detach the retrieved entities
|
||||
for (AZ::Entity* entity : entities)
|
||||
{
|
||||
commonRootEntityOwningInstance->get().DetachEntity(entity->GetId()).release();
|
||||
}
|
||||
|
||||
// When we create a prefab with other prefab instances, we have to remove the existing links between the source and
|
||||
// target templates of the other instances.
|
||||
for (auto& nestedInstance : instances)
|
||||
{
|
||||
AZStd::unique_ptr<Instance> outInstance = commonRootEntityOwningInstance->get().DetachNestedInstance(nestedInstance->GetInstanceAlias());
|
||||
|
||||
auto linkRef = m_prefabSystemComponentInterface->FindLink(nestedInstance->GetLinkId());
|
||||
|
||||
if (linkRef.has_value())
|
||||
@@ -104,10 +117,12 @@ namespace AzToolsFramework
|
||||
PrefabDom oldLinkPatches;
|
||||
oldLinkPatches.CopyFrom(linkRef->get().GetLinkDom(), oldLinkPatches.GetAllocator());
|
||||
|
||||
nestedInstanceLinkPatchesMap.emplace(nestedInstance.get(), AZStd::move(oldLinkPatches));
|
||||
nestedInstanceLinkPatchesMap.emplace(nestedInstance, AZStd::move(oldLinkPatches));
|
||||
}
|
||||
|
||||
RemoveLink(nestedInstance, commonRootEntityOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch());
|
||||
RemoveLink(outInstance, commonRootEntityOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch());
|
||||
|
||||
instancePtrs.emplace_back(AZStd::move(outInstance));
|
||||
}
|
||||
|
||||
PrefabUndoHelpers::UpdatePrefabInstance(
|
||||
@@ -123,7 +138,7 @@ namespace AzToolsFramework
|
||||
|
||||
// Create the Prefab
|
||||
instanceToCreate = prefabEditorEntityOwnershipInterface->CreatePrefab(
|
||||
entities, AZStd::move(instances), filePath, commonRootEntityOwningInstance);
|
||||
entities, AZStd::move(instancePtrs), filePath, commonRootEntityOwningInstance);
|
||||
|
||||
if (!instanceToCreate)
|
||||
{
|
||||
@@ -388,7 +403,7 @@ namespace AzToolsFramework
|
||||
// Find common root and top level entities
|
||||
bool entitiesHaveCommonRoot = false;
|
||||
|
||||
AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult(
|
||||
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(
|
||||
entitiesHaveCommonRoot, &AzToolsFramework::ToolsApplicationRequests::FindCommonRootInactive, inputEntityList,
|
||||
commonRootEntityId, &topLevelEntities);
|
||||
|
||||
@@ -710,6 +725,151 @@ namespace AzToolsFramework
|
||||
return DeleteFromInstance(entityIds, true);
|
||||
}
|
||||
|
||||
PrefabOperationResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds)
|
||||
{
|
||||
if (entityIds.empty())
|
||||
{
|
||||
return AZ::Failure(AZStd::string("No entities to duplicate."));
|
||||
}
|
||||
|
||||
if (!EntitiesBelongToSameInstance(entityIds))
|
||||
{
|
||||
return AZ::Failure(AZStd::string("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 commonEntityOwningInstance = GetOwnerInstanceByEntityId(entityIds[0]);
|
||||
AZ_Assert(
|
||||
commonEntityOwningInstance.has_value(),
|
||||
"Failed to duplicate : Couldn't get a valid owning instance for the common root entity of the entities provided");
|
||||
|
||||
// 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);
|
||||
|
||||
ScopedUndoBatch undoBatch("Duplicate Entities");
|
||||
|
||||
{
|
||||
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, commonEntityOwningInstance->get());
|
||||
|
||||
AZStd::vector<AZ::Entity*> entities;
|
||||
AZStd::vector<Instance*> 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, commonEntityOwningInstance->get(), entities, instances);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return AZ::Failure(AZStd::string("Failed to retrieve entities and instances from the given list of entity ids for duplication"));
|
||||
}
|
||||
|
||||
// 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<EntityAlias, EntityAlias> oldAliasToNewAliasMap;
|
||||
AZStd::unordered_map<EntityAlias, QString> aliasToEntityDomMap;
|
||||
|
||||
for (AZ::Entity* entity : entities)
|
||||
{
|
||||
EntityAliasOptionalReference oldAliasRef = commonEntityOwningInstance->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));
|
||||
|
||||
rapidjson::StringBuffer buffer;
|
||||
rapidjson::Writer<rapidjson::StringBuffer> 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
|
||||
// We bookend the aliases with \" and also with a / as an extra precaution to prevent
|
||||
// inadvertently replacing a matching string vs. where an actual EntityId is expected
|
||||
// This will cover both cases where an alias could be used in a normal entity vs. an instance
|
||||
for (auto aliasMapIter : oldAliasToNewAliasMap)
|
||||
{
|
||||
QString oldAliasQuotes = QString("\"%1\"").arg(aliasMapIter.first.c_str());
|
||||
QString newAliasQuotes = QString("\"%1\"").arg(aliasMapIter.second.c_str());
|
||||
|
||||
newEntityDomString.replace(oldAliasQuotes, newAliasQuotes);
|
||||
|
||||
QString oldAliasPathRef = QString("/%1").arg(aliasMapIter.first.c_str());
|
||||
QString newAliasPathRef = QString("/%1").arg(aliasMapIter.second.c_str());
|
||||
|
||||
newEntityDomString.replace(oldAliasPathRef, newAliasPathRef);
|
||||
}
|
||||
|
||||
// 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("Entity duplication");
|
||||
command->SetParent(undoBatch.GetUndoBatch());
|
||||
command->Capture(instanceDomBefore, instanceDomAfter, commonEntityOwningInstance->get().GetTemplateId());
|
||||
command->RunRedo();
|
||||
|
||||
EntityIdList duplicatedEntityIds;
|
||||
for (auto aliasMapIter : oldAliasToNewAliasMap)
|
||||
{
|
||||
EntityAlias newEntityAlias = aliasMapIter.second;
|
||||
|
||||
AliasPath absoluteEntityPath = commonEntityOwningInstance->get().GetAbsoluteInstanceAliasPath();
|
||||
absoluteEntityPath.Append(newEntityAlias);
|
||||
|
||||
AZ::EntityId newEntityId = InstanceEntityIdMapper::GenerateEntityIdForAliasPath(absoluteEntityPath);
|
||||
duplicatedEntityIds.push_back(newEntityId);
|
||||
}
|
||||
|
||||
// Select the duplicated entities
|
||||
auto selectionUndo = aznew SelectionCommand(duplicatedEntityIds, "Select Duplicated Entities");
|
||||
selectionUndo->SetParent(undoBatch.GetUndoBatch());
|
||||
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::RunRedoSeparately, selectionUndo);
|
||||
}
|
||||
|
||||
return AZ::Success();
|
||||
}
|
||||
|
||||
PrefabOperationResult PrefabPublicHandler::DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants)
|
||||
{
|
||||
if (entityIds.empty())
|
||||
@@ -737,17 +897,7 @@ namespace AzToolsFramework
|
||||
|
||||
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, "Delete Selected");
|
||||
AZ_Assert(currentUndoBatch, "Failed to create new undo batch.");
|
||||
}
|
||||
ScopedUndoBatch undoBatch("Delete Selected");
|
||||
|
||||
// In order to undo DeleteSelected, we have to create a selection command which selects the current selection
|
||||
// and then add the deletion as children.
|
||||
@@ -775,7 +925,7 @@ namespace AzToolsFramework
|
||||
if (deleteDescendants)
|
||||
{
|
||||
AZStd::vector<AZ::Entity*> entities;
|
||||
AZStd::vector<AZStd::unique_ptr<Instance>> instances;
|
||||
AZStd::vector<Instance*> instances;
|
||||
|
||||
bool success = RetrieveAndSortPrefabEntitiesAndInstances(inputEntityList, commonOwningInstance->get(), entities, instances);
|
||||
|
||||
@@ -786,13 +936,15 @@ namespace AzToolsFramework
|
||||
|
||||
for (AZ::Entity* entity : entities)
|
||||
{
|
||||
commonOwningInstance->get().DetachEntity(entity->GetId()).release();
|
||||
AZ::ComponentApplicationBus::Broadcast(&AZ::ComponentApplicationRequests::DeleteEntity, entity->GetId());
|
||||
}
|
||||
|
||||
for (auto& nestedInstance : instances)
|
||||
{
|
||||
RemoveLink(nestedInstance, commonOwningInstance->get().GetTemplateId(), currentUndoBatch);
|
||||
nestedInstance.reset();
|
||||
AZStd::unique_ptr<Instance> outInstance = commonOwningInstance->get().DetachNestedInstance(nestedInstance->GetInstanceAlias());
|
||||
RemoveLink(outInstance, commonOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch());
|
||||
outInstance.reset();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -804,7 +956,7 @@ namespace AzToolsFramework
|
||||
if (owningInstance->get().GetContainerEntityId() == entityId)
|
||||
{
|
||||
auto instancePtr = commonOwningInstance->get().DetachNestedInstance(owningInstance->get().GetInstanceAlias());
|
||||
RemoveLink(instancePtr, commonOwningInstance->get().GetTemplateId(), currentUndoBatch);
|
||||
RemoveLink(instancePtr, commonOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -822,17 +974,12 @@ namespace AzToolsFramework
|
||||
command->SetParent(selCommand);
|
||||
}
|
||||
|
||||
selCommand->SetParent(currentUndoBatch);
|
||||
selCommand->SetParent(undoBatch.GetUndoBatch());
|
||||
{
|
||||
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "Internal::DeleteEntities:RunRedo");
|
||||
selCommand->RunRedo();
|
||||
}
|
||||
|
||||
if (createdUndo)
|
||||
{
|
||||
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequests::EndUndoBatch);
|
||||
}
|
||||
|
||||
return AZ::Success();
|
||||
}
|
||||
|
||||
@@ -944,7 +1091,7 @@ namespace AzToolsFramework
|
||||
|
||||
bool PrefabPublicHandler::RetrieveAndSortPrefabEntitiesAndInstances(
|
||||
const EntityList& inputEntities, Instance& commonRootEntityOwningInstance,
|
||||
EntityList& outEntities, AZStd::vector<AZStd::unique_ptr<Instance>>& outInstances) const
|
||||
EntityList& outEntities, AZStd::vector<Instance*>& outInstances) const
|
||||
{
|
||||
if (inputEntities.size() == 0)
|
||||
{
|
||||
@@ -1028,14 +1175,14 @@ namespace AzToolsFramework
|
||||
|
||||
for (AZ::Entity* entity : entities)
|
||||
{
|
||||
outEntities.emplace_back(commonRootEntityOwningInstance.DetachEntity(entity->GetId()).release());
|
||||
outEntities.emplace_back(entity);
|
||||
}
|
||||
|
||||
outInstances.clear();
|
||||
outInstances.reserve(instances.size());
|
||||
for (Instance* instancePtr : instances)
|
||||
{
|
||||
outInstances.push_back(AZStd::move(commonRootEntityOwningInstance.DetachNestedInstance(instancePtr->GetInstanceAlias())));
|
||||
outInstances.push_back(instancePtr);
|
||||
}
|
||||
|
||||
return (outEntities.size() + outInstances.size()) > 0;
|
||||
|
||||
@@ -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<AZStd::unique_ptr<Instance>>& outInstances) const;
|
||||
EntityList& outEntities, AZStd::vector<Instance*>& outInstances) const;
|
||||
|
||||
InstanceOptionalReference GetOwnerInstanceByEntityId(AZ::EntityId entityId) const;
|
||||
bool EntitiesBelongToSameInstance(const EntityIdList& entityIds) const;
|
||||
|
||||
@@ -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
|
||||
|
||||
+7
-1
@@ -138,7 +138,7 @@ namespace UnitTest
|
||||
if (!GetApplication())
|
||||
{
|
||||
// Create & Start a new ToolsApplication if there's no existing one
|
||||
m_app = AZStd::make_unique<ToolsTestApplication>("ToolsApplication");
|
||||
m_app = CreateTestApplication();
|
||||
m_app->Start(AzFramework::Application::Descriptor());
|
||||
}
|
||||
|
||||
@@ -216,6 +216,12 @@ namespace UnitTest
|
||||
TestEditorActions m_editorActions;
|
||||
ToolsApplicationMessageHandler m_messageHandler; // used to suppress trace messages in test output
|
||||
|
||||
// Override this if your test fixture needs to use a custom TestApplication
|
||||
virtual AZStd::unique_ptr<ToolsTestApplication> CreateTestApplication()
|
||||
{
|
||||
return AZStd::make_unique<ToolsTestApplication>("ToolsApplication");
|
||||
}
|
||||
|
||||
private:
|
||||
AZStd::unique_ptr<ToolsTestApplication> m_app;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
|
||||
|
||||
#include <Prefab/PrefabTestComponent.h>
|
||||
#include <Prefab/PrefabTestDomUtils.h>
|
||||
#include <Prefab/PrefabTestFixture.h>
|
||||
|
||||
namespace UnitTest
|
||||
{
|
||||
using PrefabDuplicateTest = PrefabTestFixture;
|
||||
|
||||
TEST_F(PrefabDuplicateTest, PrefabDuplicate_DuplicateSingleEntitySucceeds)
|
||||
{
|
||||
AZStd::string entityName("Same Name");
|
||||
AZ::Entity* entity1 = CreateEntity(entityName.c_str());
|
||||
entity1->Deactivate();
|
||||
entity1->CreateComponent<PrefabTestComponent>();
|
||||
entity1->Activate();
|
||||
|
||||
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(
|
||||
&AzToolsFramework::EditorEntityContextRequests::HandleEntitiesAdded, AzToolsFramework::EntityList{ entity1 });
|
||||
AZStd::unique_ptr<Instance> newInstance = m_prefabSystemComponent->CreatePrefab(
|
||||
{ entity1 },
|
||||
{},
|
||||
PrefabMockFilePath);
|
||||
|
||||
// We've created a prefab with a single Entity, so there should only be one EntityAlias in our instance
|
||||
EXPECT_EQ(newInstance->GetEntityAliases().size(), 1);
|
||||
|
||||
// Duplicate the Entity and trigger the UpdateTemplateInstancesInQueue so the changes get propagated
|
||||
m_prefabPublicInterface->DuplicateEntitiesInInstance(AzToolsFramework::EntityIdList{ entity1->GetId() });
|
||||
m_instanceUpdateExecutorInterface->UpdateTemplateInstancesInQueue();
|
||||
|
||||
// We duplicated a single Entity, so there should now be two EntityAliases
|
||||
EXPECT_EQ(newInstance->GetEntityAliases().size(), 2);
|
||||
|
||||
newInstance->GetConstEntities([&](const AZ::Entity& entity)
|
||||
{
|
||||
// Both of the entities should have the same name
|
||||
EXPECT_EQ(entity.GetName(), entityName);
|
||||
|
||||
// Both of the entities should have the PrefabTestComponent we added
|
||||
auto testComponent = entity.FindComponent<PrefabTestComponent>();
|
||||
EXPECT_NE(nullptr, testComponent);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(PrefabDuplicateTest, PrefabDuplicate_DuplicateMultipleEntitiesAndFixesReferences)
|
||||
{
|
||||
AZ::Entity* parentEntity = CreateEntity("Parent Entity");
|
||||
|
||||
AZ::Entity* childEntity = CreateEntity("Child Entity");
|
||||
childEntity->Deactivate();
|
||||
auto newComponent = childEntity->CreateComponent<PrefabTestComponent>();
|
||||
childEntity->Activate();
|
||||
|
||||
// Set the EntityId reference property on our PrefabTestComponent so we can
|
||||
// verify that arbitrary EntityId's are fixed up properly
|
||||
newComponent->m_entityIdProperty = parentEntity->GetId();
|
||||
|
||||
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(
|
||||
&AzToolsFramework::EditorEntityContextRequests::HandleEntitiesAdded, AzToolsFramework::EntityList{ parentEntity, childEntity });
|
||||
|
||||
AZStd::unique_ptr<Instance> newInstance = m_prefabSystemComponent->CreatePrefab(
|
||||
{ parentEntity, childEntity },
|
||||
{},
|
||||
PrefabMockFilePath);
|
||||
|
||||
// We've created a prefab with two entities, so there should be two EntityAliases in our instance
|
||||
EXPECT_EQ(newInstance->GetEntityAliases().size(), 2);
|
||||
|
||||
// Duplicate the entities and trigger the UpdateTemplateInstancesInQueue so the changes get propagated
|
||||
m_prefabPublicInterface->DuplicateEntitiesInInstance(AzToolsFramework::EntityIdList{ parentEntity->GetId(), childEntity->GetId() });
|
||||
m_instanceUpdateExecutorInterface->UpdateTemplateInstancesInQueue();
|
||||
|
||||
// We duplicated two entities, so there should now be four EntityAliases
|
||||
EXPECT_EQ(newInstance->GetEntityAliases().size(), 4);
|
||||
|
||||
AzToolsFramework::EntityIdList parentEntityIds;
|
||||
newInstance->GetConstEntities([&](const AZ::Entity& entity)
|
||||
{
|
||||
// Gather the parent EntityIds by tracking which entities don't have a PrefabTestComponent
|
||||
auto testComponent = entity.FindComponent<PrefabTestComponent>();
|
||||
if (!testComponent)
|
||||
{
|
||||
parentEntityIds.push_back(entity.GetId());
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// There should only be two parents
|
||||
EXPECT_EQ(parentEntityIds.size(), 2);
|
||||
|
||||
// Verify that the EntityId reference on the PrefabTestComponent on the children correspond
|
||||
// to unique entities, which will verify that the EntityIds are fixed up on duplicate
|
||||
newInstance->GetConstEntities([&](const AZ::Entity& entity)
|
||||
{
|
||||
// Only the child entities have a PrefabTestComponent
|
||||
auto testComponent = entity.FindComponent<PrefabTestComponent>();
|
||||
if (testComponent)
|
||||
{
|
||||
auto it = AZStd::find(parentEntityIds.begin(), parentEntityIds.end(), testComponent->m_entityIdProperty);
|
||||
EXPECT_NE(it, parentEntityIds.end());
|
||||
|
||||
// Erase when we find it so that the matches will be unique
|
||||
parentEntityIds.erase(it);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Verify we matched each of the parent EntityIds
|
||||
EXPECT_EQ(parentEntityIds.size(), 0);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,17 @@
|
||||
|
||||
namespace UnitTest
|
||||
{
|
||||
PrefabTestToolsApplication::PrefabTestToolsApplication(AZStd::string appName)
|
||||
: ToolsTestApplication(AZStd::move(appName))
|
||||
{
|
||||
}
|
||||
|
||||
bool PrefabTestToolsApplication::IsPrefabSystemEnabled() const
|
||||
{
|
||||
// Make sure our prefab tests always run with prefabs enabled
|
||||
return true;
|
||||
}
|
||||
|
||||
void PrefabTestFixture::SetUpEditorFixtureImpl()
|
||||
{
|
||||
// Acquire the system entity
|
||||
@@ -32,6 +43,9 @@ namespace UnitTest
|
||||
m_prefabLoaderInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabLoaderInterface>::Get();
|
||||
EXPECT_TRUE(m_prefabLoaderInterface);
|
||||
|
||||
m_prefabPublicInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabPublicInterface>::Get();
|
||||
EXPECT_TRUE(m_prefabPublicInterface);
|
||||
|
||||
m_instanceUpdateExecutorInterface = AZ::Interface<AzToolsFramework::Prefab::InstanceUpdateExecutorInterface>::Get();
|
||||
EXPECT_TRUE(m_instanceUpdateExecutorInterface);
|
||||
|
||||
@@ -41,6 +55,11 @@ namespace UnitTest
|
||||
GetApplication()->RegisterComponentDescriptor(PrefabTestComponent::CreateDescriptor());
|
||||
}
|
||||
|
||||
AZStd::unique_ptr<ToolsTestApplication> PrefabTestFixture::CreateTestApplication()
|
||||
{
|
||||
return AZStd::make_unique<PrefabTestToolsApplication>("PrefabTestApplication");
|
||||
}
|
||||
|
||||
AZ::Entity* PrefabTestFixture::CreateEntity(const char* entityName, const bool shouldActivate)
|
||||
{
|
||||
// Circumvent the EntityContext system and generate a new entity with a transformcomponent
|
||||
|
||||
@@ -31,6 +31,16 @@ namespace UnitTest
|
||||
using namespace AzToolsFramework::Prefab;
|
||||
using namespace PrefabTestUtils;
|
||||
|
||||
class PrefabTestToolsApplication
|
||||
: public ToolsTestApplication
|
||||
{
|
||||
public:
|
||||
PrefabTestToolsApplication(AZStd::string appName);
|
||||
|
||||
// Make sure our prefab tests always run with prefabs enabled
|
||||
bool IsPrefabSystemEnabled() const override;
|
||||
};
|
||||
|
||||
class PrefabTestFixture
|
||||
: public ToolsApplicationFixture,
|
||||
public UnitTest::TraceBusRedirector
|
||||
@@ -45,6 +55,8 @@ namespace UnitTest
|
||||
|
||||
void SetUpEditorFixtureImpl() override;
|
||||
|
||||
AZStd::unique_ptr<ToolsTestApplication> CreateTestApplication() override;
|
||||
|
||||
AZ::Entity* CreateEntity(const char* entityName, const bool shouldActivate = true);
|
||||
|
||||
void CompareInstances(const Instance& instanceA, const Instance& instanceB, bool shouldCompareLinkIds = true,
|
||||
@@ -57,6 +69,7 @@ namespace UnitTest
|
||||
|
||||
PrefabSystemComponent* m_prefabSystemComponent = nullptr;
|
||||
PrefabLoaderInterface* m_prefabLoaderInterface = nullptr;
|
||||
PrefabPublicInterface* m_prefabPublicInterface = nullptr;
|
||||
InstanceUpdateExecutorInterface* m_instanceUpdateExecutorInterface = nullptr;
|
||||
InstanceToTemplateInterface* m_instanceToTemplateInterface = nullptr;
|
||||
};
|
||||
|
||||
@@ -54,6 +54,7 @@ set(FILES
|
||||
Prefab/Spawnable/SpawnableMetaDataTests.cpp
|
||||
Prefab/MockPrefabFileIOActionValidator.cpp
|
||||
Prefab/MockPrefabFileIOActionValidator.h
|
||||
Prefab/PrefabDuplicateTests.cpp
|
||||
Prefab/PrefabEntityAliasTests.cpp
|
||||
Prefab/PrefabInstanceToTemplatePropagatorTests.cpp
|
||||
Prefab/PrefabInstantiateTests.cpp
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include <AzFramework/StringFunc/StringFunc.h>
|
||||
#include <AzFramework/Visibility/BoundsBus.h>
|
||||
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
|
||||
#include <AzToolsFramework/API/EditorEntityAPI.h>
|
||||
#include <AzToolsFramework/API/EntityCompositionRequestBus.h>
|
||||
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
|
||||
#include <AzToolsFramework/AssetBrowser/AssetBrowserBus.h>
|
||||
@@ -192,6 +193,9 @@ void SandboxIntegrationManager::Setup()
|
||||
(m_prefabIntegrationInterface != nullptr),
|
||||
"SandboxIntegrationManager requires a PrefabIntegrationInterface instance to be present on Setup().");
|
||||
|
||||
m_editorEntityAPI = AZ::Interface<AzToolsFramework::EditorEntityAPI>::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<AZ::EntityId> 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<AZ::EntityId> clonedEntities;
|
||||
handled = AzToolsFramework::CloneInstantiatedEntities(duplicationSet, clonedEntities);
|
||||
m_unsavedEntities.insert(clonedEntities.begin(), clonedEntities.end());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user