Merge branch 'main' into Atom/dmcdiar/ATOM-15517

main
Doug McDiarmid 5 years ago
commit d63edf9b84

@ -13,6 +13,7 @@ AllowShortFunctionsOnASingleLine: None
AllowShortLambdasOnASingleLine: None
AlwaysBreakAfterReturnType: None
AlwaysBreakTemplateDeclarations: true
BinPackParameters: false
BreakBeforeBraces: Custom
BraceWrapping:
AfterClass: true

@ -613,6 +613,7 @@ namespace AzToolsFramework
AZStd::unique_ptr<AZ::Entity> Instance::DetachContainerEntity()
{
m_instanceEntityMapper->UnregisterEntity(m_containerEntity->GetId());
return AZStd::move(m_containerEntity);
}
}

@ -48,6 +48,7 @@ namespace AzToolsFramework
using EntityAliasOptionalReference = AZStd::optional<AZStd::reference_wrapper<EntityAlias>>;
using InstanceOptionalReference = AZStd::optional<AZStd::reference_wrapper<Instance>>;
using InstanceOptionalConstReference = AZStd::optional<AZStd::reference_wrapper<const Instance>>;
using InstanceSet = AZStd::unordered_set<Instance*>;
using InstanceSetConstReference = AZStd::optional<AZStd::reference_wrapper<const InstanceSet>>;
using EntityOptionalReference = AZStd::optional<AZStd::reference_wrapper<AZ::Entity>>;
@ -85,6 +86,7 @@ namespace AzToolsFramework
bool AddEntity(AZ::Entity& entity);
bool AddEntity(AZ::Entity& entity, EntityAlias entityAlias);
AZStd::unique_ptr<AZ::Entity> DetachEntity(const AZ::EntityId& entityId);
void DetachEntities(const AZStd::function<void(AZStd::unique_ptr<AZ::Entity>)>& callback);
void DetachNestedEntities(const AZStd::function<void(AZStd::unique_ptr<AZ::Entity>)>& callback);
void RemoveNestedEntities(const AZStd::function<bool(const AZStd::unique_ptr<AZ::Entity>&)>& filter);
@ -182,7 +184,6 @@ namespace AzToolsFramework
void ClearEntities();
void DetachEntities(const AZStd::function<void(AZStd::unique_ptr<AZ::Entity>)>& callback);
void RemoveEntities(const AZStd::function<bool(const AZStd::unique_ptr<AZ::Entity>&)>& filter);
bool RegisterEntity(const AZ::EntityId& entityId, const EntityAlias& entityAlias);

@ -19,6 +19,7 @@
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
#include <AzToolsFramework/Prefab/EditorPrefabComponent.h>
#include <AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h>
#include <AzToolsFramework/Prefab/Instance/Instance.h>
#include <AzToolsFramework/Prefab/Instance/InstanceEntityIdMapper.h>
@ -191,24 +192,7 @@ namespace AzToolsFramework
if (nestedInstanceLinkPatchesMap.contains(nestedInstance.get()))
{
previousPatch = AZStd::move(nestedInstanceLinkPatchesMap[nestedInstance.get()]);
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
previousPatch.Accept(writer);
QString previousPatchString(buffer.GetString());
for (AZ::Entity* entity : entities)
{
AZ::EntityId entityId = entity->GetId();
AZStd::string oldEntityAlias = oldEntityAliases[entityId];
EntityAliasOptionalReference newEntityAlias = instanceToCreate->get().GetEntityAlias(entityId);
AZ_Assert(
newEntityAlias.has_value(),
"Could not fetch entity alias for entity with id '%llu' during prefab creation.",
static_cast<AZ::u64>(entityId));
ReplaceOldAliases(previousPatchString, oldEntityAlias, newEntityAlias->get());
}
previousPatch.Parse(previousPatchString.toUtf8().constData());
UpdateLinkPatchesWithNewEntityAliases(previousPatch, oldEntityAliases, instanceToCreate->get());
}
// These link creations shouldn't be undone because that would put the template in a non-usable state if a user
@ -243,11 +227,33 @@ namespace AzToolsFramework
instanceToCreate->get(), commonRootEntityOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch(),
AZStd::move(patch));
// Reset the transform of the container entity so that the new values aren't saved in the new prefab's dom.
// The new values were saved in the link, so propagation will apply them correctly.
{
AZ::Entity* containerEntity = GetEntityById(containerEntityId);
PrefabDom containerBeforeReset;
m_instanceToTemplateInterface->GenerateDomForEntity(containerBeforeReset, *containerEntity);
AZ::TransformBus::Event(containerEntityId, &AZ::TransformBus::Events::SetParent, AZ::EntityId());
AZ::TransformBus::Event(containerEntityId, &AZ::TransformBus::Events::SetLocalTM, AZ::Transform::CreateIdentity());
PrefabDom containerAfterReset;
m_instanceToTemplateInterface->GenerateDomForEntity(containerAfterReset, *containerEntity);
// Update the state of the entity
PrefabUndoEntityUpdate* state = aznew PrefabUndoEntityUpdate(AZStd::to_string(static_cast<AZ::u64>(containerEntityId)));
state->SetParent(undoBatch.GetUndoBatch());
state->Capture(containerBeforeReset, containerAfterReset, containerEntityId);
state->Redo();
}
// This clears any entities marked as dirty due to reparenting of entities during the process of creating a prefab.
// We are doing this so that the changes in those enities are not queued up twice for propagation.
// We are doing this so that the changes in those entities are not queued up twice for propagation.
AzToolsFramework::ToolsApplicationRequestBus::Broadcast(
&AzToolsFramework::ToolsApplicationRequestBus::Events::ClearDirtyEntities);
// Select Container Entity
{
auto selectionUndo = aznew SelectionCommand({containerEntityId}, "Select Prefab Container Entity");
@ -385,8 +391,8 @@ namespace AzToolsFramework
CreateLink(instanceToCreate->get(), instanceToParentUnder->get().GetTemplateId(), undoBatch.GetUndoBatch(), AZStd::move(patch));
// Update the cache - this prevents these changes from being stored in the regular undo/redo nodes
m_prefabUndoCache.Store(containerEntityId, AZStd::move(containerEntityDomAfter));
AzToolsFramework::ToolsApplicationRequestBus::Broadcast(
&AzToolsFramework::ToolsApplicationRequestBus::Events::ClearDirtyEntities);
}
return AZ::Success();
@ -989,6 +995,123 @@ namespace AzToolsFramework
return AZ::Success();
}
PrefabOperationResult PrefabPublicHandler::DetachPrefab(const AZ::EntityId& containerEntityId)
{
if (!containerEntityId.IsValid())
{
return AZ::Failure(AZStd::string("Cannot detach Prefab Instance with invalid container entity."));
}
if (IsLevelInstanceContainerEntity(containerEntityId))
{
return AZ::Failure(AZStd::string("Cannot detach level Prefab Instance."));
}
InstanceOptionalReference owningInstance = GetOwnerInstanceByEntityId(containerEntityId);
if (owningInstance->get().GetContainerEntityId() != containerEntityId)
{
return AZ::Failure(AZStd::string("Input entity should be its owning Instance's container entity."));
}
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
{
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "Internal::DetachPrefab:UndoCapture");
ScopedUndoBatch undoBatch("Detach Prefab");
InstanceOptionalReference getParentInstanceResult = owningInstance->get().GetParentInstance();
AZ_Assert(getParentInstanceResult.has_value(), "Can't get parent Instance from Instance of given container entity.");
auto& parentInstance = getParentInstanceResult->get();
const auto parentTemplateId = parentInstance.GetTemplateId();
{
auto instancePtr = parentInstance.DetachNestedInstance(owningInstance->get().GetInstanceAlias());
AZ_Assert(instancePtr, "Can't detach selected Instance from its parent Instance.");
RemoveLink(instancePtr, parentTemplateId, undoBatch.GetUndoBatch());
Prefab::PrefabDom instanceDomBefore;
m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomBefore, parentInstance);
AZStd::unordered_map<AZ::EntityId, AZStd::string> oldEntityAliases;
oldEntityAliases.emplace(containerEntityId, instancePtr->GetEntityAlias(containerEntityId)->get());
auto containerEntityPtr = instancePtr->DetachContainerEntity();
auto& containerEntity = *containerEntityPtr.release();
auto editorPrefabComponent = containerEntity.FindComponent<EditorPrefabComponent>();
containerEntity.Deactivate();
const bool editorPrefabComponentRemoved = containerEntity.RemoveComponent(editorPrefabComponent);
AZ_Assert(editorPrefabComponentRemoved, "Remove EditorPrefabComponent failed.");
delete editorPrefabComponent;
containerEntity.Activate();
const bool containerEntityAdded = parentInstance.AddEntity(containerEntity);
AZ_Assert(containerEntityAdded, "Add target Instance's container entity to its parent Instance failed.");
EntityIdList entityIds;
entityIds.emplace_back(containerEntity.GetId());
instancePtr->GetEntities(
[&](AZStd::unique_ptr<AZ::Entity>& entityPtr)
{
oldEntityAliases.emplace(entityPtr->GetId(), instancePtr->GetEntityAlias(entityPtr->GetId())->get());
return true;
});
instancePtr->DetachEntities(
[&](AZStd::unique_ptr<AZ::Entity> entityPtr)
{
auto& entity = *entityPtr.release();
const bool entityAdded = parentInstance.AddEntity(entity);
AZ_Assert(entityAdded, "Add target Instance's entity to its parent Instance failed.");
entityIds.emplace_back(entity.GetId());
});
Prefab::PrefabDom instanceDomAfter;
m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomAfter, parentInstance);
PrefabUndoInstance* command = aznew PrefabUndoInstance("Instance detachment");
command->Capture(instanceDomBefore, instanceDomAfter, parentTemplateId);
command->SetParent(undoBatch.GetUndoBatch());
{
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "Internal::DetachPrefab:RunRedo");
command->RunRedo();
}
const auto instanceTemplateId = instancePtr->GetTemplateId();
auto parentContainerEntityId = parentInstance.GetContainerEntityId();
instancePtr->GetNestedInstances(
[&](AZStd::unique_ptr<Instance>& nestedInstancePtr)
{
//get previous link patch
auto linkRef = m_prefabSystemComponentInterface->FindLink(nestedInstancePtr->GetLinkId());
PrefabDomValueReference linkPatches = linkRef->get().GetLinkPatches();
AZ_Assert(
linkPatches.has_value(), "Unable to get patches on link with id '%llu' during prefab creation.",
nestedInstancePtr->GetLinkId());
PrefabDom linkPatchesCopy;
linkPatchesCopy.CopyFrom(linkPatches->get(), linkPatchesCopy.GetAllocator());
RemoveLink(nestedInstancePtr, instanceTemplateId, undoBatch.GetUndoBatch());
UpdateLinkPatchesWithNewEntityAliases(linkPatchesCopy, oldEntityAliases, parentInstance);
CreateLink(*nestedInstancePtr, parentTemplateId, undoBatch.GetUndoBatch(),
AZStd::move(linkPatchesCopy), true);
});
}
AzToolsFramework::ToolsApplicationRequestBus::Broadcast(
&AzToolsFramework::ToolsApplicationRequestBus::Events::ClearDirtyEntities);
}
return AZ::Success();
}
void PrefabPublicHandler::GenerateContainerEntityTransform(const EntityList& topLevelEntities,
AZ::Vector3& translation, AZ::Quaternion& rotation)
{
@ -1252,5 +1375,30 @@ namespace AzToolsFramework
stringToReplace.replace(oldAliasPathRef, newAliasPathRef);
}
void PrefabPublicHandler::UpdateLinkPatchesWithNewEntityAliases(
PrefabDom& linkPatch,
const AZStd::unordered_map<AZ::EntityId, AZStd::string>& oldEntityAliases,
Instance& newParent)
{
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
linkPatch.Accept(writer);
QString previousPatchString(buffer.GetString());
for (const auto& [entityId, oldEntityAlias] : oldEntityAliases)
{
EntityAliasOptionalReference newEntityAlias = newParent.GetEntityAlias(entityId);
AZ_Assert(
newEntityAlias.has_value(),
"Could not fetch entity alias for entity with id '%llu' during prefab creation.",
static_cast<AZ::u64>(entityId));
ReplaceOldAliases(previousPatchString, oldEntityAlias, newEntityAlias->get());
}
linkPatch.Parse(previousPatchString.toUtf8().constData());
}
} // namespace Prefab
} // namespace AzToolsFramework

@ -64,6 +64,8 @@ namespace AzToolsFramework
PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override;
private:
PrefabOperationResult DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants);
bool RetrieveAndSortPrefabEntitiesAndInstances(const EntityList& inputEntities, Instance& commonRootEntityOwningInstance,
@ -132,7 +134,12 @@ namespace AzToolsFramework
bool IsCyclicalDependencyFound(
InstanceOptionalConstReference instance, const AZStd::unordered_set<AZ::IO::Path>& templateSourcePaths);
void ReplaceOldAliases(QString& stringToReplace, AZStd::string_view oldAlias, AZStd::string_view newAlias);
void UpdateLinkPatchesWithNewEntityAliases(
PrefabDom& linkPatch,
const AZStd::unordered_map<AZ::EntityId, AZStd::string>& oldEntityAliases,
Instance& newParent);
static void ReplaceOldAliases(QString& stringToReplace, AZStd::string_view oldAlias, AZStd::string_view newAlias);
static Instance* GetParentInstance(Instance* instance);
static Instance* GetAncestorOfInstanceThatIsChildOfRoot(const Instance* ancestor, Instance* descendant);

@ -150,6 +150,17 @@ namespace AzToolsFramework
* @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;
/**
* If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting
* the container entity into a regular entity and putting it under the parent prefab, removing the link between this
* instance and the parent, removing links between this instance and it's nested instances, adding entities directly
* owned by this instance under the parent instance.
* Bails if the entity is not a container entity or belongs to the level prefab instance.
* @param containerEntityId The container entity id of the instance to detach.
* @return An outcome object; on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) = 0;
};
} // namespace Prefab

@ -652,7 +652,8 @@ namespace AzToolsFramework
if (instancesValue->get().FindMember(rapidjson::StringRef(instanceAlias.c_str())) == instancesValue->get().MemberEnd())
{
instancesValue->get().AddMember(
rapidjson::StringRef(instanceAlias.c_str()), PrefabDomValue(), targetTemplateDom.GetAllocator());
rapidjson::Value(instanceAlias.c_str(), targetTemplateDom.GetAllocator()), PrefabDomValue(),
targetTemplateDom.GetAllocator());
}
Template& sourceTemplate = sourceTemplateRef->get();

@ -237,6 +237,24 @@ namespace AzToolsFramework
{
deleteAction->setDisabled(true);
}
// Detach Prefab
if (selectedEntities.size() == 1)
{
AZ::EntityId selectedEntity = selectedEntities[0];
if (s_prefabPublicInterface->IsInstanceContainerEntity(selectedEntity) &&
!s_prefabPublicInterface->IsLevelInstanceContainerEntity(selectedEntity))
{
QAction* detachPrefabAction = menu->addAction(QObject::tr("Detach Prefab..."));
QObject::connect(
detachPrefabAction, &QAction::triggered, detachPrefabAction,
[this, selectedEntity]
{
ContextMenu_DetachPrefab(selectedEntity);
});
}
}
}
void PrefabIntegrationManager::HandleSourceFileType(AZStd::string_view sourceFilePath, AZ::EntityId parentId, AZ::Vector3 position) const
@ -392,6 +410,17 @@ namespace AzToolsFramework
}
}
void PrefabIntegrationManager::ContextMenu_DetachPrefab(AZ::EntityId containerEntity)
{
PrefabOperationResult detachPrefabResult =
s_prefabPublicInterface->DetachPrefab(containerEntity);
if (!detachPrefabResult.IsSuccess())
{
WarnUserOfError("Detach Prefab error", detachPrefabResult.GetError());
}
}
void PrefabIntegrationManager::GenerateSuggestedFilenameFromEntities(const EntityIdList& entityIds, AZStd::string& outName)
{
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);

@ -93,6 +93,7 @@ namespace AzToolsFramework
static void ContextMenu_EditPrefab(AZ::EntityId containerEntity);
static void ContextMenu_SavePrefab(AZ::EntityId containerEntity);
static void ContextMenu_DeleteSelected();
static void ContextMenu_DetachPrefab(AZ::EntityId containerEntity);
// Prompt and resolve dialogs
static bool QueryUserForPrefabSaveLocation(

@ -969,7 +969,8 @@ namespace AzToolsFramework
{
// Build up components to display
SharedComponentArray sharedComponentArray;
BuildSharedComponentArray(sharedComponentArray, selectionEntityTypeInfo != SelectionEntityTypeInfo::OnlyStandardEntities);
BuildSharedComponentArray(sharedComponentArray,
!(selectionEntityTypeInfo == SelectionEntityTypeInfo::OnlyStandardEntities || selectionEntityTypeInfo == SelectionEntityTypeInfo::OnlyPrefabEntities));
if (sharedComponentArray.size() == 0)
{

@ -2471,7 +2471,17 @@ namespace AzToolsFramework
break;
}
});
AddAction(
m_actions, { QKeySequence(Qt::Key_U) },
/*ID_VIEWPORTUI_VISIBLE=*/50040, "Toggle ViewportUI", "Hide/Unhide Viewport UI",
[this]()
{
SetViewportUiClusterVisible(m_transformModeClusterId, !m_viewportUiVisible);
SetViewportUiClusterVisible(m_spaceCluster.m_spaceClusterId, !m_viewportUiVisible);
m_viewportUiVisible = !m_viewportUiVisible;
});
EditorMenuRequestBus::Broadcast(&EditorMenuRequests::RestoreEditMenuToDefault);
}

@ -306,6 +306,7 @@ namespace AzToolsFramework
AzFramework::ClickDetector m_clickDetector; //!< Detect different types of mouse click.
AzFramework::CursorState m_cursorState; //!< Track the mouse position and delta movement each frame.
SpaceCluster m_spaceCluster; //!< Related viewport ui state for controlling the current reference space.
bool m_viewportUiVisible = true; //!< Used to hide/show the viewport ui elements.
};
//! The ETCS (EntityTransformComponentSelection) namespace contains functions and data used exclusively by

@ -75,14 +75,14 @@
<widget class="QSvgWidget" name="m_logo" native="true">
<property name="minimumSize">
<size>
<width>161</width>
<height>49</height>
<width>175</width>
<height>66</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>161</width>
<height>49</height>
<width>175</width>
<height>66</height>
</size>
</property>
</widget>

@ -177,6 +177,11 @@ ly_add_target(
Legacy::EditorLib
ProjectManager
)
set_property(SOURCE
CryEdit.cpp
APPEND PROPERTY
COMPILE_DEFINITIONS LY_CMAKE_TARGET="Editor"
)
ly_add_translations(
TARGETS Editor
PREFIX Translations
@ -186,15 +191,8 @@ ly_add_translations(
)
ly_add_dependencies(Editor AssetProcessor)
if(TARGET Editor)
set_property(SOURCE
CryEdit.cpp
APPEND PROPERTY
COMPILE_DEFINITIONS LY_CMAKE_TARGET="Editor"
)
else()
message(FATAL_ERROR "Cannot set LY_CMAKE_TARGET define to Editor as the target doesn't exist anymore."
" Perhaps it has been renamed")
if(LY_FIRST_PROJECT_PATH)
set_property(TARGET Editor APPEND PROPERTY VS_DEBUGGER_COMMAND_ARGUMENTS "--project-path=\"${LY_FIRST_PROJECT_PATH}\"")
endif()
################################################################################

@ -369,3 +369,4 @@
#define ID_TOOLBAR_WIDGET_SPACER_RIGHT 50013
#define ID_TOOLBAR_WIDGET_PLAYCONSOLE_LABEL 50014
#define ID_TOOLBAR_WIDGET_LAST 50020
#define ID_VIEWPORTUI_VISIBLE 50040

@ -42,14 +42,14 @@
<widget class="QSvgWidget" name="m_logo" native="true">
<property name="minimumSize">
<size>
<width>161</width>
<height>49</height>
<width>175</width>
<height>66</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>161</width>
<height>50</height>
<width>175</width>
<height>66</height>
</size>
</property>
</widget>

@ -1,22 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="323px" height="98px" viewBox="0 0 323 98" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Group 12</title>
<defs>
<polygon id="path-1" points="0 97.741 322.084 97.741 322.084 0 0 0"></polygon>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-12" transform="translate(0.000000, 0.000000)">
<path d="M99.7068,20.425 C91.1008,11.686 79.6658,6.841 67.5068,6.782 L67.2838,6.781 C62.9678,6.781 58.7408,7.396 54.6908,8.566 L54.6908,25.339 C58.5158,23.54 62.6988,22.563 67.0218,22.517 C67.1388,22.516 67.2538,22.515 67.3698,22.515 C75.1318,22.515 82.4608,25.519 88.0388,30.996 C93.7828,36.635 96.9708,44.124 97.0168,52.084 C97.0628,60.037 93.9658,67.554 88.2968,73.251 C82.6908,78.884 75.2578,82 67.3558,82.025 L67.2718,82.025 C59.4228,82.025 51.9878,78.959 46.3368,73.393 C40.6918,67.833 37.5578,60.397 37.5108,52.453 C37.4888,48.659 38.1798,44.975 39.5088,41.546 L23.0748,41.546 C19.4908,56.362 23.4348,72.648 34.9328,84.219 C43.5408,92.882 54.9608,97.683 67.0878,97.738 L67.3028,97.738 L67.3058,97.738 C79.3458,97.738 90.7008,93.045 99.2768,84.524 C107.8718,75.984 112.6488,64.62 112.7288,52.524 C112.8088,40.435 108.1838,29.035 99.7068,20.425" id="Fill-1" fill="#FFFFFF"></path>
<path d="M175.6326,27.8629 C175.6326,33.3889 173.9586,38.0879 170.6116,41.9599 C167.2646,45.8319 162.5656,48.4939 156.5146,49.9459 L156.5146,50.3089 C163.6536,51.1969 169.0586,53.3629 172.7296,56.8129 C176.3996,60.2619 178.2356,64.9099 178.2356,70.7579 C178.2356,79.2689 175.1496,85.8939 168.9786,90.6319 C162.8076,95.3719 153.9936,97.7409 142.5386,97.7409 C132.9386,97.7409 124.4286,96.1489 117.0076,92.9609 L117.0076,77.0489 C120.4356,78.7839 124.2076,80.1959 128.3216,81.2839 C132.4356,82.3729 136.5096,82.9179 140.5426,82.9179 C146.7146,82.9179 151.2716,81.8699 154.2156,79.7719 C157.1596,77.6749 158.6326,74.3079 158.6326,69.6679 C158.6326,65.5139 156.9386,62.5699 153.5506,60.8349 C150.1626,59.1009 144.7576,58.2329 137.3366,58.2329 L130.6206,58.2329 L130.6206,43.8939 L137.4576,43.8939 C144.3146,43.8939 149.3246,42.9979 152.4916,41.2019 C155.6576,39.4079 157.2406,36.3319 157.2406,31.9759 C157.2406,25.2809 153.0456,21.9329 144.6566,21.9329 C141.7526,21.9329 138.7986,22.4169 135.7936,23.3849 C132.7886,24.3529 129.4506,26.0279 125.7806,28.4069 L117.1306,15.5199 C125.1956,9.7119 134.8166,6.8079 145.9886,6.8079 C155.1446,6.8079 162.3736,8.6639 167.6786,12.3739 C172.9806,16.0869 175.6326,21.2499 175.6326,27.8629" id="Fill-3" fill="#FFFFFF"></path>
<path d="M241.8563,51.9425 C241.8563,32.9455 233.4653,23.4465 216.6883,23.4465 L206.7053,23.4465 L206.7053,81.0435 L214.7523,81.0435 C232.8213,81.0435 241.8563,71.3435 241.8563,51.9425 M261.3363,51.4595 C261.3363,66.0195 257.1933,77.1715 248.9043,84.9165 C240.6153,92.6605 228.6463,96.5325 212.9973,96.5325 L187.9503,96.5325 L187.9503,8.0815 L215.7193,8.0815 C230.1593,8.0815 241.3723,11.8925 249.3583,19.5155 C257.3433,27.1365 261.3363,37.7855 261.3363,51.4595" id="Fill-5" fill="#FFFFFF"></path>
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Clip-8"></g>
<polygon id="Fill-7" fill="#FFFFFF" mask="url(#mask-2)" points="23.185 30.421 45.046 30.421 45.046 8.56 23.185 8.56"></polygon>
<polygon id="Fill-9" fill="#FFFFFF" mask="url(#mask-2)" points="5.251 9.038 14.289 9.038 14.289 0 5.251 0"></polygon>
<polygon id="Fill-10" fill="#FFFFFF" mask="url(#mask-2)" points="0 36.195 14.18 36.195 14.18 22.015 0 22.015"></polygon>
<polygon id="Fill-11" fill="#FFFFFF" mask="url(#mask-2)" points="322.0838 96.4337 271.0538 96.4337 271.0538 7.8287 322.0838 7.8287 322.0838 23.2227 289.8418 23.2227 289.8418 42.6767 319.8418 42.6767 319.8418 58.0707 289.8418 58.0707 289.8418 80.9187 322.0838 80.9187"></polygon>
<svg width="350px" height="133px" viewBox="0 0 350 133" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Artboard</title>
<g id="Artboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="O3DE-Logo-with-WorkMark-REV-Mono" transform="translate(-0.060000, 0.000000)" fill="#FFFFFF">
<g id="Group-3" transform="translate(25.446000, 117.517000)">
<path d="M1.833,7.614 C1.833,9.618 2.262,11.139 3.121,12.175 C3.979,13.211 5.226,13.729 6.863,13.729 C8.511,13.729 9.758,13.212 10.599,12.18 C11.441,11.146 11.862,9.625 11.862,7.614 C11.862,5.623 11.443,4.113 10.604,3.083 C9.766,2.055 8.525,1.539 6.883,1.539 C5.234,1.539 3.979,2.057 3.121,3.093 C2.262,4.129 1.833,5.637 1.833,7.614 Z M13.694,7.614 C13.694,9.983 13.092,11.846 11.887,13.202 C10.683,14.559 9.007,15.237 6.863,15.237 C4.67,15.237 2.979,14.571 1.787,13.237 C0.596,11.904 3.55271368e-15,10.023 3.55271368e-15,7.594 C3.55271368e-15,5.184 0.597,3.315 1.792,1.989 C2.986,0.663 4.683,0 6.883,0 C9.022,0 10.692,0.676 11.892,2.025 C13.095,3.375 13.694,5.238 13.694,7.614 L13.694,7.614 Z" id="Fill-1"></path>
</g>
<path d="M55.659,125.252 L57.216,125.252 C58.75,125.252 59.86,125.005 60.546,124.513 C61.231,124.02 61.574,123.231 61.574,122.144 C61.574,121.165 61.251,120.436 60.606,119.956 C59.961,119.478 58.958,119.239 57.593,119.239 L55.659,119.239 L55.659,125.252 Z M63.366,122.063 C63.366,123.561 62.852,124.714 61.823,125.521 C60.795,126.327 59.324,126.731 57.41,126.731 L55.659,126.731 L55.659,132.551 L53.927,132.551 L53.927,117.749 L57.786,117.749 C61.506,117.749 63.366,119.188 63.366,122.063 L63.366,122.063 Z" id="Fill-4"></path>
<polyline id="Fill-5" points="86.247 132.551 77.949 132.551 77.949 117.749 86.247 117.749 86.247 119.279 79.681 119.279 79.681 124.047 85.849 124.047 85.849 125.566 79.681 125.566 79.681 131.013 86.247 131.013 86.247 132.551"></polyline>
<g id="Group-9" transform="translate(101.016000, 117.537000)">
<path d="M11.628,15.014 L9.651,15.014 L1.517,2.592 L1.436,2.592 C1.544,4.05 1.599,5.387 1.599,6.602 L1.599,15.014 L-1.42108547e-14,15.014 L-1.42108547e-14,0.212 L1.954,0.212 L10.069,12.584 L10.151,12.584 C10.137,12.403 10.106,11.817 10.059,10.828 C10.011,9.839 9.995,9.133 10.008,8.707 L10.008,0.212 L11.628,0.212 L11.628,15.014" id="Fill-6"></path>
<path d="M56.235,3.696 C56.235,4.64 55.968,5.413 55.436,6.014 C54.902,6.616 54.147,7.016 53.169,7.219 L53.169,7.3 C54.364,7.449 55.251,7.827 55.828,8.434 C56.404,9.041 56.693,9.837 56.693,10.823 C56.693,12.234 56.201,13.319 55.216,14.079 C54.232,14.837 52.834,15.217 51.022,15.217 C50.234,15.217 49.513,15.159 48.858,15.039 C48.203,14.922 47.567,14.714 46.949,14.417 L46.949,12.818 C47.594,13.135 48.281,13.376 49.011,13.541 C49.741,13.707 50.431,13.789 51.083,13.789 C53.655,13.789 54.942,12.788 54.942,10.783 C54.942,8.987 53.522,8.089 50.685,8.089 L49.219,8.089 L49.219,6.642 L50.707,6.642 C51.866,6.642 52.787,6.387 53.465,5.877 C54.144,5.369 54.483,4.661 54.483,3.756 C54.483,3.034 54.234,2.467 53.735,2.055 C53.236,1.644 52.559,1.438 51.704,1.438 C51.052,1.438 50.438,1.526 49.861,1.702 C49.284,1.877 48.626,2.201 47.886,2.673 L47.031,1.539 C47.642,1.06 48.345,0.683 49.144,0.411 C49.94,0.136 50.78,1.42108547e-14 51.663,1.42108547e-14 C53.109,1.42108547e-14 54.232,0.33 55.033,0.987 C55.834,1.646 56.235,2.548 56.235,3.696" id="Fill-8"></path>
</g>
<path d="M182.497,125.07 C182.497,123.14 182.009,121.685 181.035,120.706 C180.061,119.727 178.614,119.239 176.693,119.239 L174.178,119.239 L174.178,131.064 L176.286,131.064 C178.349,131.064 179.9,130.56 180.939,129.55 C181.976,128.541 182.497,127.048 182.497,125.07 Z M184.329,125.009 C184.329,127.452 183.662,129.321 182.329,130.613 C180.995,131.906 179.076,132.551 176.571,132.551 L172.447,132.551 L172.447,117.749 L177.008,117.749 C179.323,117.749 181.122,118.388 182.405,119.663 C183.687,120.94 184.329,122.721 184.329,125.009 L184.329,125.009 Z" id="Fill-10"></path>
<polyline id="Fill-11" points="228.264 132.551 219.965 132.551 219.965 117.749 228.264 117.749 228.264 119.279 221.697 119.279 221.697 124.047 227.867 124.047 227.867 125.566 221.697 125.566 221.697 131.013 228.264 131.013 228.264 132.551"></polyline>
<g id="Group-15" transform="translate(243.032000, 117.537000)">
<path d="M11.629,15.014 L9.652,15.014 L1.517,2.592 L1.436,2.592 C1.545,4.05 1.599,5.387 1.599,6.602 L1.599,15.014 L2.84217094e-14,15.014 L2.84217094e-14,0.212 L1.955,0.212 L10.07,12.584 L10.151,12.584 C10.138,12.403 10.107,11.817 10.059,10.828 C10.011,9.839 9.996,9.133 10.008,8.707 L10.008,0.212 L11.629,0.212 L11.629,15.014" id="Fill-12"></path>
<path d="M33.736,7.259 L38.796,7.259 L38.796,14.458 C38.008,14.707 37.208,14.897 36.393,15.025 C35.578,15.154 34.634,15.217 33.563,15.217 C31.309,15.217 29.554,14.551 28.299,13.217 C27.044,11.884 26.415,10.017 26.415,7.614 C26.415,6.076 26.726,4.727 27.346,3.569 C27.968,2.413 28.862,1.528 30.03,0.916 C31.197,0.306 32.564,1.42108547e-14 34.133,1.42108547e-14 C35.721,1.42108547e-14 37.201,0.29 38.573,0.872 L37.9,2.39 C36.555,1.823 35.263,1.539 34.021,1.539 C32.209,1.539 30.794,2.076 29.776,3.149 C28.757,4.222 28.248,5.71 28.248,7.614 C28.248,9.612 28.738,11.126 29.719,12.16 C30.7,13.192 32.14,13.709 34.042,13.709 C35.073,13.709 36.081,13.59 37.066,13.354 L37.066,8.798 L33.736,8.798 L33.736,7.259" id="Fill-14"></path>
</g>
<polygon id="Fill-16" points="296.871 132.551 298.602 132.551 298.602 117.749 296.871 117.749"></polygon>
<path d="M325.781,132.551 L323.805,132.551 L315.67,120.129 L315.588,120.129 C315.698,121.587 315.751,122.924 315.751,124.139 L315.751,132.551 L314.153,132.551 L314.153,117.749 L316.108,117.749 L324.223,130.121 L324.304,130.121 C324.29,129.94 324.259,129.354 324.212,128.365 C324.164,127.376 324.148,126.67 324.162,126.244 L324.162,117.749 L325.781,117.749 L325.781,132.551" id="Fill-17"></path>
<polyline id="Fill-18" points="349.64 132.551 341.341 132.551 341.341 117.749 349.64 117.749 349.64 119.279 343.073 119.279 343.073 124.047 349.243 124.047 349.243 125.566 343.073 125.566 343.073 131.013 349.64 131.013 349.64 132.551"></polyline>
<path d="M187.562,26.549 C187.562,32.293 185.822,37.178 182.341,41.203 C178.861,45.228 173.977,47.995 167.687,49.505 L167.687,49.882 C175.109,50.805 180.727,53.058 184.543,56.643 C188.358,60.228 190.266,65.061 190.266,71.14 C190.266,79.989 187.059,86.875 180.644,91.801 C174.228,96.728 165.067,99.191 153.159,99.191 C143.18,99.191 134.332,97.535 126.618,94.222 L126.618,77.681 C130.181,79.485 134.102,80.951 138.379,82.083 C142.656,83.216 146.891,83.781 151.083,83.781 C157.498,83.781 162.236,82.693 165.297,80.511 C168.358,78.331 169.889,74.831 169.889,70.008 C169.889,65.69 168.128,62.63 164.606,60.825 C161.083,59.023 155.464,58.121 147.75,58.121 L140.769,58.121 L140.769,43.216 L147.876,43.216 C155.004,43.216 160.213,42.283 163.505,40.417 C166.796,38.551 168.442,35.354 168.442,30.825 C168.442,23.865 164.081,20.385 155.36,20.385 C152.341,20.385 149.269,20.888 146.146,21.894 C143.022,22.901 139.552,24.642 135.737,27.115 L126.744,13.718 C135.129,7.68 145.129,4.661 156.744,4.661 C166.262,4.661 173.777,6.591 179.291,10.448 C184.804,14.305 187.562,19.673 187.562,26.549" id="Fill-19"></path>
<path d="M261.093,51.538 C261.093,31.236 252.127,21.084 234.196,21.084 L223.527,21.084 L223.527,82.639 L232.126,82.639 C251.437,82.639 261.093,72.273 261.093,51.538 Z M281.913,51.021 C281.913,66.583 277.484,78.5 268.625,86.777 C259.768,95.053 246.976,99.191 230.251,99.191 L203.483,99.191 L203.483,4.661 L233.162,4.661 C248.592,4.661 260.576,8.735 269.111,16.882 C277.646,25.029 281.913,36.408 281.913,51.021 L281.913,51.021 Z" id="Fill-20"></path>
<polyline id="Fill-21" points="349.64 99.191 295.198 99.191 295.198 4.661 349.64 4.661 349.64 21.084 315.242 21.084 315.242 41.84 347.247 41.84 347.247 58.262 315.242 58.262 315.242 82.639 349.64 82.639 349.64 99.191"></polyline>
<path d="M72.71,4.661 C68.177,4.661 63.799,5.313 59.648,6.505 L59.648,24.983 C63.596,23.066 68.027,21.988 72.71,21.988 C89.245,21.988 102.648,35.392 102.648,51.926 C102.648,68.46 89.245,81.865 72.71,81.865 C56.176,81.865 42.773,68.46 42.773,51.926 C42.773,47.99 43.539,44.237 44.92,40.794 L26.777,40.794 C25.914,44.365 25.446,48.09 25.446,51.926 C25.446,78.03 46.607,99.191 72.71,99.191 C98.814,99.191 119.976,78.03 119.976,51.926 C119.976,25.823 98.814,4.661 72.71,4.661" id="Fill-22"></path>
<g id="Group-27">
<polyline id="Fill-23" points="53.023 29.655 30.237 34.445 25.447 11.66 48.232 6.869 53.023 29.655"></polyline>
<polygon id="Fill-25" points="11.806 9.313 21.12 9.313 21.12 0 11.806 0"></polygon>
<polyline id="Fill-26" points="14.761 31.326 0 27.533 3.793 12.773 18.554 16.566 14.761 31.326"></polyline>
</g>
</g>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

@ -125,6 +125,10 @@ ly_add_target(
AZ::AssetProcessorBatch.Static
)
if(LY_FIRST_PROJECT_PATH)
set_property(TARGET AssetProcessor AssetProcessorBatch APPEND PROPERTY VS_DEBUGGER_COMMAND_ARGUMENTS "--project-path=\"${LY_FIRST_PROJECT_PATH}\"")
endif()
# Adds the AssetProcessorBatch target as a C preprocessor define so that it can be used as a Settings Registry
# specialization in order to look up the generated .setreg which contains the dependencies
# specified for the target.

@ -25,7 +25,6 @@ ly_add_target(
OUTPUT_NAME o3de
NAMESPACE AZ
AUTOMOC
AUTOUIC
AUTORCC
FILES_CMAKE
project_manager_files.cmake

@ -232,6 +232,18 @@ QTabBar::tab:pressed
margin-left:30px;
}
#projectSettingsTab::tab-bar {
left: 60px;
}
#projectSettingsTabBar::tab {
height:50px;
}
#projectSettingsTopFrame {
background-color:#1E252F;
}
/************** Projects **************/
#firstTimeContent > #titleLabel {
font-size:60px;

@ -15,7 +15,6 @@
#include <PythonBindingsInterface.h>
#include <NewProjectSettingsScreen.h>
#include <ScreenHeaderWidget.h>
#include <GemCatalog/GemCatalogScreen.h>
#include <QDialogButtonBox>
#include <QHBoxLayout>
@ -42,9 +41,10 @@ namespace O3DE::ProjectManager
m_stack = new QStackedWidget(this);
m_stack->setObjectName("body");
m_stack->setSizePolicy(QSizePolicy(QSizePolicy::Preferred,QSizePolicy::Expanding));
m_stack->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding));
m_stack->addWidget(new NewProjectSettingsScreen());
m_stack->addWidget(new GemCatalogScreen());
m_gemCatalog = new GemCatalogScreen();
m_stack->addWidget(m_gemCatalog);
vLayout->addWidget(m_stack);
QDialogButtonBox* backNextButtons = new QDialogButtonBox();
@ -88,6 +88,7 @@ namespace O3DE::ProjectManager
emit GotoPreviousScreenRequest();
}
}
void CreateProjectCtrl::HandleNextButton()
{
ScreenWidget* currentScreen = reinterpret_cast<ScreenWidget*>(m_stack->currentWidget());
@ -106,6 +107,9 @@ namespace O3DE::ProjectManager
m_projectInfo = newProjectScreen->GetProjectInfo();
m_projectTemplatePath = newProjectScreen->GetProjectTemplatePath();
// The next page is the gem catalog. Gather the available gems that will be shown in the gem catalog.
m_gemCatalog->ReinitForProject(m_projectInfo.m_path, /*isNewProject=*/true);
}
}
@ -129,6 +133,9 @@ namespace O3DE::ProjectManager
{
QMessageBox::critical(this, tr("Project creation failed"), tr("Failed to create project."));
}
// Enable/disable gems for the newly created project.
m_gemCatalog->EnableDisableGemsForProject(m_projectInfo.m_path);
}
}

@ -14,6 +14,7 @@
#if !defined(Q_MOC_RUN)
#include <ScreenWidget.h>
#include <ProjectInfo.h>
#include <GemCatalog/GemCatalogScreen.h>
#endif
QT_FORWARD_DECLARE_CLASS(QStackedWidget)
@ -48,6 +49,8 @@ namespace O3DE::ProjectManager
QString m_projectTemplatePath;
ProjectInfo m_projectInfo;
GemCatalogScreen* m_gemCatalog = nullptr;
};
} // namespace O3DE::ProjectManager

@ -15,13 +15,12 @@
#include <GemCatalog/GemCatalogHeaderWidget.h>
#include <GemCatalog/GemListHeaderWidget.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <GemCatalog/GemFilterWidget.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QTimer>
//#define USE_TESTGEMDATA
#include <PythonBindingsInterface.h>
#include <QMessageBox>
namespace O3DE::ProjectManager
{
@ -29,47 +28,32 @@ namespace O3DE::ProjectManager
: ScreenWidget(parent)
{
m_gemModel = new GemModel(this);
GemSortFilterProxyModel* proxyModel = new GemSortFilterProxyModel(m_gemModel, this);
m_proxModel = new GemSortFilterProxyModel(m_gemModel, this);
QVBoxLayout* vLayout = new QVBoxLayout();
vLayout->setMargin(0);
vLayout->setSpacing(0);
setLayout(vLayout);
GemCatalogHeaderWidget* headerWidget = new GemCatalogHeaderWidget(proxyModel);
GemCatalogHeaderWidget* headerWidget = new GemCatalogHeaderWidget(m_proxModel);
vLayout->addWidget(headerWidget);
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->setMargin(0);
vLayout->addLayout(hLayout);
m_gemListView = new GemListView(proxyModel, proxyModel->GetSelectionModel(), this);
m_gemListView = new GemListView(m_proxModel, m_proxModel->GetSelectionModel(), this);
m_gemInspector = new GemInspector(m_gemModel, this);
m_gemInspector->setFixedWidth(320);
// Start: Temporary gem test data
#ifdef USE_TESTGEMDATA
QVector<GemInfo> testGemData = GenerateTestData();
for (const GemInfo& gemInfo : testGemData)
{
m_gemModel->AddGem(gemInfo);
}
#else
// End: Temporary gem test data
auto result = PythonBindingsInterface::Get()->GetGems();
if (result.IsSuccess())
{
for (auto gemInfo : result.GetValue())
{
m_gemModel->AddGem(gemInfo);
}
}
#endif
m_gemInspector->setFixedWidth(240);
GemFilterWidget* filterWidget = new GemFilterWidget(proxyModel);
filterWidget->setFixedWidth(250);
QWidget* filterWidget = new QWidget(this);
filterWidget->setFixedWidth(240);
m_filterWidgetLayout = new QVBoxLayout();
m_filterWidgetLayout->setMargin(0);
m_filterWidgetLayout->setSpacing(0);
filterWidget->setLayout(m_filterWidgetLayout);
GemListHeaderWidget* listHeaderWidget = new GemListHeaderWidget(proxyModel);
GemListHeaderWidget* listHeaderWidget = new GemListHeaderWidget(m_proxModel);
QVBoxLayout* middleVLayout = new QVBoxLayout();
middleVLayout->setMargin(0);
@ -80,98 +64,111 @@ namespace O3DE::ProjectManager
hLayout->addWidget(filterWidget);
hLayout->addLayout(middleVLayout);
hLayout->addWidget(m_gemInspector);
}
void GemCatalogScreen::ReinitForProject(const QString& projectPath, bool isNewProject)
{
m_gemModel->clear();
FillModel(projectPath, isNewProject);
if (m_filterWidget)
{
m_filterWidget->hide();
m_filterWidget->deleteLater();
}
m_filterWidget = new GemFilterWidget(m_proxModel);
m_filterWidgetLayout->addWidget(m_filterWidget);
m_proxModel->InvalidateFilter();
// Select the first entry after everything got correctly sized
QTimer::singleShot(200, [=]{
QModelIndex firstModelIndex = m_gemListView->model()->index(0,0);
m_gemListView->selectionModel()->select(firstModelIndex, QItemSelectionModel::ClearAndSelect);
});
}
void GemCatalogScreen::FillModel(const QString& projectPath, bool isNewProject)
{
AZ::Outcome<QVector<GemInfo>, AZStd::string> allGemInfosResult;
if (isNewProject)
{
allGemInfosResult = PythonBindingsInterface::Get()->GetEngineGemInfos();
}
else
{
allGemInfosResult = PythonBindingsInterface::Get()->GetAllGemInfos(projectPath);
}
if (allGemInfosResult.IsSuccess())
{
// Add all available gems to the model.
const QVector<GemInfo> allGemInfos = allGemInfosResult.GetValue();
for (const GemInfo& gemInfo : allGemInfos)
{
m_gemModel->AddGem(gemInfo);
}
proxyModel->InvalidateFilter();
// Gather enabled gems for the given project.
auto enabledGemNamesResult = PythonBindingsInterface::Get()->GetEnabledGemNames(projectPath);
if (enabledGemNamesResult.IsSuccess())
{
const QVector<AZStd::string> enabledGemNames = enabledGemNamesResult.GetValue();
for (const AZStd::string& enabledGemName : enabledGemNames)
{
const QModelIndex modelIndex = m_gemModel->FindIndexByNameString(enabledGemName.c_str());
if (modelIndex.isValid())
{
GemModel::SetWasPreviouslyAdded(*m_gemModel, modelIndex, true);
GemModel::SetIsAdded(*m_gemModel, modelIndex, true);
}
else
{
AZ_Warning("ProjectManager::GemCatalog", false,
"Cannot find entry for gem with name '%s'. The CMake target name probably does not match the specified name in the gem.json.",
enabledGemName.c_str());
}
}
}
else
{
QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve enabled gems for project %1.\n\nError:\n%2").arg(projectPath, enabledGemNamesResult.GetError().c_str()));
}
}
else
{
QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve gems for %1.\n\nError:\n%2").arg(projectPath, allGemInfosResult.GetError().c_str()));
}
}
QVector<GemInfo> GemCatalogScreen::GenerateTestData()
void GemCatalogScreen::EnableDisableGemsForProject(const QString& projectPath)
{
QVector<GemInfo> result;
GemInfo gem("EMotion FX",
"O3DE Foundation",
"EMFX is a real-time character animation system. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
(GemInfo::Android | GemInfo::iOS | GemInfo::macOS | GemInfo::Windows | GemInfo::Linux),
true);
gem.m_directoryLink = "C:/";
gem.m_documentationLink = "http://www.amazon.com";
gem.m_dependingGemUuids = QStringList({"EMotionFX", "Atom"});
gem.m_conflictingGemUuids = QStringList({"Vegetation", "Camera", "ScriptCanvas", "CloudCanvas", "Networking"});
gem.m_types = (GemInfo::Code | GemInfo::Asset);
gem.m_version = "v1.01";
gem.m_lastUpdatedDate = "24th April 2021";
gem.m_binarySizeInKB = 40;
gem.m_features = QStringList({"Animation", "Assets", "Physics"});
gem.m_gemOrigin = GemInfo::O3DEFoundation;
result.push_back(gem);
gem.m_name = "Atom";
gem.m_creator = "O3DE Seattle";
gem.m_summary = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
gem.m_platforms = (GemInfo::Android | GemInfo::Windows | GemInfo::Linux | GemInfo::macOS);
gem.m_isAdded = true;
gem.m_directoryLink = "C:/";
gem.m_documentationLink = "https://aws.amazon.com/gametech/";
gem.m_dependingGemUuids = QStringList({"EMotionFX", "Core", "AudioSystem", "Camera", "Particles"});
gem.m_conflictingGemUuids = QStringList({"CloudCanvas", "NovaNet"});
gem.m_version = "v2.31";
gem.m_lastUpdatedDate = "24th November 2020";
gem.m_features = QStringList({"Assets", "Rendering", "UI", "VR", "Debug", "Environment"});
gem.m_binarySizeInKB = 2087;
result.push_back(gem);
gem.m_name = "Physics";
gem.m_creator = "O3DE London";
gem.m_summary = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
gem.m_platforms = (GemInfo::Android | GemInfo::Linux | GemInfo::macOS);
gem.m_isAdded = true;
gem.m_directoryLink = "C:/";
gem.m_documentationLink = "https://aws.amazon.com/gametech/";
gem.m_dependingGemUuids = QStringList({"GraphCanvas", "ExpressionEvaluation", "UI Lib", "Multiplayer", "GameStateSamples"});
gem.m_conflictingGemUuids = QStringList({"Cloud Canvas", "EMotion FX", "Streaming", "MessagePopup", "Cloth", "Graph Canvas", "Twitch Integration"});
gem.m_version = "v1.5.102145";
gem.m_lastUpdatedDate = "1st January 2021";
gem.m_binarySizeInKB = 2000000;
gem.m_features = QStringList({"Physics", "Gameplay", "Debug", "Assets"});
result.push_back(gem);
result.push_back(O3DE::ProjectManager::GemInfo("Certificate Manager",
"O3DE Irvine",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
GemInfo::Windows,
false));
result.push_back(O3DE::ProjectManager::GemInfo("Cloud Gem Framework",
"O3DE Seattle",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
GemInfo::iOS | GemInfo::Linux,
false));
result.push_back(O3DE::ProjectManager::GemInfo("Cloud Gem Core",
"O3DE Foundation",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
GemInfo::Android | GemInfo::Windows | GemInfo::Linux,
true));
result.push_back(O3DE::ProjectManager::GemInfo("Gestures",
"O3DE Foundation",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
GemInfo::Android | GemInfo::Windows | GemInfo::Linux,
false));
result.push_back(O3DE::ProjectManager::GemInfo("Effects System",
"O3DE Foundation",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
GemInfo::Android | GemInfo::Windows | GemInfo::Linux,
true));
result.push_back(O3DE::ProjectManager::GemInfo("Microphone",
"O3DE Foundation",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus euismod ligula vitae dui dictum, a sodales dolor luctus. Sed id elit dapibus, finibus neque sed, efficitur mi. Nam facilisis ligula at eleifend pellentesque. Praesent non ex consectetur, blandit tellus in, venenatis lacus. Duis nec neque in urna ullamcorper euismod id eu leo. Nam efficitur dolor sed odio vehicula venenatis. Suspendisse nec est non velit commodo cursus in sit amet dui. Ut bibendum nisl et libero hendrerit dapibus. Vestibulum ultrices ullamcorper urna, placerat porttitor est lobortis in. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer a magna ac tellus sollicitudin porttitor. Phasellus lobortis viverra justo id bibendum. Etiam ac pharetra risus. Nulla vitae justo nibh. Nulla viverra leo et molestie interdum. Duis sit amet bibendum nulla, sit amet vehicula augue.",
GemInfo::Android | GemInfo::Windows | GemInfo::Linux,
false));
return result;
IPythonBindings* pythonBindings = PythonBindingsInterface::Get();
QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded();
QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved();
for (const QModelIndex& modelIndex : toBeAdded)
{
const QString gemPath = GemModel::GetPath(modelIndex);
const AZ::Outcome<void, AZStd::string> result = pythonBindings->AddGemToProject(gemPath, projectPath);
if (!result.IsSuccess())
{
QMessageBox::critical(nullptr, "Operation failed",
QString("Cannot add gem %1 to project.\n\nError:\n%2").arg(GemModel::GetName(modelIndex), result.GetError().c_str()));
}
}
for (const QModelIndex& modelIndex : toBeRemoved)
{
const QString gemPath = GemModel::GetPath(modelIndex);
const AZ::Outcome<void, AZStd::string> result = pythonBindings->RemoveGemFromProject(gemPath, projectPath);
if (!result.IsSuccess())
{
QMessageBox::critical(nullptr, "Operation failed",
QString("Cannot remove gem %1 from project.\n\nError:\n%2").arg(GemModel::GetName(modelIndex), result.GetError().c_str()));
}
}
}
ProjectManagerScreen GemCatalogScreen::GetScreenEnum()

@ -14,9 +14,11 @@
#if !defined(Q_MOC_RUN)
#include <ScreenWidget.h>
#include <GemCatalog/GemFilterWidget.h>
#include <GemCatalog/GemListView.h>
#include <GemCatalog/GemInspector.h>
#include <GemCatalog/GemModel.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#endif
namespace O3DE::ProjectManager
@ -29,11 +31,17 @@ namespace O3DE::ProjectManager
~GemCatalogScreen() = default;
ProjectManagerScreen GetScreenEnum() override;
void ReinitForProject(const QString& projectPath, bool isNewProject);
void EnableDisableGemsForProject(const QString& projectPath);
private:
QVector<GemInfo> GenerateTestData();
void FillModel(const QString& projectPath, bool isNewProject);
GemListView* m_gemListView = nullptr;
GemInspector* m_gemInspector = nullptr;
GemModel* m_gemModel = nullptr;
GemSortFilterProxyModel* m_proxModel = nullptr;
QVBoxLayout* m_filterWidgetLayout = nullptr;
GemFilterWidget* m_filterWidget = nullptr;
};
} // namespace O3DE::ProjectManager

@ -79,4 +79,9 @@ namespace O3DE::ProjectManager
{
return (m_platforms & platform);
}
bool GemInfo::operator<(const GemInfo& gemInfo) const
{
return (m_displayName < gemInfo.m_displayName);
}
} // namespace O3DE::ProjectManager

@ -61,6 +61,8 @@ namespace O3DE::ProjectManager
bool IsValid() const;
bool operator<(const GemInfo& gemInfo) const;
QString m_path;
QString m_name = "Unknown Gem Name";
QString m_displayName = "Unknown Gem Name";

@ -131,6 +131,22 @@ namespace O3DE::ProjectManager
return false;
}
if (event->type() == QEvent::MouseButtonPress)
{
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
QRect fullRect, itemRect, contentRect;
CalcRects(option, fullRect, itemRect, contentRect);
const QRect buttonRect = CalcButtonRect(contentRect);
if (buttonRect.contains(mouseEvent->pos()))
{
const bool isAdded = GemModel::IsAdded(modelIndex);
GemModel::SetIsAdded(*model, modelIndex, !isAdded);
return true;
}
}
return QStyledItemDelegate::editorEvent(event, model, option, modelIndex);
}

@ -38,6 +38,7 @@ namespace O3DE::ProjectManager
item->setData(aznumeric_cast<int>(gemInfo.m_platforms), RolePlatforms);
item->setData(aznumeric_cast<int>(gemInfo.m_types), RoleTypes);
item->setData(gemInfo.m_summary, RoleSummary);
item->setData(false, RoleWasPreviouslyAdded);
item->setData(gemInfo.m_isAdded, RoleIsAdded);
item->setData(gemInfo.m_directoryLink, RoleDirectoryLink);
item->setData(gemInfo.m_documentationLink, RoleDocLink);
@ -47,6 +48,7 @@ namespace O3DE::ProjectManager
item->setData(gemInfo.m_lastUpdatedDate, RoleLastUpdated);
item->setData(gemInfo.m_binarySizeInKB, RoleBinarySize);
item->setData(gemInfo.m_features, RoleFeatures);
item->setData(gemInfo.m_path, RolePath);
appendRow(item);
@ -89,11 +91,6 @@ namespace O3DE::ProjectManager
return modelIndex.data(RoleSummary).toString();
}
bool GemModel::IsAdded(const QModelIndex& modelIndex)
{
return modelIndex.data(RoleIsAdded).toBool();
}
QString GemModel::GetDirectoryLink(const QModelIndex& modelIndex)
{
return modelIndex.data(RoleDirectoryLink).toString();
@ -180,4 +177,62 @@ namespace O3DE::ProjectManager
{
return modelIndex.data(RoleFeatures).toStringList();
}
QString GemModel::GetPath(const QModelIndex& modelIndex)
{
return modelIndex.data(RolePath).toString();
}
bool GemModel::IsAdded(const QModelIndex& modelIndex)
{
return modelIndex.data(RoleIsAdded).toBool();
}
void GemModel::SetIsAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded)
{
model.setData(modelIndex, isAdded, RoleIsAdded);
}
void GemModel::SetWasPreviouslyAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool wasAdded)
{
model.setData(modelIndex, wasAdded, RoleWasPreviouslyAdded);
}
bool GemModel::NeedsToBeAdded(const QModelIndex& modelIndex)
{
return (!modelIndex.data(RoleWasPreviouslyAdded).toBool() && modelIndex.data(RoleIsAdded).toBool());
}
bool GemModel::NeedsToBeRemoved(const QModelIndex& modelIndex)
{
return (modelIndex.data(RoleWasPreviouslyAdded).toBool() && !modelIndex.data(RoleIsAdded).toBool());
}
QVector<QModelIndex> GemModel::GatherGemsToBeAdded() const
{
QVector<QModelIndex> result;
for (int row = 0; row < rowCount(); ++row)
{
const QModelIndex modelIndex = index(row, 0);
if (NeedsToBeAdded(modelIndex))
{
result.push_back(modelIndex);
}
}
return result;
}
QVector<QModelIndex> GemModel::GatherGemsToBeRemoved() const
{
QVector<QModelIndex> result;
for (int row = 0; row < rowCount(); ++row)
{
const QModelIndex modelIndex = index(row, 0);
if (NeedsToBeRemoved(modelIndex))
{
result.push_back(modelIndex);
}
}
return result;
}
} // namespace O3DE::ProjectManager

@ -46,13 +46,22 @@ namespace O3DE::ProjectManager
static GemInfo::Platforms GetPlatforms(const QModelIndex& modelIndex);
static GemInfo::Types GetTypes(const QModelIndex& modelIndex);
static QString GetSummary(const QModelIndex& modelIndex);
static bool IsAdded(const QModelIndex& modelIndex);
static QString GetDirectoryLink(const QModelIndex& modelIndex);
static QString GetDocLink(const QModelIndex& modelIndex);
static QString GetVersion(const QModelIndex& modelIndex);
static QString GetLastUpdated(const QModelIndex& modelIndex);
static int GetBinarySizeInKB(const QModelIndex& modelIndex);
static QStringList GetFeatures(const QModelIndex& modelIndex);
static QString GetPath(const QModelIndex& modelIndex);
static bool IsAdded(const QModelIndex& modelIndex);
static void SetIsAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded);
static void SetWasPreviouslyAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool wasAdded);
static bool NeedsToBeAdded(const QModelIndex& modelIndex);
static bool NeedsToBeRemoved(const QModelIndex& modelIndex);
QVector<QModelIndex> GatherGemsToBeAdded() const;
QVector<QModelIndex> GatherGemsToBeRemoved() const;
private:
enum UserRole
@ -62,6 +71,7 @@ namespace O3DE::ProjectManager
RoleGemOrigin,
RolePlatforms,
RoleSummary,
RoleWasPreviouslyAdded,
RoleIsAdded,
RoleDirectoryLink,
RoleDocLink,
@ -71,7 +81,8 @@ namespace O3DE::ProjectManager
RoleLastUpdated,
RoleBinarySize,
RoleFeatures,
RoleTypes
RoleTypes,
RolePath
};
QHash<QString, QModelIndex> m_nameToIndexMap;

@ -34,83 +34,57 @@ namespace O3DE::ProjectManager
constexpr const char* k_pathProperty = "Path";
NewProjectSettingsScreen::NewProjectSettingsScreen(QWidget* parent)
: ScreenWidget(parent)
: ProjectSettingsScreen(parent)
{
QHBoxLayout* hLayout = new QHBoxLayout(this);
hLayout->setAlignment(Qt::AlignLeft);
hLayout->setContentsMargins(0,0,0,0);
// if we don't provide a parent for this box layout the stylesheet doesn't take
// if we don't set this in a frame (just use a sub-layout) all the content will align incorrectly horizontally
QFrame* projectSettingsFrame = new QFrame(this);
projectSettingsFrame->setObjectName("projectSettings");
QVBoxLayout* vLayout = new QVBoxLayout(this);
// you cannot remove content margins in qss
vLayout->setContentsMargins(0,0,0,0);
vLayout->setAlignment(Qt::AlignTop);
const QString defaultName{ "NewProject" };
const QString defaultPath = QDir::toNativeSeparators(GetDefaultProjectPath() + "/" + defaultName);
m_projectName->lineEdit()->setText(defaultName);
m_projectPath->lineEdit()->setText(defaultPath);
// if we don't use a QFrame we cannot "contain" the widgets inside and move them around
// as a group
QFrame* projectTemplateWidget = new QFrame(this);
projectTemplateWidget->setObjectName("projectTemplate");
QVBoxLayout* containerLayout = new QVBoxLayout();
containerLayout->setAlignment(Qt::AlignTop);
{
const QString defaultName{ "NewProject" };
const QString defaultPath = QDir::toNativeSeparators(GetDefaultProjectPath() + "/" + defaultName);
m_projectName = new FormLineEditWidget(tr("Project name"), defaultName, this);
connect(m_projectName->lineEdit(), &QLineEdit::textChanged, this, &NewProjectSettingsScreen::ValidateProjectPath);
vLayout->addWidget(m_projectName);
m_projectPath = new FormBrowseEditWidget(tr("Project Location"), defaultPath, this);
m_projectPath->lineEdit()->setReadOnly(true);
connect(m_projectPath->lineEdit(), &QLineEdit::textChanged, this, &NewProjectSettingsScreen::ValidateProjectPath);
vLayout->addWidget(m_projectPath);
// if we don't use a QFrame we cannot "contain" the widgets inside and move them around
// as a group
QFrame* projectTemplateWidget = new QFrame(this);
projectTemplateWidget->setObjectName("projectTemplate");
QVBoxLayout* containerLayout = new QVBoxLayout();
containerLayout->setAlignment(Qt::AlignTop);
QLabel* projectTemplateLabel = new QLabel(tr("Select a Project Template"));
projectTemplateLabel->setObjectName("projectTemplateLabel");
containerLayout->addWidget(projectTemplateLabel);
QLabel* projectTemplateDetailsLabel = new QLabel(tr("Project templates are pre-configured with relevant Gems that provide "
"additional functionality and content to the project."));
projectTemplateDetailsLabel->setWordWrap(true);
projectTemplateDetailsLabel->setObjectName("projectTemplateDetailsLabel");
containerLayout->addWidget(projectTemplateDetailsLabel);
QHBoxLayout* templateLayout = new QHBoxLayout(this);
containerLayout->addItem(templateLayout);
m_projectTemplateButtonGroup = new QButtonGroup(this);
m_projectTemplateButtonGroup->setObjectName("templateButtonGroup");
auto templatesResult = PythonBindingsInterface::Get()->GetProjectTemplates();
if (templatesResult.IsSuccess() && !templatesResult.GetValue().isEmpty())
{
QLabel* projectTemplateLabel = new QLabel(tr("Select a Project Template"));
projectTemplateLabel->setObjectName("projectTemplateLabel");
containerLayout->addWidget(projectTemplateLabel);
QLabel* projectTemplateDetailsLabel = new QLabel(tr("Project templates are pre-configured with relevant Gems that provide "
"additional functionality and content to the project."));
projectTemplateDetailsLabel->setWordWrap(true);
projectTemplateDetailsLabel->setObjectName("projectTemplateDetailsLabel");
containerLayout->addWidget(projectTemplateDetailsLabel);
QHBoxLayout* templateLayout = new QHBoxLayout(this);
containerLayout->addItem(templateLayout);
m_projectTemplateButtonGroup = new QButtonGroup(this);
m_projectTemplateButtonGroup->setObjectName("templateButtonGroup");
auto templatesResult = PythonBindingsInterface::Get()->GetProjectTemplates();
if (templatesResult.IsSuccess() && !templatesResult.GetValue().isEmpty())
for (const ProjectTemplateInfo& projectTemplate : templatesResult.GetValue())
{
for (auto projectTemplate : templatesResult.GetValue())
{
QRadioButton* radioButton = new QRadioButton(projectTemplate.m_name, this);
radioButton->setProperty(k_pathProperty, projectTemplate.m_path);
m_projectTemplateButtonGroup->addButton(radioButton);
QRadioButton* radioButton = new QRadioButton(projectTemplate.m_name, this);
radioButton->setProperty(k_pathProperty, projectTemplate.m_path);
m_projectTemplateButtonGroup->addButton(radioButton);
containerLayout->addWidget(radioButton);
}
m_projectTemplateButtonGroup->buttons().first()->setChecked(true);
containerLayout->addWidget(radioButton);
}
m_projectTemplateButtonGroup->buttons().first()->setChecked(true);
}
projectTemplateWidget->setLayout(containerLayout);
vLayout->addWidget(projectTemplateWidget);
}
projectSettingsFrame->setLayout(vLayout);
hLayout->addWidget(projectSettingsFrame);
projectTemplateWidget->setLayout(containerLayout);
m_verticalLayout->addWidget(projectTemplateWidget);
QWidget* projectTemplateDetails = new QWidget(this);
projectTemplateDetails->setObjectName("projectTemplateDetails");
hLayout->addWidget(projectTemplateDetails);
this->setLayout(hLayout);
m_horizontalLayout->addWidget(projectTemplateDetails);
}
QString NewProjectSettingsScreen::GetDefaultProjectPath()
@ -133,69 +107,13 @@ namespace O3DE::ProjectManager
return ProjectManagerScreen::NewProjectSettings;
}
void NewProjectSettingsScreen::ValidateProjectPath()
{
Validate();
}
void NewProjectSettingsScreen::NotifyCurrentScreen()
{
Validate();
}
ProjectInfo NewProjectSettingsScreen::GetProjectInfo()
{
ProjectInfo projectInfo;
projectInfo.m_projectName = m_projectName->lineEdit()->text();
projectInfo.m_path = m_projectPath->lineEdit()->text();
return projectInfo;
}
QString NewProjectSettingsScreen::GetProjectTemplatePath()
{
return m_projectTemplateButtonGroup->checkedButton()->property(k_pathProperty).toString();
}
bool NewProjectSettingsScreen::Validate()
{
bool projectPathIsValid = true;
if (m_projectPath->lineEdit()->text().isEmpty())
{
projectPathIsValid = false;
m_projectPath->setErrorLabelText(tr("Please provide a valid location."));
}
else
{
QDir path(m_projectPath->lineEdit()->text());
if (path.exists() && !path.isEmpty())
{
projectPathIsValid = false;
m_projectPath->setErrorLabelText(tr("This folder exists and isn't empty. Please choose a different location."));
}
}
bool projectNameIsValid = true;
if (m_projectName->lineEdit()->text().isEmpty())
{
projectNameIsValid = false;
m_projectName->setErrorLabelText(tr("Please provide a project name."));
}
else
{
// this validation should roughly match the utils.validate_identifier which the cli
// uses to validate project names
QRegExp validProjectNameRegex("[A-Za-z][A-Za-z0-9_-]{0,63}");
const bool result = validProjectNameRegex.exactMatch(m_projectName->lineEdit()->text());
if (!result)
{
projectNameIsValid = false;
m_projectName->setErrorLabelText(tr("Project names must start with a letter and consist of up to 64 letter, number, '_' or '-' characters"));
}
}
m_projectName->setErrorLabelVisible(!projectNameIsValid);
m_projectPath->setErrorLabelVisible(!projectPathIsValid);
return projectNameIsValid && projectPathIsValid;
}
} // namespace O3DE::ProjectManager

@ -12,41 +12,28 @@
#pragma once
#if !defined(Q_MOC_RUN)
#include <ScreenWidget.h>
#include <ProjectInfo.h>
#include <ProjectSettingsScreen.h>
#endif
QT_FORWARD_DECLARE_CLASS(QButtonGroup)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(FormLineEditWidget)
QT_FORWARD_DECLARE_CLASS(FormBrowseEditWidget)
class NewProjectSettingsScreen
: public ScreenWidget
: public ProjectSettingsScreen
{
public:
explicit NewProjectSettingsScreen(QWidget* parent = nullptr);
~NewProjectSettingsScreen() = default;
ProjectManagerScreen GetScreenEnum() override;
ProjectInfo GetProjectInfo();
QString GetProjectTemplatePath();
bool Validate();
void NotifyCurrentScreen() override;
protected slots:
void HandleBrowseButton();
void ValidateProjectPath();
private:
QString GetDefaultProjectPath();
FormLineEditWidget* m_projectName;
FormBrowseEditWidget* m_projectPath;
QButtonGroup* m_projectTemplateButtonGroup;
};

@ -22,8 +22,6 @@
#include <QMenu>
#include <QSpacerItem>
//#define SHOW_ALL_PROJECT_ACTIONS
namespace O3DE::ProjectManager
{
inline constexpr static int s_projectImageWidth = 210;
@ -96,10 +94,6 @@ namespace O3DE::ProjectManager
m_removeProjectAction = newProjectMenu->addAction(tr("Remove from O3DE"));
m_deleteProjectAction = newProjectMenu->addAction(tr("Delete this Project"));
#ifdef SHOW_ALL_PROJECT_ACTIONS
m_editProjectGemsAction = newProjectMenu->addAction(tr("Cutomize Gems..."));
#endif
QFrame* footer = new QFrame(this);
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->setContentsMargins(0, 0, 0, 0);
@ -121,10 +115,6 @@ namespace O3DE::ProjectManager
connect(m_copyProjectAction, &QAction::triggered, [this]() { emit CopyProject(m_projectInfo.m_path); });
connect(m_removeProjectAction, &QAction::triggered, [this]() { emit RemoveProject(m_projectInfo.m_path); });
connect(m_deleteProjectAction, &QAction::triggered, [this]() { emit DeleteProject(m_projectInfo.m_path); });
#ifdef SHOW_ALL_PROJECT_ACTIONS
connect(m_editProjectGemsAction, &QAction::triggered, [this]() { emit EditProjectGems(m_projectInfo.m_path); });
#endif
}
void ProjectButton::SetButtonEnabled(bool enabled)

@ -62,7 +62,6 @@ namespace O3DE::ProjectManager
signals:
void OpenProject(const QString& projectName);
void EditProject(const QString& projectName);
void EditProjectGems(const QString& projectName);
void CopyProject(const QString& projectName);
void RemoveProject(const QString& projectName);
void DeleteProject(const QString& projectName);
@ -73,7 +72,6 @@ namespace O3DE::ProjectManager
ProjectInfo m_projectInfo;
LabelButton* m_projectImageLabel;
QAction* m_editProjectAction;
QAction* m_editProjectGemsAction;
QAction* m_copyProjectAction;
QAction* m_removeProjectAction;
QAction* m_deleteProjectAction;

@ -25,6 +25,19 @@ namespace O3DE::ProjectManager
{
}
bool ProjectInfo::operator==(const ProjectInfo& rhs)
{
return m_path == rhs.m_path
&& m_projectName == rhs.m_projectName
&& m_imagePath == rhs.m_imagePath
&& m_backgroundImagePath == rhs.m_backgroundImagePath;
}
bool ProjectInfo::operator!=(const ProjectInfo& rhs)
{
return !operator==(rhs);
}
bool ProjectInfo::IsValid() const
{
return !m_path.isEmpty() && !m_projectName.isEmpty();

@ -25,6 +25,8 @@ namespace O3DE::ProjectManager
ProjectInfo() = default;
ProjectInfo(const QString& path, const QString& projectName, const QString& displayName,
const QString& imagePath, const QString& backgroundImagePath, bool isNew);
bool operator==(const ProjectInfo& rhs);
bool operator!=(const ProjectInfo& rhs);
bool IsValid() const;

@ -11,45 +11,131 @@
*/
#include <ProjectSettingsScreen.h>
#include <FormBrowseEditWidget.h>
#include <FormLineEditWidget.h>
#include <PathValidator.h>
#include <PythonBindingsInterface.h>
#include <Source/ui_ProjectSettingsScreen.h>
#include <QFileDialog>
#include <QFrame>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QStandardPaths>
namespace O3DE::ProjectManager
{
ProjectSettingsScreen::ProjectSettingsScreen(QWidget* parent)
: ScreenWidget(parent)
, m_ui(new Ui::ProjectSettingsClass())
{
m_ui->setupUi(this);
m_horizontalLayout = new QHBoxLayout(this);
m_horizontalLayout->setAlignment(Qt::AlignLeft);
m_horizontalLayout->setContentsMargins(0, 0, 0, 0);
connect(m_ui->gemsButton, &QPushButton::pressed, this, &ProjectSettingsScreen::HandleGemsButton);
// if we don't provide a parent for this box layout the stylesheet doesn't take
// if we don't set this in a frame (just use a sub-layout) all the content will align incorrectly horizontally
QFrame* projectSettingsFrame = new QFrame(this);
projectSettingsFrame->setObjectName("projectSettings");
m_verticalLayout = new QVBoxLayout(this);
// you cannot remove content margins in qss
m_verticalLayout->setContentsMargins(0, 0, 0, 0);
m_verticalLayout->setAlignment(Qt::AlignTop);
m_projectName = new FormLineEditWidget(tr("Project name"), "", this);
connect(m_projectName->lineEdit(), &QLineEdit::textChanged, this, &ProjectSettingsScreen::ValidateProjectName);
m_verticalLayout->addWidget(m_projectName);
m_projectPath = new FormBrowseEditWidget(tr("Project Location"), "", this);
m_projectPath->lineEdit()->setReadOnly(true);
connect(m_projectPath->lineEdit(), &QLineEdit::textChanged, this, &ProjectSettingsScreen::Validate);
m_verticalLayout->addWidget(m_projectPath);
projectSettingsFrame->setLayout(m_verticalLayout);
m_horizontalLayout->addWidget(projectSettingsFrame);
setLayout(m_horizontalLayout);
}
ProjectManagerScreen ProjectSettingsScreen::GetScreenEnum()
{
return ProjectManagerScreen::ProjectSettings;
return ProjectManagerScreen::Invalid;
}
ProjectInfo ProjectSettingsScreen::GetProjectInfo()
QString ProjectSettingsScreen::GetDefaultProjectPath()
{
// Impl pending next PR
return ProjectInfo();
QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
if (engineInfoResult.IsSuccess())
{
QDir path(QDir::toNativeSeparators(engineInfoResult.GetValue().m_defaultProjectsFolder));
if (path.exists())
{
defaultPath = path.absolutePath();
}
}
return defaultPath;
}
void ProjectSettingsScreen::SetProjectInfo()
ProjectInfo ProjectSettingsScreen::GetProjectInfo()
{
// Impl pending next PR
ProjectInfo projectInfo;
projectInfo.m_projectName = m_projectName->lineEdit()->text();
projectInfo.m_path = m_projectPath->lineEdit()->text();
return projectInfo;
}
bool ProjectSettingsScreen::Validate()
bool ProjectSettingsScreen::ValidateProjectName()
{
// Impl pending next PR
return true;
}
bool projectNameIsValid = true;
if (m_projectName->lineEdit()->text().isEmpty())
{
projectNameIsValid = false;
m_projectName->setErrorLabelText(tr("Please provide a project name."));
}
else
{
// this validation should roughly match the utils.validate_identifier which the cli
// uses to validate project names
QRegExp validProjectNameRegex("[A-Za-z][A-Za-z0-9_-]{0,63}");
const bool result = validProjectNameRegex.exactMatch(m_projectName->lineEdit()->text());
if (!result)
{
projectNameIsValid = false;
m_projectName->setErrorLabelText(
tr("Project names must start with a letter and consist of up to 64 letter, number, '_' or '-' characters"));
}
}
void ProjectSettingsScreen::HandleGemsButton()
m_projectName->setErrorLabelVisible(!projectNameIsValid);
return projectNameIsValid;
}
bool ProjectSettingsScreen::ValidateProjectPath()
{
emit ChangeScreenRequest(ProjectManagerScreen::GemCatalog);
bool projectPathIsValid = true;
if (m_projectPath->lineEdit()->text().isEmpty())
{
projectPathIsValid = false;
m_projectPath->setErrorLabelText(tr("Please provide a valid location."));
}
else
{
QDir path(m_projectPath->lineEdit()->text());
if (path.exists() && !path.isEmpty())
{
projectPathIsValid = false;
m_projectPath->setErrorLabelText(tr("This folder exists and isn't empty. Please choose a different location."));
}
}
m_projectPath->setErrorLabelVisible(!projectPathIsValid);
return projectPathIsValid;
}
bool ProjectSettingsScreen::Validate()
{
return ValidateProjectName() && ValidateProjectPath();
}
} // namespace O3DE::ProjectManager

@ -12,17 +12,18 @@
#pragma once
#if !defined(Q_MOC_RUN)
#include <ScreenWidget.h>
#include <ProjectInfo.h>
#include <ScreenWidget.h>
#endif
namespace Ui
{
class ProjectSettingsClass;
}
QT_FORWARD_DECLARE_CLASS(QHBoxLayout)
QT_FORWARD_DECLARE_CLASS(QVBoxLayout)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(FormLineEditWidget)
QT_FORWARD_DECLARE_CLASS(FormBrowseEditWidget)
class ProjectSettingsScreen
: public ScreenWidget
{
@ -32,15 +33,20 @@ namespace O3DE::ProjectManager
ProjectManagerScreen GetScreenEnum() override;
ProjectInfo GetProjectInfo();
void SetProjectInfo();
bool Validate();
protected slots:
void HandleGemsButton();
virtual bool ValidateProjectName();
virtual bool ValidateProjectPath();
protected:
QString GetDefaultProjectPath();
private:
QScopedPointer<Ui::ProjectSettingsClass> m_ui;
QHBoxLayout* m_horizontalLayout;
QVBoxLayout* m_verticalLayout;
FormLineEditWidget* m_projectName;
FormBrowseEditWidget* m_projectPath;
};
} // namespace O3DE::ProjectManager

@ -1,113 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ProjectSettingsClass</class>
<widget class="QWidget" name="ProjectSettingsClass">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>782</width>
<height>579</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="projectSettingsButton">
<property name="text">
<string>Project Settings</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="gemsButton">
<property name="text">
<string>Gems</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>761</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Project Name</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit"/>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Project Location</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_2"/>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Project Image Location</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_3"/>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Project Background Image Location</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_4"/>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

@ -182,10 +182,6 @@ namespace O3DE::ProjectManager
connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject);
connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject);
connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject);
#ifdef SHOW_ALL_PROJECT_ACTIONS
connect(projectButton, &ProjectButton::EditProjectGems, this, &ProjectsScreen::HandleEditProjectGems);
#endif
}
layout->addWidget(projectsScrollArea);
@ -293,14 +289,8 @@ namespace O3DE::ProjectManager
void ProjectsScreen::HandleEditProject(const QString& projectPath)
{
emit NotifyCurrentProject(projectPath);
emit ResetScreenRequest(ProjectManagerScreen::UpdateProject);
emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
}
void ProjectsScreen::HandleEditProjectGems(const QString& projectPath)
{
emit NotifyCurrentProject(projectPath);
emit ChangeScreenRequest(ProjectManagerScreen::GemCatalog);
}
void ProjectsScreen::HandleCopyProject(const QString& projectPath)
{
// Open file dialog and choose location for copied project then register copy with O3DE

@ -41,7 +41,6 @@ namespace O3DE::ProjectManager
void HandleAddProjectButton();
void HandleOpenProject(const QString& projectPath);
void HandleEditProject(const QString& projectPath);
void HandleEditProjectGems(const QString& projectPath);
void HandleCopyProject(const QString& projectPath);
void HandleRemoveProject(const QString& projectPath);
void HandleDeleteProject(const QString& projectPath);

@ -283,12 +283,16 @@ namespace O3DE::ProjectManager
AZ_Warning("ProjectManagerWindow", result != -1, "Append to sys path failed");
// import required modules
m_cmake = pybind11::module::import("o3de.cmake");
m_register = pybind11::module::import("o3de.register");
m_manifest = pybind11::module::import("o3de.manifest");
m_engineTemplate = pybind11::module::import("o3de.engine_template");
m_enableGemProject = pybind11::module::import("o3de.enable_gem");
m_disableGemProject = pybind11::module::import("o3de.disable_gem");
// make sure the engine is registered
RegisterThisEngine();
return result == 0 && !PyErr_Occurred();
} catch ([[maybe_unused]] const std::exception& e)
{
@ -311,7 +315,37 @@ namespace O3DE::ProjectManager
return !PyErr_Occurred();
}
bool PythonBindings::ExecuteWithLock(AZStd::function<void()> executionCallback)
bool PythonBindings::RegisterThisEngine()
{
bool registrationResult = true; // already registered is considered successful
bool pythonResult = ExecuteWithLock(
[&]
{
// check current engine path against all other registered engines
// to see if we are already registered
auto allEngines = m_manifest.attr("get_engines")();
if (pybind11::isinstance<pybind11::list>(allEngines))
{
for (auto engine : allEngines)
{
AZ::IO::FixedMaxPath enginePath(Py_To_String(engine["path"]));
if (enginePath.Compare(m_enginePath) == 0)
{
return;
}
}
}
auto result = m_register.attr("register")(m_enginePath.c_str());
registrationResult = (result.cast<int>() == 0);
});
bool finalResult = (registrationResult && pythonResult);
AZ_Assert(finalResult, "Registration of this engine failed!");
return finalResult;
}
AZ::Outcome<void, AZStd::string> PythonBindings::ExecuteWithLockErrorHandling(AZStd::function<void()> executionCallback)
{
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_release release;
@ -320,13 +354,19 @@ namespace O3DE::ProjectManager
try
{
executionCallback();
return true;
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("PythonBindings", false, "Python exception %s", e.what());
return false;
return AZ::Failure<AZStd::string>(e.what());
}
return AZ::Success();
}
bool PythonBindings::ExecuteWithLock(AZStd::function<void()> executionCallback)
{
return ExecuteWithLockErrorHandling(executionCallback).IsSuccess();
}
AZ::Outcome<EngineInfo> PythonBindings::GetEngineInfo()
@ -419,7 +459,7 @@ namespace O3DE::ProjectManager
return result;
}
AZ::Outcome<GemInfo> PythonBindings::GetGem(const QString& path)
AZ::Outcome<GemInfo> PythonBindings::GetGemInfo(const QString& path)
{
GemInfo gemInfo = GemInfoFromPath(pybind11::str(path.toStdString()));
if (gemInfo.IsValid())
@ -432,32 +472,79 @@ namespace O3DE::ProjectManager
}
}
AZ::Outcome<QVector<GemInfo>> PythonBindings::GetGems()
AZ::Outcome<QVector<GemInfo>, AZStd::string> PythonBindings::GetEngineGemInfos()
{
QVector<GemInfo> gems;
bool result = ExecuteWithLock([&] {
// external gems
for (auto path : m_manifest.attr("get_gems")())
auto result = ExecuteWithLockErrorHandling([&]
{
gems.push_back(GemInfoFromPath(path));
}
for (auto path : m_manifest.attr("get_engine_gems")())
{
gems.push_back(GemInfoFromPath(path));
}
});
if (!result.IsSuccess())
{
return AZ::Failure<AZStd::string>(result.GetError().c_str());
}
std::sort(gems.begin(), gems.end());
return AZ::Success(AZStd::move(gems));
}
// gems from the engine
for (auto path : m_manifest.attr("get_engine_gems")())
AZ::Outcome<QVector<GemInfo>, AZStd::string> PythonBindings::GetAllGemInfos(const QString& projectPath)
{
QVector<GemInfo> gems;
auto result = ExecuteWithLockErrorHandling([&]
{
gems.push_back(GemInfoFromPath(path));
}
});
pybind11::str pyProjectPath = projectPath.toStdString();
for (auto path : m_manifest.attr("get_all_gems")(pyProjectPath))
{
gems.push_back(GemInfoFromPath(path));
}
});
if (!result.IsSuccess())
{
return AZ::Failure<AZStd::string>(result.GetError().c_str());
}
if (!result)
std::sort(gems.begin(), gems.end());
return AZ::Success(AZStd::move(gems));
}
AZ::Outcome<QVector<AZStd::string>, AZStd::string> PythonBindings::GetEnabledGemNames(const QString& projectPath)
{
// Retrieve the path to the cmake file that lists the enabled gems.
pybind11::str enabledGemsFilename;
auto result = ExecuteWithLockErrorHandling([&]
{
const pybind11::str pyProjectPath = projectPath.toStdString();
enabledGemsFilename = m_cmake.attr("get_enabled_gem_cmake_file")(
pybind11::none(), // project_name
pyProjectPath); // project_path
});
if (!result.IsSuccess())
{
return AZ::Failure();
return AZ::Failure<AZStd::string>(result.GetError().c_str());
}
else
// Retrieve the actual list of names from the cmake file.
QVector<AZStd::string> gemNames;
result = ExecuteWithLockErrorHandling([&]
{
const auto pyGemNames = m_cmake.attr("get_enabled_gems")(enabledGemsFilename);
for (auto gemName : pyGemNames)
{
gemNames.push_back(Py_To_String(gemName));
}
});
if (!result.IsSuccess())
{
return AZ::Success(AZStd::move(gems));
return AZ::Failure<AZStd::string>(result.GetError().c_str());
}
return AZ::Success(AZStd::move(gemNames));
}
bool PythonBindings::AddProject(const QString& path)
@ -637,38 +724,36 @@ namespace O3DE::ProjectManager
}
}
bool PythonBindings::AddGemToProject(const QString& gemPath, const QString& projectPath)
AZ::Outcome<void, AZStd::string> PythonBindings::AddGemToProject(const QString& gemPath, const QString& projectPath)
{
bool result = ExecuteWithLock([&] {
pybind11::str pyGemPath = gemPath.toStdString();
pybind11::str pyProjectPath = projectPath.toStdString();
m_enableGemProject.attr("enable_gem_in_project")(
pybind11::none(), // gem_name
pyGemPath,
pybind11::none(), // project_name
pyProjectPath
);
});
return result;
return ExecuteWithLockErrorHandling([&]
{
pybind11::str pyGemPath = gemPath.toStdString();
pybind11::str pyProjectPath = projectPath.toStdString();
m_enableGemProject.attr("enable_gem_in_project")(
pybind11::none(), // gem name not needed as path is provided
pyGemPath,
pybind11::none(), // project name not needed as path is provided
pyProjectPath
);
});
}
bool PythonBindings::RemoveGemFromProject(const QString& gemPath, const QString& projectPath)
AZ::Outcome<void, AZStd::string> PythonBindings::RemoveGemFromProject(const QString& gemPath, const QString& projectPath)
{
bool result = ExecuteWithLock([&] {
pybind11::str pyGemPath = gemPath.toStdString();
pybind11::str pyProjectPath = projectPath.toStdString();
m_disableGemProject.attr("disable_gem_in_project")(
pybind11::none(), // gem_name
pyGemPath,
pybind11::none(), // project_name
pyProjectPath
);
});
return result;
return ExecuteWithLockErrorHandling([&]
{
pybind11::str pyGemPath = gemPath.toStdString();
pybind11::str pyProjectPath = projectPath.toStdString();
m_disableGemProject.attr("disable_gem_in_project")(
pybind11::none(), // gem name not needed as path is provided
pyGemPath,
pybind11::none(), // project name not needed as path is provided
pyProjectPath
);
});
}
bool PythonBindings::UpdateProject([[maybe_unused]] const ProjectInfo& projectInfo)

@ -39,8 +39,10 @@ namespace O3DE::ProjectManager
bool SetEngineInfo(const EngineInfo& engineInfo) override;
// Gem
AZ::Outcome<GemInfo> GetGem(const QString& path) override;
AZ::Outcome<QVector<GemInfo>> GetGems() override;
AZ::Outcome<GemInfo> GetGemInfo(const QString& path) override;
AZ::Outcome<QVector<GemInfo>, AZStd::string> GetEngineGemInfos() override;
AZ::Outcome<QVector<GemInfo>, AZStd::string> GetAllGemInfos(const QString& projectPath) override;
AZ::Outcome<QVector<AZStd::string>, AZStd::string> GetEnabledGemNames(const QString& projectPath) override;
// Project
AZ::Outcome<ProjectInfo> CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo) override;
@ -49,8 +51,8 @@ namespace O3DE::ProjectManager
bool AddProject(const QString& path) override;
bool RemoveProject(const QString& path) override;
bool UpdateProject(const ProjectInfo& projectInfo) override;
bool AddGemToProject(const QString& gemPath, const QString& projectPath) override;
bool RemoveGemFromProject(const QString& gemPath, const QString& projectPath) override;
AZ::Outcome<void, AZStd::string> AddGemToProject(const QString& gemPath, const QString& projectPath) override;
AZ::Outcome<void, AZStd::string> RemoveGemFromProject(const QString& gemPath, const QString& projectPath) override;
// ProjectTemplate
AZ::Outcome<QVector<ProjectTemplateInfo>> GetProjectTemplates() override;
@ -58,16 +60,20 @@ namespace O3DE::ProjectManager
private:
AZ_DISABLE_COPY_MOVE(PythonBindings);
AZ::Outcome<void, AZStd::string> ExecuteWithLockErrorHandling(AZStd::function<void()> executionCallback);
bool ExecuteWithLock(AZStd::function<void()> executionCallback);
GemInfo GemInfoFromPath(pybind11::handle path);
ProjectInfo ProjectInfoFromPath(pybind11::handle path);
ProjectTemplateInfo ProjectTemplateInfoFromPath(pybind11::handle path);
bool RegisterThisEngine();
bool StartPython();
bool StopPython();
AZ::IO::FixedMaxPath m_enginePath;
pybind11::handle m_engineTemplate;
AZStd::recursive_mutex m_lock;
pybind11::handle m_cmake;
pybind11::handle m_register;
pybind11::handle m_manifest;
pybind11::handle m_enableGemProject;

@ -57,13 +57,27 @@ namespace O3DE::ProjectManager
* @param path the absolute path to the Gem
* @return an outcome with GemInfo on success
*/
virtual AZ::Outcome<GemInfo> GetGem(const QString& path) = 0;
virtual AZ::Outcome<GemInfo> GetGemInfo(const QString& path) = 0;
/**
* Get info about all known Gems
* @return an outcome with GemInfos on success
* Get all available gem infos. This concatenates gems registered by the engine and the project.
* @param path The absolute path to the project.
* @return A list of gem infos.
*/
virtual AZ::Outcome<QVector<GemInfo>> GetGems() = 0;
virtual AZ::Outcome<QVector<GemInfo>, AZStd::string> GetAllGemInfos(const QString& projectPath) = 0;
/**
* Get engine gem infos.
* @return A list of all registered gem infos.
*/
virtual AZ::Outcome<QVector<GemInfo>, AZStd::string> GetEngineGemInfos() = 0;
/**
* Get a list of all enabled gem names for a given project.
* @param[in] projectPath Absolute file path to the project.
* @return A list of gem names of all the enabled gems for a given project or a error message on failure.
*/
virtual AZ::Outcome<QVector<AZStd::string>, AZStd::string> GetEnabledGemNames(const QString& projectPath) = 0;
// Projects
@ -114,17 +128,17 @@ namespace O3DE::ProjectManager
* Add a gem to a project
* @param gemPath the absolute path to the gem
* @param projectPath the absolute path to the project
* @return true on success, false on failure
* @return An outcome with the success flag as well as an error message in case of a failure.
*/
virtual bool AddGemToProject(const QString& gemPath, const QString& projectPath) = 0;
virtual AZ::Outcome<void, AZStd::string> AddGemToProject(const QString& gemPath, const QString& projectPath) = 0;
/**
* Remove gem to a project
* @param gemPath the absolute path to the gem
* @param projectPath the absolute path to the project
* @return true on success, false on failure
* @return An outcome with the success flag as well as an error message in case of a failure.
*/
virtual bool RemoveGemFromProject(const QString& gemPath, const QString& projectPath) = 0;
virtual AZ::Outcome<void, AZStd::string> RemoveGemFromProject(const QString& gemPath, const QString& projectPath) = 0;
// Project Templates

@ -26,7 +26,7 @@ namespace O3DE::ProjectManager
GemCatalog,
Projects,
UpdateProject,
ProjectSettings,
UpdateProjectSettings,
EngineSettings
};
@ -37,7 +37,7 @@ namespace O3DE::ProjectManager
{ "GemCatalog", ProjectManagerScreen::GemCatalog},
{ "Projects", ProjectManagerScreen::Projects},
{ "UpdateProject", ProjectManagerScreen::UpdateProject},
{ "ProjectSettings", ProjectManagerScreen::ProjectSettings},
{ "UpdateProjectSettings", ProjectManagerScreen::UpdateProjectSettings},
{ "EngineSettings", ProjectManagerScreen::EngineSettings}
};

@ -16,7 +16,7 @@
#include <NewProjectSettingsScreen.h>
#include <GemCatalog/GemCatalogScreen.h>
#include <ProjectsScreen.h>
#include <ProjectSettingsScreen.h>
#include <UpdateProjectSettingsScreen.h>
#include <EngineSettingsScreen.h>
namespace O3DE::ProjectManager
@ -42,8 +42,8 @@ namespace O3DE::ProjectManager
case (ProjectManagerScreen::UpdateProject):
newScreen = new UpdateProjectCtrl(parent);
break;
case (ProjectManagerScreen::ProjectSettings):
newScreen = new ProjectSettingsScreen(parent);
case (ProjectManagerScreen::UpdateProjectSettings):
newScreen = new UpdateProjectSettingsScreen(parent);
break;
case (ProjectManagerScreen::EngineSettings):
newScreen = new EngineSettingsScreen(parent);

@ -13,6 +13,7 @@
#include <ScreensCtrl.h>
#include <ScreenFactory.h>
#include <ScreenWidget.h>
#include <UpdateProjectCtrl.h>
#include <QTabWidget>
#include <QVBoxLayout>

@ -10,15 +10,20 @@
*
*/
#include <UpdateProjectCtrl.h>
#include <ScreensCtrl.h>
#include <GemCatalog/GemCatalogScreen.h>
#include <PythonBindingsInterface.h>
#include <ProjectSettingsScreen.h>
#include <ScreenHeaderWidget.h>
#include <ScreensCtrl.h>
#include <UpdateProjectCtrl.h>
#include <UpdateProjectSettingsScreen.h>
#include <ProjectUtils.h>
#include <QDialogButtonBox>
#include <QVBoxLayout>
#include <QPushButton>
#include <QMessageBox>
#include <QPushButton>
#include <QStackedWidget>
#include <QTabWidget>
#include <QVBoxLayout>
namespace O3DE::ProjectManager
{
@ -26,31 +31,57 @@ namespace O3DE::ProjectManager
: ScreenWidget(parent)
{
QVBoxLayout* vLayout = new QVBoxLayout();
setLayout(vLayout);
vLayout->setContentsMargins(0, 0, 0, 0);
m_header = new ScreenHeader(this);
m_header->setTitle(tr(""));
m_header->setSubTitle(tr("Edit Project Settings:"));
connect(m_header->backButton(), &QPushButton::clicked, this, &UpdateProjectCtrl::HandleBackButton);
vLayout->addWidget(m_header);
m_updateSettingsScreen = new UpdateProjectSettingsScreen();
m_gemCatalogScreen = new GemCatalogScreen();
m_stack = new QStackedWidget(this);
m_stack->setObjectName("body");
m_stack->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding));
vLayout->addWidget(m_stack);
QFrame* topBarFrameWidget = new QFrame(this);
topBarFrameWidget->setObjectName("projectSettingsTopFrame");
QHBoxLayout* topBarHLayout = new QHBoxLayout();
topBarHLayout->setContentsMargins(0, 0, 0, 0);
topBarFrameWidget->setLayout(topBarHLayout);
QTabWidget* tabWidget = new QTabWidget();
tabWidget->setObjectName("projectSettingsTab");
tabWidget->tabBar()->setObjectName("projectSettingsTabBar");
tabWidget->addTab(m_updateSettingsScreen, tr("General"));
QPushButton* gemsButton = new QPushButton(tr("Add More Gems"), this);
topBarHLayout->addWidget(gemsButton);
tabWidget->setCornerWidget(gemsButton);
m_screensCtrl = new ScreensCtrl();
vLayout->addWidget(m_screensCtrl);
topBarHLayout->addWidget(tabWidget);
m_stack->addWidget(topBarFrameWidget);
m_stack->addWidget(m_gemCatalogScreen);
QDialogButtonBox* backNextButtons = new QDialogButtonBox();
backNextButtons->setObjectName("footer");
vLayout->addWidget(backNextButtons);
m_backButton = backNextButtons->addButton(tr("Back"), QDialogButtonBox::RejectRole);
m_backButton->setProperty("secondary", true);
m_nextButton = backNextButtons->addButton(tr("Next"), QDialogButtonBox::ApplyRole);
connect(m_backButton, &QPushButton::pressed, this, &UpdateProjectCtrl::HandleBackButton);
connect(m_nextButton, &QPushButton::pressed, this, &UpdateProjectCtrl::HandleNextButton);
connect(gemsButton, &QPushButton::clicked, this, &UpdateProjectCtrl::HandleGemsButton);
connect(m_backButton, &QPushButton::clicked, this, &UpdateProjectCtrl::HandleBackButton);
connect(m_nextButton, &QPushButton::clicked, this, &UpdateProjectCtrl::HandleNextButton);
connect(reinterpret_cast<ScreensCtrl*>(parent), &ScreensCtrl::NotifyCurrentProject, this, &UpdateProjectCtrl::UpdateCurrentProject);
m_screensOrder =
{
ProjectManagerScreen::ProjectSettings,
ProjectManagerScreen::GemCatalog
};
m_screensCtrl->BuildScreens(m_screensOrder);
m_screensCtrl->ForceChangeToScreen(ProjectManagerScreen::ProjectSettings, false);
UpdateNextButtonText();
Update();
setLayout(vLayout);
}
ProjectManagerScreen UpdateProjectCtrl::GetScreenEnum()
@ -58,63 +89,80 @@ namespace O3DE::ProjectManager
return ProjectManagerScreen::UpdateProject;
}
void UpdateProjectCtrl::NotifyCurrentScreen()
{
m_stack->setCurrentIndex(ScreenOrder::Settings);
Update();
}
void UpdateProjectCtrl::HandleGemsButton()
{
// The next page is the gem catalog. Gather the available gems that will be shown in the gem catalog.
m_gemCatalogScreen->ReinitForProject(m_projectInfo.m_path, /*isNewProject=*/false);
m_stack->setCurrentWidget(m_gemCatalogScreen);
Update();
}
void UpdateProjectCtrl::HandleBackButton()
{
if (!m_screensCtrl->GotoPreviousScreen())
if (m_stack->currentIndex() > 0)
{
emit GotoPreviousScreenRequest();
m_stack->setCurrentIndex(m_stack->currentIndex() - 1);
Update();
}
else
{
UpdateNextButtonText();
emit GotoPreviousScreenRequest();
}
}
void UpdateProjectCtrl::HandleNextButton()
{
ScreenWidget* currentScreen = m_screensCtrl->GetCurrentScreen();
ProjectManagerScreen screenEnum = currentScreen->GetScreenEnum();
auto screenOrderIter = m_screensOrder.begin();
for (; screenOrderIter != m_screensOrder.end(); ++screenOrderIter)
if (m_stack->currentIndex() == ScreenOrder::Settings)
{
if (*screenOrderIter == screenEnum)
if (m_updateSettingsScreen)
{
++screenOrderIter;
break;
}
}
if (screenEnum == ProjectManagerScreen::ProjectSettings)
{
auto projectScreen = reinterpret_cast<ProjectSettingsScreen*>(currentScreen);
if (projectScreen)
{
if (!projectScreen->Validate())
if (!m_updateSettingsScreen->Validate())
{
QMessageBox::critical(this, tr("Invalid project settings"), tr("Invalid project settings"));
return;
}
m_projectInfo = projectScreen->GetProjectInfo();
ProjectInfo newProjectSettings = m_updateSettingsScreen->GetProjectInfo();
// Update project if settings changed
if (m_projectInfo != newProjectSettings)
{
bool result = PythonBindingsInterface::Get()->UpdateProject(newProjectSettings);
if (!result)
{
QMessageBox::critical(this, tr("Project update failed"), tr("Failed to update project."));
return;
}
}
// Check if project path has changed and move it
if (newProjectSettings.m_path != m_projectInfo.m_path)
{
if (!ProjectUtils::MoveProject(m_projectInfo.m_path, newProjectSettings.m_path))
{
QMessageBox::critical(this, tr("Project move failed"), tr("Failed to move project."));
return;
}
}
m_projectInfo = newProjectSettings;
}
}
if (screenOrderIter != m_screensOrder.end())
{
m_screensCtrl->ChangeToScreen(*screenOrderIter);
UpdateNextButtonText();
}
else
if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen)
{
auto result = PythonBindingsInterface::Get()->UpdateProject(m_projectInfo);
if (result)
{
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
else
{
QMessageBox::critical(this, tr("Project update failed"), tr("Failed to update project."));
}
// Enable or disable the gems that got adjusted in the gem catalog and apply them to the given project.
m_gemCatalogScreen->EnableDisableGemsForProject(m_projectInfo.m_path);
}
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
void UpdateProjectCtrl::UpdateCurrentProject(const QString& projectPath)
@ -124,16 +172,28 @@ namespace O3DE::ProjectManager
{
m_projectInfo = projectResult.GetValue();
}
Update();
UpdateSettingsScreen();
}
void UpdateProjectCtrl::UpdateNextButtonText()
void UpdateProjectCtrl::Update()
{
QString nextButtonText = tr("Continue");
if (m_screensCtrl->GetCurrentScreen()->GetScreenEnum() == ProjectManagerScreen::GemCatalog)
if (m_stack->currentIndex() == ScreenOrder::Gems)
{
m_header->setSubTitle(QString(tr("Add More Gems to \"%1\"")).arg(m_projectInfo.m_projectName));
m_nextButton->setText(tr("Confirm"));
}
else
{
nextButtonText = tr("Update Project");
m_header->setSubTitle(QString(tr("Edit Project Settings: \"%1\"")).arg(m_projectInfo.m_projectName));
m_nextButton->setText(tr("Save"));
}
m_nextButton->setText(nextButtonText);
}
void UpdateProjectCtrl::UpdateSettingsScreen()
{
m_updateSettingsScreen->SetProjectInfo(m_projectInfo);
}
} // namespace O3DE::ProjectManager

@ -12,40 +12,57 @@
#pragma once
#if !defined(Q_MOC_RUN)
#include "ProjectInfo.h"
#include <ProjectInfo.h>
#include <ScreenWidget.h>
#include <ScreensCtrl.h>
#include <QPushButton>
#endif
QT_FORWARD_DECLARE_CLASS(QStackedWidget)
QT_FORWARD_DECLARE_CLASS(QTabWidget)
QT_FORWARD_DECLARE_CLASS(QPushButton)
QT_FORWARD_DECLARE_CLASS(QFrame)
namespace O3DE::ProjectManager
{
class UpdateProjectCtrl
: public ScreenWidget
QT_FORWARD_DECLARE_CLASS(ScreenHeader)
QT_FORWARD_DECLARE_CLASS(UpdateProjectSettingsScreen)
QT_FORWARD_DECLARE_CLASS(GemCatalogScreen)
class UpdateProjectCtrl : public ScreenWidget
{
public:
explicit UpdateProjectCtrl(QWidget* parent = nullptr);
~UpdateProjectCtrl() = default;
ProjectManagerScreen GetScreenEnum() override;
protected:
void NotifyCurrentScreen() override;
protected slots:
void HandleBackButton();
void HandleNextButton();
void HandleGemsButton();
void UpdateCurrentProject(const QString& projectPath);
private:
void UpdateNextButtonText();
void Update();
void UpdateSettingsScreen();
enum ScreenOrder
{
Settings,
Gems
};
ScreensCtrl* m_screensCtrl;
QPushButton* m_backButton;
QPushButton* m_nextButton;
ScreenHeader* m_header = nullptr;
QStackedWidget* m_stack = nullptr;
UpdateProjectSettingsScreen* m_updateSettingsScreen = nullptr;
GemCatalogScreen* m_gemCatalogScreen = nullptr;
QPushButton* m_backButton = nullptr;
QPushButton* m_nextButton = nullptr;
QVector<ProjectManagerScreen> m_screensOrder;
ProjectInfo m_projectInfo;
ProjectManagerScreen m_screenEnum;
};
} // namespace O3DE::ProjectManager

@ -0,0 +1,51 @@
/*
* 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 <UpdateProjectSettingsScreen.h>
#include <FormBrowseEditWidget.h>
#include <FormLineEditWidget.h>
#include <QLineEdit>
#include <QDir>
namespace O3DE::ProjectManager
{
UpdateProjectSettingsScreen::UpdateProjectSettingsScreen(QWidget* parent)
: ProjectSettingsScreen(parent)
{
}
ProjectManagerScreen UpdateProjectSettingsScreen::GetScreenEnum()
{
return ProjectManagerScreen::UpdateProjectSettings;
}
void UpdateProjectSettingsScreen::SetProjectInfo(const ProjectInfo& projectInfo)
{
m_projectName->lineEdit()->setText(projectInfo.m_projectName);
m_projectPath->lineEdit()->setText(projectInfo.m_path);
}
bool UpdateProjectSettingsScreen::ValidateProjectPath()
{
bool projectPathIsValid = true;
if (m_projectPath->lineEdit()->text().isEmpty())
{
projectPathIsValid = false;
m_projectPath->setErrorLabelText(tr("Please provide a valid location."));
}
m_projectPath->setErrorLabelVisible(!projectPathIsValid);
return projectPathIsValid;
}
} // namespace O3DE::ProjectManager

@ -0,0 +1,34 @@
/*
* 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.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <ProjectSettingsScreen.h>
#endif
namespace O3DE::ProjectManager
{
class UpdateProjectSettingsScreen
: public ProjectSettingsScreen
{
public:
explicit UpdateProjectSettingsScreen(QWidget* parent = nullptr);
~UpdateProjectSettingsScreen() = default;
ProjectManagerScreen GetScreenEnum() override;
void SetProjectInfo(const ProjectInfo& projectInfo);
protected:
bool ValidateProjectPath() override;
};
} // namespace O3DE::ProjectManager

@ -38,6 +38,8 @@ set(FILES
Source/ProjectInfo.cpp
Source/ProjectUtils.h
Source/ProjectUtils.cpp
Source/UpdateProjectSettingsScreen.h
Source/UpdateProjectSettingsScreen.cpp
Source/NewProjectSettingsScreen.h
Source/NewProjectSettingsScreen.cpp
Source/CreateProjectCtrl.h
@ -48,7 +50,6 @@ set(FILES
Source/ProjectsScreen.cpp
Source/ProjectSettingsScreen.h
Source/ProjectSettingsScreen.cpp
Source/ProjectSettingsScreen.ui
Source/EngineSettingsScreen.h
Source/EngineSettingsScreen.cpp
Source/ProjectButtonWidget.h

@ -42,7 +42,7 @@ namespace AZ
}
#if AZ_TRAIT_COMPILER_SUPPORT_CSIGNAL
void signal_handler(int signal)
void signal_handler([[maybe_unused]] int signal)
{
AZ_TracePrintf(
SceneAPI::Utilities::ErrorWindow,

@ -56,9 +56,13 @@ ly_add_target(
Gem::HttpRequestor
)
# servers and clients use the above module.
# Load the "Gem::AWSClientAuth" module in all types of applications.
ly_create_alias(NAME AWSClientAuth.Servers NAMESPACE Gem TARGETS Gem::AWSClientAuth)
ly_create_alias(NAME AWSClientAuth.Clients NAMESPACE Gem TARGETS Gem::AWSClientAuth)
if (PAL_TRAIT_BUILD_HOST_TOOLS)
ly_create_alias(NAME AWSClientAuth.Tools NAMESPACE Gem TARGETS Gem::AWSClientAuth)
ly_create_alias(NAME AWSClientAuth.Builders NAMESPACE Gem TARGETS Gem::AWSClientAuth)
endif()
################################################################################
# Tests

@ -79,14 +79,12 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS)
INCLUDE_DIRECTORIES
PRIVATE
Include/Private
COMPILE_DEFINITIONS
PRIVATE
AWSCORE_EDITOR
BUILD_DEPENDENCIES
PRIVATE
AZ::AzCore
Gem::AWSCore.Static
Gem::AWSCore.Editor.Static
RUNTIME_DEPENDENCIES
Gem::AWSCore
)
ly_add_target(

@ -11,15 +11,15 @@
#pragma once
#include <AWSCoreModule.h>
#include <AzCore/Module/Module.h>
namespace AWSCore
{
class AWSCoreEditorModule
: public AWSCoreModule
:public AZ::Module
{
public:
AZ_RTTI(AWSCoreEditorModule, "{C1C9B898-848B-4C2F-A7AA-69642D12BCB5}", AWSCoreModule);
AZ_RTTI(AWSCoreEditorModule, "{C1C9B898-848B-4C2F-A7AA-69642D12BCB5}", AZ::Module);
AZ_CLASS_ALLOCATOR(AWSCoreEditorModule, AZ::SystemAllocator, 0);
AWSCoreEditorModule();

@ -15,7 +15,6 @@
namespace AWSCore
{
AWSCoreEditorModule::AWSCoreEditorModule()
: AWSCoreModule()
{
// Push results of [MyComponent]::CreateDescriptor() into m_descriptors here.
m_descriptors.insert(m_descriptors.end(), {
@ -28,10 +27,9 @@ namespace AWSCore
*/
AZ::ComponentTypeList AWSCoreEditorModule::GetRequiredSystemComponents() const
{
AZ::ComponentTypeList requiredComponents = AWSCoreModule::GetRequiredSystemComponents();
requiredComponents.push_back(azrtti_typeid<AWSCoreEditorSystemComponent>());
return requiredComponents;
return AZ::ComponentTypeList{
azrtti_typeid<AWSCoreEditorSystemComponent>()
};
}
}

@ -40,9 +40,7 @@ namespace AWSCore
}
#if !defined(AWSCORE_EDITOR)
// DO NOT MODIFY THIS LINE UNLESS YOU RENAME THE GEM
// The first parameter should be GemName_GemIdLower
// The second should be the fully qualified name of the class above
AZ_DECLARE_MODULE_CLASS(Gem_AWSCore, AWSCore::AWSCoreModule)
#endif

@ -11,7 +11,5 @@
set(FILES
Include/Private/AWSCoreEditorModule.h
Include/Private/AWSCoreModule.h
Source/AWSCoreEditorModule.cpp
Source/AWSCoreModule.cpp
)

@ -46,9 +46,13 @@ ly_add_target(
Gem::AWSCore
)
# Servers and Clients use the above metrics module
# Load the "Gem::AWSMetrics" module in all types of applications.
ly_create_alias(NAME AWSMetrics.Servers NAMESPACE Gem TARGETS Gem::AWSMetrics)
ly_create_alias(NAME AWSMetrics.Clients NAMESPACE Gem TARGETS Gem::AWSMetrics)
if (PAL_TRAIT_BUILD_HOST_TOOLS)
ly_create_alias(NAME AWSMetrics.Tools NAMESPACE Gem TARGETS Gem::AWSMetrics)
ly_create_alias(NAME AWSMetrics.Builders NAMESPACE Gem TARGETS Gem::AWSMetrics)
endif()
################################################################################
# Tests

@ -12,7 +12,9 @@
#include <Atom/Feature/Material/MaterialAssignment.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Serialization/Json/RegistrationContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <Material/MaterialAssignmentSerializer.h>
namespace AZ
{
@ -22,6 +24,11 @@ namespace AZ
{
MaterialAssignmentId::Reflect(context);
if (auto jsonContext = azrtti_cast<JsonRegistrationContext*>(context))
{
jsonContext->Serializer<JsonMaterialAssignmentSerializer>()->HandlesType<MaterialAssignment>();
}
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->RegisterGenericType<MaterialAssignmentMap>();

@ -0,0 +1,214 @@
/*
* 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 <Material/MaterialAssignmentSerializer.h>
#include <Atom/Feature/Material/MaterialAssignment.h>
namespace AZ
{
namespace Render
{
AZ_CLASS_ALLOCATOR_IMPL(JsonMaterialAssignmentSerializer, AZ::SystemAllocator, 0);
JsonSerializationResult::Result JsonMaterialAssignmentSerializer::Load(
void* outputValue, [[maybe_unused]] const Uuid& outputValueTypeId, const rapidjson::Value& inputValue,
JsonDeserializerContext& context)
{
namespace JSR = JsonSerializationResult;
AZ_Assert(
azrtti_typeid<AZ::Render::MaterialAssignment>() == outputValueTypeId,
"Unable to deserialize MaterialAssignment from json because the provided type is %s.",
outputValueTypeId.ToString<AZStd::string>().c_str());
AZ::Render::MaterialAssignment* materialAssignment = reinterpret_cast<AZ::Render::MaterialAssignment*>(outputValue);
AZ_Assert(materialAssignment, "Output value for JsonMaterialAssignmentSerializer can't be null.");
JSR::ResultCode result(JSR::Tasks::ReadField);
{
result.Combine(ContinueLoadingFromJsonObjectField(
&materialAssignment->m_materialAsset, azrtti_typeid<decltype(materialAssignment->m_materialAsset)>(), inputValue,
"MaterialAsset", context));
}
if (inputValue.HasMember("PropertyOverrides") && inputValue["PropertyOverrides"].IsObject())
{
// Attempt to load material property override values for a subset of types
for (const auto& inputPropertyPair : inputValue["PropertyOverrides"].GetObject())
{
const AZ::Name propertyName(inputPropertyPair.name.GetString());
if (!propertyName.IsEmpty())
{
AZStd::any propertyValue;
if (LoadAny<bool>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::u8>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::u16>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::u32>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::u64>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::s8>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::s16>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::s32>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::s64>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<float>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<double>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::Vector2>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::Vector3>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::Vector4>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::Color>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZStd::string>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::Data::AssetId>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::Data::Asset<AZ::RPI::ImageAsset>>(propertyValue, inputPropertyPair.value, context, result) ||
LoadAny<AZ::Data::Asset<AZ::RPI::StreamingImageAsset>>(propertyValue, inputPropertyPair.value, context, result))
{
materialAssignment->m_propertyOverrides[propertyName] = propertyValue;
}
}
}
}
return context.Report(
result,
result.GetProcessing() != JSR::Processing::Halted ? "Succesfully loaded MaterialAssignment information."
: "Failed to load MaterialAssignment information.");
}
JsonSerializationResult::Result JsonMaterialAssignmentSerializer::Store(
rapidjson::Value& outputValue, const void* inputValue, const void* defaultValue, [[maybe_unused]] const Uuid& valueTypeId,
JsonSerializerContext& context)
{
namespace JSR = AZ::JsonSerializationResult;
AZ_Assert(
azrtti_typeid<AZ::Render::MaterialAssignment>() == valueTypeId,
"Unable to Serialize MaterialAssignment because the provided type is %s.", valueTypeId.ToString<AZStd::string>().c_str());
const AZ::Render::MaterialAssignment* materialAssignment = reinterpret_cast<const AZ::Render::MaterialAssignment*>(inputValue);
AZ_Assert(materialAssignment, "Input value for JsonMaterialAssignmentSerializer can't be null.");
const AZ::Render::MaterialAssignment* defaultMaterialAssignmentInstance =
reinterpret_cast<const AZ::Render::MaterialAssignment*>(defaultValue);
outputValue.SetObject();
JSR::ResultCode result(JSR::Tasks::WriteValue);
{
AZ::ScopedContextPath subPathMaterialAsset(context, "m_materialAsset");
const AZ::Data::Asset<RPI::MaterialAsset>* materialAsset = &materialAssignment->m_materialAsset;
const AZ::Data::Asset<RPI::MaterialAsset>* defaultmaterialAsset =
defaultMaterialAssignmentInstance ? &defaultMaterialAssignmentInstance->m_materialAsset : nullptr;
result.Combine(ContinueStoringToJsonObjectField(
outputValue, "MaterialAsset", materialAsset, defaultmaterialAsset,
azrtti_typeid<decltype(materialAssignment->m_materialAsset)>(), context));
}
{
AZ::ScopedContextPath subPathPropertyOverrides(context, "m_propertyOverrides");
if (!materialAssignment->m_propertyOverrides.empty())
{
rapidjson::Value outputPropertyValueContainer;
outputPropertyValueContainer.SetObject();
// Attempt to extract and store material property override values for a subset of types
for (const auto& propertyPair : materialAssignment->m_propertyOverrides)
{
const AZ::Name& propertyName = propertyPair.first;
const AZStd::any& propertyValue = propertyPair.second;
if (!propertyName.IsEmpty() && !propertyValue.empty())
{
rapidjson::Value outputPropertyValue;
if (StoreAny<bool>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::u8>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::u16>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::u32>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::u64>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::s8>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::s16>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::s32>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::s64>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<float>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<double>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::Vector2>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::Vector3>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::Vector4>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::Color>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZStd::string>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::Data::AssetId>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::Data::Asset<AZ::RPI::ImageAsset>>(propertyValue, outputPropertyValue, context, result) ||
StoreAny<AZ::Data::Asset<AZ::RPI::StreamingImageAsset>>(
propertyValue, outputPropertyValue, context, result))
{
outputPropertyValueContainer.AddMember(
rapidjson::Value::StringRefType(propertyName.GetCStr()), outputPropertyValue,
context.GetJsonAllocator());
}
}
}
if (outputPropertyValueContainer.MemberCount() > 0)
{
outputValue.AddMember("PropertyOverrides", outputPropertyValueContainer, context.GetJsonAllocator());
}
}
}
return context.Report(
result,
result.GetProcessing() != JSR::Processing::Halted ? "Successfully stored MaterialAssignment information."
: "Failed to store MaterialAssignment information.");
}
template<typename T>
bool JsonMaterialAssignmentSerializer::LoadAny(
AZStd::any& propertyValue, const rapidjson::Value& inputPropertyValue, AZ::JsonDeserializerContext& context,
AZ::JsonSerializationResult::ResultCode& result)
{
if (inputPropertyValue.IsObject() && inputPropertyValue.HasMember("Value") && inputPropertyValue.HasMember("$type"))
{
// Requiring explicit type info to differentiate be=tween colors versus vectors and numeric types
const AZ::Uuid baseTypeId = azrtti_typeid<T>();
AZ::Uuid typeId = AZ::Uuid::CreateNull();
result.Combine(LoadTypeId(typeId, inputPropertyValue, context, &baseTypeId));
if (typeId == azrtti_typeid<T>())
{
T value = {};
result.Combine(ContinueLoadingFromJsonObjectField(&value, azrtti_typeid<T>(), inputPropertyValue, "Value", context));
propertyValue = value;
return true;
}
}
return false;
}
template<typename T>
bool JsonMaterialAssignmentSerializer::StoreAny(
const AZStd::any& propertyValue, rapidjson::Value& outputPropertyValue, AZ::JsonSerializerContext& context,
AZ::JsonSerializationResult::ResultCode& result)
{
if (propertyValue.is<T>())
{
outputPropertyValue.SetObject();
// Storing explicit type info to differentiate be=tween colors versus vectors and numeric types
rapidjson::Value typeValue;
result.Combine(StoreTypeId(typeValue, azrtti_typeid<T>(), context));
outputPropertyValue.AddMember("$type", typeValue, context.GetJsonAllocator());
T value = AZStd::any_cast<T>(propertyValue);
result.Combine(
ContinueStoringToJsonObjectField(outputPropertyValue, "Value", &value, nullptr, azrtti_typeid<T>(), context));
return true;
}
return false;
}
} // namespace Render
} // namespace AZ

@ -0,0 +1,50 @@
/*
* 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.
*
*/
#pragma once
#include <AzCore/Memory/Memory.h>
#include <AzCore/Name/Name.h>
#include <AzCore/Serialization/Json/BaseJsonSerializer.h>
namespace AZ
{
namespace Render
{
// Custom JSON serializer for material assignment objects containing AZStd::any property overrides,
// which aren't supported by the system
class JsonMaterialAssignmentSerializer : public BaseJsonSerializer
{
public:
AZ_RTTI(JsonMaterialAssignmentSerializer, "{3D33653E-4582-483F-91F5-BBCC347C3DF0}", BaseJsonSerializer);
AZ_CLASS_ALLOCATOR_DECL;
JsonSerializationResult::Result Load(
void* outputValue, const Uuid& outputValueTypeId, const rapidjson::Value& inputValue,
JsonDeserializerContext& context) override;
JsonSerializationResult::Result Store(
rapidjson::Value& outputValue, const void* inputValue, const void* defaultValue, const Uuid& valueTypeId,
JsonSerializerContext& context) override;
private:
template<typename T>
bool LoadAny(
AZStd::any& propertyValue, const rapidjson::Value& inputPropertyValue, AZ::JsonDeserializerContext& context,
AZ::JsonSerializationResult::ResultCode& result);
template<typename T>
bool StoreAny(
const AZStd::any& propertyValue, rapidjson::Value& outputPropertyValue, AZ::JsonSerializerContext& context,
AZ::JsonSerializationResult::ResultCode& result);
};
} // namespace Render
} // namespace AZ

@ -16,6 +16,8 @@ set(FILES
Include/Atom/Feature/Utils/ModelPreset.h
Source/Material/MaterialAssignment.cpp
Source/Material/MaterialAssignmentId.cpp
Source/Material/MaterialAssignmentSerializer.cpp
Source/Material/MaterialAssignmentSerializer.h
Source/Utils/LightingPreset.cpp
Source/Utils/ModelPreset.cpp
)

@ -0,0 +1,209 @@
vulkan.h was generated using a code generator from https://github.com/Dav1dde/glad
/*
** Copyright (c) 2014-2020 The Khronos Group Inc.
**
** SPDX-License-Identifier: Apache-2.0
*/
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -10,3 +10,5 @@
#
add_subdirectory(Code)
add_subdirectory(Tools)

@ -43,7 +43,7 @@ namespace AZ
{
PrimitiveType = AZ_BIT(0),
DepthState = AZ_BIT(1),
EnableStencil = AZ_BIT(2),
StencilState = AZ_BIT(2),
FaceCullMode = AZ_BIT(3),
BlendMode = AZ_BIT(4)
};
@ -110,8 +110,8 @@ namespace AZ
//! Set DepthState if DrawStateOptions::DepthState option is enabled
void SetDepthState(RHI::DepthState depthState);
//! Enable/disable stencil if DrawStateOptions::EnableStencil option is enabled
void SetEnableStencil(bool enable);
//! Set StencilState if DrawStateOptions::StencilState option is enabled
void SetStencilState(RHI::StencilState stencilState);
//! Set CullMode if DrawStateOptions::FaceCullMode option is enabled
void SetCullMode(RHI::CullMode cullMode);
//! Set TargetBlendState for target 0 if DrawStateOptions::BlendMode option is enabled
@ -188,7 +188,7 @@ namespace AZ
// states available for change
RHI::CullMode m_cullMode;
RHI::DepthState m_depthState;
bool m_enableStencil;
RHI::StencilState m_stencilState;
RHI::PrimitiveTopology m_topology;
RHI::TargetBlendState m_blendState0;

@ -35,7 +35,15 @@ namespace AZ
ModelKdTree() = default;
bool Build(const ModelAsset* model);
bool RayIntersection(const AZ::Vector3& raySrc, const AZ::Vector3& rayDir, float& distance, AZ::Vector3& normal) const;
//! Return if a ray intersected the model.
//! @param raySrc The starting point of the ray.
//! @param rayDir The direction and length of the ray (magnitude is encoded in the direction).
//! @param[out] The normalized distance of the intersection (in the range 0.0-1.0) - to calculate the actual
//! distance, multiply distanceNormalized by the magnitude of rayDir.
//! @param[out] The surface normal of the intersection with the model.
//! @return Return true if there was an intersection with the model, false otherwise.
bool RayIntersection(
const AZ::Vector3& raySrc, const AZ::Vector3& rayDir, float& distanceNormalized, AZ::Vector3& normal) const;
void GetPenetratedBoxes(const AZ::Vector3& raySrc, const AZ::Vector3& rayDir, AZStd::vector<AZ::Aabb>& outBoxes);
enum ESplitAxis
@ -53,8 +61,14 @@ namespace AZ
private:
void BuildRecursively(ModelKdTreeNode* pNode, const AZ::Aabb& boundbox, AZStd::vector<ObjectIdTriangleIndices>& indices);
bool RayIntersectionRecursively(ModelKdTreeNode* pNode, const AZ::Vector3& raySrc, const AZ::Vector3& rayDir, float& distance, AZ::Vector3& normal) const;
void GetPenetratedBoxesRecursively(ModelKdTreeNode* pNode, const AZ::Vector3& raySrc, const AZ::Vector3& rayDir, AZStd::vector<AZ::Aabb>& outBoxes);
bool RayIntersectionRecursively(
ModelKdTreeNode* pNode,
const AZ::Vector3& raySrc,
const AZ::Vector3& rayDir,
float& distanceNormalized,
AZ::Vector3& normal) const;
void GetPenetratedBoxesRecursively(
ModelKdTreeNode* pNode, const AZ::Vector3& raySrc, const AZ::Vector3& rayDir, AZStd::vector<AZ::Aabb>& outBoxes);
void ConstructMeshList(const ModelAsset* model, const AZ::Transform& matParent);
static const int s_MinimumVertexSizeInLeafNode = 3 * 10;

@ -30,25 +30,7 @@ namespace AZ
constexpr const char* PerContextSrgName = "PerContextSrg";
constexpr const char* PerDrawSrgName = "PerDrawSrg";
};
bool CompareTargetBlendState(const RHI::TargetBlendState& firstState, const RHI::TargetBlendState& secondState)
{
return !(firstState.m_enable != secondState.m_enable
|| firstState.m_blendOp != secondState.m_blendOp
|| firstState.m_blendDest != secondState.m_blendDest
|| firstState.m_blendSource != secondState.m_blendSource
|| firstState.m_blendAlphaDest != secondState.m_blendAlphaDest
|| firstState.m_blendAlphaOp != secondState.m_blendAlphaOp
|| firstState.m_blendAlphaSource != secondState.m_blendAlphaSource);
}
bool CompareDepthState(const RHI::DepthState& firstState, const RHI::DepthState& secondState)
{
return !(firstState.m_enable != secondState.m_enable
|| firstState.m_func != secondState.m_func
|| firstState.m_writeMask != secondState.m_writeMask);
}
void DynamicDrawContext::MultiStates::UpdateHash(const DrawStateOptions& drawStateOptions)
{
if (!m_isDirty)
@ -70,9 +52,19 @@ namespace AZ
seed = TypeHash64(m_depthState.m_writeMask, seed);
}
if (RHI::CheckBitsAny(drawStateOptions, DrawStateOptions::EnableStencil))
if (RHI::CheckBitsAny(drawStateOptions, DrawStateOptions::StencilState))
{
seed = TypeHash64(m_enableStencil, seed);
seed = TypeHash64(m_stencilState.m_enable, seed);
seed = TypeHash64(m_stencilState.m_readMask, seed);
seed = TypeHash64(m_stencilState.m_writeMask, seed);
seed = TypeHash64(m_stencilState.m_frontFace.m_failOp, seed);
seed = TypeHash64(m_stencilState.m_frontFace.m_depthFailOp, seed);
seed = TypeHash64(m_stencilState.m_frontFace.m_passOp, seed);
seed = TypeHash64(m_stencilState.m_frontFace.m_func, seed);
seed = TypeHash64(m_stencilState.m_backFace.m_failOp, seed);
seed = TypeHash64(m_stencilState.m_backFace.m_depthFailOp, seed);
seed = TypeHash64(m_stencilState.m_backFace.m_passOp, seed);
seed = TypeHash64(m_stencilState.m_backFace.m_func, seed);
}
if (RHI::CheckBitsAny(drawStateOptions, DrawStateOptions::FaceCullMode))
@ -203,7 +195,7 @@ namespace AZ
m_currentStates.m_cullMode = m_pipelineState->ConstDescriptor().m_renderStates.m_rasterState.m_cullMode;
m_currentStates.m_topology = m_pipelineState->ConstDescriptor().m_inputStreamLayout.GetTopology();
m_currentStates.m_depthState = m_pipelineState->ConstDescriptor().m_renderStates.m_depthStencilState.m_depth;
m_currentStates.m_enableStencil = m_pipelineState->ConstDescriptor().m_renderStates.m_depthStencilState.m_stencil.m_enable;
m_currentStates.m_stencilState = m_pipelineState->ConstDescriptor().m_renderStates.m_depthStencilState.m_stencil;
m_currentStates.m_blendState0 = m_pipelineState->ConstDescriptor().m_renderStates.m_blendState.m_targets[0];
m_currentStates.UpdateHash(m_drawStateOptions);
@ -291,7 +283,7 @@ namespace AZ
{
if (RHI::CheckBitsAny(m_drawStateOptions, DrawStateOptions::DepthState))
{
if (!CompareDepthState(m_currentStates.m_depthState, depthState))
if (!(m_currentStates.m_depthState == depthState))
{
m_currentStates.m_depthState = depthState;
m_currentStates.m_isDirty = true;
@ -303,19 +295,19 @@ namespace AZ
}
}
void DynamicDrawContext::SetEnableStencil(bool enable)
void DynamicDrawContext::SetStencilState(RHI::StencilState stencilState)
{
if (RHI::CheckBitsAny(m_drawStateOptions, DrawStateOptions::EnableStencil))
if (RHI::CheckBitsAny(m_drawStateOptions, DrawStateOptions::StencilState))
{
if (m_currentStates.m_enableStencil != enable)
if (!(m_currentStates.m_stencilState == stencilState))
{
m_currentStates.m_enableStencil = enable;
m_currentStates.m_stencilState = stencilState;
m_currentStates.m_isDirty = true;
}
}
else
{
AZ_Warning("RHI", false, "Can't set SetEnableStencil if DrawVariation::EnableStencil wasn't enabled");
AZ_Warning("RHI", false, "Can't set SetStencilState if DrawVariation::StencilState wasn't enabled");
}
}
@ -340,7 +332,7 @@ namespace AZ
{
if (RHI::CheckBitsAny(m_drawStateOptions, DrawStateOptions::BlendMode))
{
if (!CompareTargetBlendState(m_currentStates.m_blendState0, blendState))
if (!(m_currentStates.m_blendState0 == blendState))
{
m_currentStates.m_blendState0 = blendState;
m_currentStates.m_isDirty = true;
@ -695,9 +687,9 @@ namespace AZ
{
m_pipelineState->RenderStatesOverlay().m_depthStencilState.m_depth = m_currentStates.m_depthState;
}
if (RHI::CheckBitsAny(m_drawStateOptions, DrawStateOptions::EnableStencil))
if (RHI::CheckBitsAny(m_drawStateOptions, DrawStateOptions::StencilState))
{
m_pipelineState->RenderStatesOverlay().m_depthStencilState.m_stencil.m_enable = m_currentStates.m_enableStencil;
m_pipelineState->RenderStatesOverlay().m_depthStencilState.m_stencil = m_currentStates.m_stencilState;
}
if (RHI::CheckBitsAny(m_drawStateOptions, DrawStateOptions::FaceCullMode))
{

@ -140,8 +140,9 @@ namespace AZ
bool Model::LocalRayIntersection(const AZ::Vector3& rayStart, const AZ::Vector3& dir, float& distance, AZ::Vector3& normal) const
{
AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
float firstHit;
const int result = Intersect::IntersectRayAABB2(rayStart, dir.GetReciprocal(), m_aabb, firstHit, distance);
float start;
float end;
const int result = Intersect::IntersectRayAABB2(rayStart, dir.GetReciprocal(), m_aabb, start, end);
if (Intersect::ISECT_RAY_AABB_NONE != result)
{
if (ModelAsset* modelAssetPtr = m_modelAsset.Get())
@ -164,7 +165,9 @@ namespace AZ
return false;
}
bool Model::RayIntersection(const AZ::Transform& modelTransform, const AZ::Vector3& nonUniformScale, const AZ::Vector3& rayStart, const AZ::Vector3& dir, float& distanceFactor, AZ::Vector3& normal) const
bool Model::RayIntersection(
const AZ::Transform& modelTransform, const AZ::Vector3& nonUniformScale, const AZ::Vector3& rayStart, const AZ::Vector3& dir,
float& distanceFactor, AZ::Vector3& normal) const
{
AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
const AZ::Vector3 clampedScale = nonUniformScale.GetMax(AZ::Vector3(AZ::MinTransformScale));

@ -11,6 +11,7 @@
*/
#include <AzCore/std/numeric.h>
#include <AzCore/std/limits.h>
#include <Atom/RPI.Reflect/Model/ModelKdTree.h>
#include <AzCore/Math/IntersectSegment.h>
@ -191,10 +192,10 @@ namespace AZ
if (ModelLodAsset* lodAssetPtr = model->GetLodAssets()[0].Get())
{
AZ_Warning("ModelKdTree", lodAssetPtr->GetMeshes().size() <= std::numeric_limits<AZ::u8>::max() + 1,
AZ_Warning("ModelKdTree", lodAssetPtr->GetMeshes().size() <= AZStd::numeric_limits<AZ::u8>::max() + 1,
"KdTree generation doesn't support models with greater than 256 meshes. RayIntersection results will be incorrect "
"unless the meshes are merged or broken up into multiple models");
const size_t size = AZStd::min<size_t>(lodAssetPtr->GetMeshes().size(), std::numeric_limits<AZ::u8>::max() + 1);
const size_t size = AZStd::min<size_t>(lodAssetPtr->GetMeshes().size(), AZStd::numeric_limits<AZ::u8>::max() + 1);
m_meshes.reserve(size);
AZStd::transform(
lodAssetPtr->GetMeshes().begin(), AZStd::next(lodAssetPtr->GetMeshes().begin(), size),
@ -204,20 +205,42 @@ namespace AZ
}
}
bool ModelKdTree::RayIntersection(const AZ::Vector3& raySrc, const AZ::Vector3& rayDir, float& distance, AZ::Vector3& normal) const
bool ModelKdTree::RayIntersection(
const AZ::Vector3& raySrc, const AZ::Vector3& rayDir, float& distanceNormalized, AZ::Vector3& normal) const
{
return RayIntersectionRecursively(m_pRootNode.get(), raySrc, rayDir, distance, normal);
float closestDistanceNormalized = AZStd::numeric_limits<float>::max();
if (RayIntersectionRecursively(m_pRootNode.get(), raySrc, rayDir, closestDistanceNormalized, normal))
{
distanceNormalized = closestDistanceNormalized;
return true;
}
return false;
}
bool ModelKdTree::RayIntersectionRecursively(ModelKdTreeNode* pNode, const AZ::Vector3& raySrc, const AZ::Vector3& rayDir, float& distance, AZ::Vector3& normal) const
bool ModelKdTree::RayIntersectionRecursively(
ModelKdTreeNode* pNode,
const AZ::Vector3& raySrc,
const AZ::Vector3& rayDir,
float& distanceNormalized,
AZ::Vector3& normal) const
{
using Intersect::IntersectRayAABB2;
using Intersect::IntersectSegmentTriangleCCW;
using Intersect::ISECT_RAY_AABB_NONE;
if (!pNode)
{
return false;
}
float start, end;
if (AZ::Intersect::IntersectRayAABB2(raySrc, rayDir.GetReciprocal(), pNode->GetBoundBox(), start, end) == Intersect::ISECT_RAY_AABB_NONE)
if (IntersectRayAABB2(raySrc, rayDir.GetReciprocal(), pNode->GetBoundBox(), start, end) == ISECT_RAY_AABB_NONE)
{
return false;
}
if (start > distanceNormalized)
{
return false;
}
@ -235,17 +258,13 @@ namespace AZ
return false;
}
AZ::Vector3 intersectionNormal;
float hitDistanceNormalized;
const float maxDist(FLT_MAX);
float nearestDist = maxDist;
float nearestDistanceNormalized = distanceNormalized;
for (AZ::u32 i = 0; i < nVBuffSize; ++i)
{
const auto& [first, second, third] = pNode->GetVertexIndex(i);
const AZ::u32 nObjIndex = pNode->GetObjIndex(i);
AZStd::array_view<float> positionBuffer = m_meshes[nObjIndex].m_vertexData;
const AZStd::array_view<float> positionBuffer = m_meshes[nObjIndex].m_vertexData;
if (positionBuffer.empty())
{
@ -258,25 +277,23 @@ namespace AZ
AZ::Vector3{positionBuffer[third * 3 + 0], positionBuffer[third * 3 + 1], positionBuffer[third * 3 + 2]},
};
const AZ::Vector3 rayEnd = raySrc + rayDir * distance;
if (AZ::Intersect::IntersectSegmentTriangleCCW(raySrc, rayEnd, trianglePoints[0], trianglePoints[1], trianglePoints[2],
intersectionNormal, hitDistanceNormalized) != Intersect::ISECT_RAY_AABB_NONE)
float hitDistanceNormalized;
AZ::Vector3 intersectionNormal;
const AZ::Vector3 rayEnd = raySrc + rayDir;
if (IntersectSegmentTriangleCCW(raySrc, rayEnd, trianglePoints[0], trianglePoints[1], trianglePoints[2],
intersectionNormal, hitDistanceNormalized) != ISECT_RAY_AABB_NONE)
{
float hitDistance = hitDistanceNormalized * distance;
if (nearestDist > hitDistance)
if (nearestDistanceNormalized > hitDistanceNormalized)
{
normal = intersectionNormal;
nearestDistanceNormalized = hitDistanceNormalized;
}
nearestDist = AZStd::GetMin(nearestDist, hitDistance);
}
}
if (nearestDist < maxDist)
if (nearestDistanceNormalized < distanceNormalized)
{
distance = AZStd::GetMin(distance, nearestDist);
distanceNormalized = nearestDistanceNormalized;
return true;
}
@ -284,8 +301,8 @@ namespace AZ
}
// running both sides to find the closest intersection
const bool bFoundChild0 = RayIntersectionRecursively(pNode->GetChild(0), raySrc, rayDir, distance, normal);
const bool bFoundChild1 = RayIntersectionRecursively(pNode->GetChild(1), raySrc, rayDir, distance, normal);
const bool bFoundChild0 = RayIntersectionRecursively(pNode->GetChild(0), raySrc, rayDir, distanceNormalized, normal);
const bool bFoundChild1 = RayIntersectionRecursively(pNode->GetChild(1), raySrc, rayDir, distanceNormalized, normal);
return bFoundChild0 || bFoundChild1;
}
@ -311,5 +328,5 @@ namespace AZ
GetPenetratedBoxesRecursively(pNode->GetChild(0), raySrc, rayDir, outBoxes);
GetPenetratedBoxesRecursively(pNode->GetChild(1), raySrc, rayDir, outBoxes);
}
}
}
} // namespace RPI
} // namespace AZ

@ -1074,13 +1074,13 @@ namespace UnitTest
}
};
class KdTreeIntersectsFixture
class KdTreeIntersectsParameterizedFixture
: public ModelTests
, public ::testing::WithParamInterface<KdTreeIntersectParams>
{
};
TEST_P(KdTreeIntersectsFixture, KdTreeIntersects)
TEST_P(KdTreeIntersectsParameterizedFixture, KdTreeIntersects)
{
TwoSeparatedPlanesMesh mesh;
@ -1090,7 +1090,10 @@ namespace UnitTest
float distance = AZStd::numeric_limits<float>::max();
AZ::Vector3 normal;
EXPECT_THAT(kdTree.RayIntersection(AZ::Vector3(GetParam().xpos, GetParam().ypos, GetParam().zpos), AZ::Vector3::CreateAxisZ(-1.0f), distance, normal), testing::Eq(GetParam().expectedShouldIntersect));
EXPECT_THAT(
kdTree.RayIntersection(
AZ::Vector3(GetParam().xpos, GetParam().ypos, GetParam().zpos), AZ::Vector3::CreateAxisZ(-1.0f), distance, normal),
testing::Eq(GetParam().expectedShouldIntersect));
EXPECT_THAT(distance, testing::FloatEq(GetParam().expectedDistance));
}
@ -1119,5 +1122,63 @@ namespace UnitTest
KdTreeIntersectParams{0.778f, 0.111f, 1.0f, 0.5f, true},
KdTreeIntersectParams{0.778f, 0.778f, 1.0f, 0.5f, true},
};
INSTANTIATE_TEST_CASE_P(KdTreeIntersectsPlane, KdTreeIntersectsFixture, ::testing::ValuesIn(intersectTestData));
INSTANTIATE_TEST_CASE_P(KdTreeIntersectsPlane, KdTreeIntersectsParameterizedFixture, ::testing::ValuesIn(intersectTestData));
class KdTreeIntersectsFixture
: public ModelTests
{
public:
void SetUp() override
{
ModelTests::SetUp();
m_mesh = AZStd::make_unique<TwoSeparatedPlanesMesh>();
m_kdTree = AZStd::make_unique<AZ::RPI::ModelKdTree>();
ASSERT_TRUE(m_kdTree->Build(m_mesh->GetModel().Get()));
}
void TearDown() override
{
m_kdTree.reset();
m_mesh.reset();
ModelTests::TearDown();
}
AZStd::unique_ptr<TwoSeparatedPlanesMesh> m_mesh;
AZStd::unique_ptr<AZ::RPI::ModelKdTree> m_kdTree;
};
TEST_F(KdTreeIntersectsFixture, KdTreeIntersectionReturnsNormalizedDistance)
{
float t = AZStd::numeric_limits<float>::max();
AZ::Vector3 normal;
constexpr float rayLength = 100.0f;
EXPECT_THAT(
m_kdTree->RayIntersection(
AZ::Vector3::CreateZero(), AZ::Vector3::CreateAxisZ(-rayLength), t, normal), testing::Eq(true));
EXPECT_THAT(t, testing::FloatEq(0.005f));
}
TEST_F(KdTreeIntersectsFixture, KdTreeIntersectionHandlesInvalidStartingNormalizedDistance)
{
float t = -0.5f; // invalid starting distance
AZ::Vector3 normal;
constexpr float rayLength = 10.0f;
EXPECT_THAT(
m_kdTree->RayIntersection(AZ::Vector3::CreateAxisZ(0.75f), AZ::Vector3::CreateAxisZ(-rayLength), t, normal), testing::Eq(true));
EXPECT_THAT(t, testing::FloatEq(0.025f));
}
TEST_F(KdTreeIntersectsFixture, KdTreeIntersectionDoesNotScaleRayByStartingDistance)
{
float t = 10.0f; // starting distance (used to check it is not read from initially by RayIntersection)
AZ::Vector3 normal;
EXPECT_THAT(
m_kdTree->RayIntersection(AZ::Vector3::CreateAxisZ(5.0f), -AZ::Vector3::CreateAxisZ(), t, normal), testing::Eq(false));
}
} // namespace UnitTest

@ -9,7 +9,15 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
ly_add_external_target(
NAME Clang
VERSION 6.0.1-az
)
if (PAL_TRAIT_BUILD_HOST_TOOLS)
ly_pip_install_local_package_editable(${CMAKE_CURRENT_LIST_DIR} atom_rpi_tools)
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
ly_add_pytest(
NAME RPI::atom_rpi_tools_tests
PATH ${CMAKE_CURRENT_LIST_DIR}/atom_rpi_tools/tests/
TIMEOUT 30
)
endif()
endif()

@ -0,0 +1,39 @@
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.
INTRODUCTION
------------
atom_rpi_tools is a Python project that contains a collection of tools
developed by the Atom team. The project contains the following tools:
* Render pipeline merge tool:
A library to manipulate .pass asset files and help gems create scripts to update render pipeline
REQUIREMENTS
------------
* Python 3.7.5 (64-bit)
It is recommended that you completely remove any other versions of Python
installed on your system.
INSTALL
-----------
It is recommended to set up these these tools with Lumberyard's CMake build commands.
UNINSTALLATION
--------------
The preferred way to uninstall the project is:
(engine install root)/python/python -m pip uninstall atom_rpi_tools

@ -0,0 +1,10 @@
"""
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.
"""

@ -0,0 +1,210 @@
"""
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.
"""
import sys, os
import json
import shutil
class PassTemplate:
# This class provide necessary functions for insert pass requests and update connections
# which are common functions required for adding features.
# It doesn't include the remove/delete furnctions since that's not common case for merging render pipeline
def __init__(self, filePath: str):
self.initialized = False
self.file_path: str = filePath
#load the json file
json_data = open(filePath, "r")
self.file_data = json.load(json_data)
if 'ClassName' not in self.file_data or 'ClassData' not in self.file_data or self.file_data['ClassName']!='PassAsset' or 'PassTemplate' not in self.file_data['ClassData']:
raise KeyError('the json file is not a PassAsset file')
return
if 'PassRequests' in self.file_data['ClassData']['PassTemplate']:
self.passRequests = self.file_data['ClassData']['PassTemplate']['PassRequests']
if 'Slots' in self.file_data['ClassData']['PassTemplate']:
self.slots = self.file_data['ClassData']['PassTemplate']['Slots']
self.initialized = True
print('PassTemplate is loaded from ', filePath)
def find_pass(self, passName):
# return pass's index in PassRequests if a PassRequest with input passName exists
if not hasattr(self, 'passRequests'):
return -1
index = 0
for passRequest in self.passRequests:
if passRequest['Name'] == passName:
return index
index += 1
return -1
def get_pass_count(self):
if not hasattr(self, 'passRequests'):
return 0
return len(self.passRequests)
def __validate_pass_request_data(self, passRequest):
if ('Name' not in passRequest or 'TemplateName' not in passRequest):
raise KeyError('invalid pass request data')
def __ensure_pass_requests_key(self):
if not hasattr(self, 'passRequests'):
self.file_data['ClassData']['PassTemplate']['PassRequests'] = []
self.passRequests = self.file_data['ClassData']['PassTemplate']['PassRequests']
def __ensure_pass_slots_key(self):
if not hasattr(self, 'slots'):
self.file_data['ClassData']['PassTemplate']['Slots'] = []
self.slots = self.file_data['ClassData']['PassTemplate']['Slots']
def insert_pass_request(self, location, passRequest):
self.__validate_pass_request_data(passRequest)
if (self.find_pass(passRequest['Name']) >= 0):
raise ValueError('pass request ', passRequest['Name'], ' is already exist')
# insert a passRequest before the specified location
self.__ensure_pass_requests_key()
self.passRequests.insert(location, passRequest)
def replace_references_after(self, startPassRequest, oldPass, oldSlot, newPass, newSlot):
if not hasattr(self, 'passRequests'):
return 0
# from all pass requests after startPassRequest
# replace all attachment references which uses oldPass and oldSlot
# with newPass and newSlot
started = False
replaced_count = 0
for request in self.passRequests:
if started:
if ('Connections' in request):
for connection in request['Connections']:
if connection['AttachmentRef']['Pass'] == oldPass and connection['AttachmentRef']['Attachment'] == oldSlot:
connection['AttachmentRef']['Pass'] = newPass
connection['AttachmentRef']['Attachment'] = newSlot
replaced_count += 1
if request['Name'] == startPassRequest and not started:
started = True
return replaced_count
def replace_references_for(self, passRequest, oldPass, oldSlot, newPass, newSlot):
if not hasattr(self, 'passRequests'):
return 0
#replace pass reference for the specified passRequest
replaced_count = 0
for request in self.passRequests:
if request['Name'] == passRequest:
if ('Connections' in request):
for connection in request['Connections']:
if connection['AttachmentRef']['Pass'] == oldPass and connection['AttachmentRef']['Attachment'] == oldSlot:
connection['AttachmentRef']['Pass'] = newPass
connection['AttachmentRef']['Attachment'] = newSlot
replaced_count += 1
return replaced_count #return when the specified pass request is updated.
return replaced_count
def __validate_slot_data(self, slotData):
if ('Name' not in slotData or 'SlotType' not in slotData):
raise KeyError('invalid slot data')
def get_slot_count(self):
if not hasattr(self, 'slots'):
return 0
return len(self.slots)
def find_slot(self, slotName):
# return slot's index in Slots if a PassRequest with input passName exists
if not hasattr(self, 'slots'):
return -1
index = 0
for slot in self.slots:
if slot['Name'] == slotName:
return index
index += 1
return -1
def insert_slot(self, location, newSlotData):
# insert a new slot at specified location
self.__validate_slot_data(newSlotData)
# check if the slot already exist
if (self.find_slot(newSlotData['Name']) >= 0):
raise ValueError('Slot ', newSlotData['Name'], ' is already exist')
self.__ensure_pass_slots_key()
self.slots.insert(location, newSlotData)
def add_slot(self, newSlotData):
# append a new slot to slots
self.__validate_slot_data(newSlotData)
# check if the slot already exist
if (self.find_slot(newSlotData['Name']) >= 0):
raise ValueError('Slot ', newSlotData['Name'], ' is already exist')
self.__ensure_pass_slots_key()
self.slots.append(newSlotData)
def get_pass_request(self, passName):
if not hasattr(self, 'passRequests'):
return
# Get the pass request from PassRequests with matching pass name
for passRequest in self.passRequests:
if passRequest['Name'] == passName:
return passRequest
def save(self):
# backup the original file
backupFilePath = self.file_path +'.backup'
shutil.copyfile(self.file_path, backupFilePath)
# save and overwrite file
with open(self.file_path, 'w') as json_file:
json.dump(self.file_data, json_file, indent = 4)
print('File [', self.file_path, '] is updated. Old version is saved in [', backupFilePath, ']')
class PassRequest:
def __init__(self, passRequest: object):
self.pass_request = passRequest
if 'Connections' in passRequest:
self.connections = passRequest['Connections']
def __validate_connection(self, connection):
if ('LocalSlot' not in connection or 'AttachmentRef' not in connection):
raise KeyError('invalid connection data')
def __ensure_connections_key(self):
if not hasattr(self, 'connections'):
self.pass_request['Connections'] = []
self.connections = self.pass_request['Connections']
def get_connection_count(self):
if not hasattr(self, 'connections'):
return 0
return len(self.connections)
def find_connection(self, localSlotName):
if not hasattr(self, 'connections'):
return -1
index = 0
for connection in self.connections:
if connection['LocalSlot'] == localSlotName:
return index
index += 1
return -1
def add_connection(self, newConnection):
self.__validate_connection(newConnection)
if self.find_connection(newConnection['LocalSlot']) >= 0:
raise ValueError('connection ', newConnection['LocalSlot'], ' already exists')
self.__ensure_connections_key()
self.connections.append(newConnection)

@ -0,0 +1,10 @@
"""
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.
"""

@ -0,0 +1,303 @@
"""
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.
Unit tests for pass_data.py
"""
import os
import pytest
import shutil
import json
from atom_rpi_tools.pass_data import PassTemplate
from atom_rpi_tools.pass_data import PassRequest
good_pass_requests_file = os.path.join(os.path.dirname(__file__), 'testdata/pass_requests.json')
good_pass_slots_file = os.path.join(os.path.dirname(__file__), 'testdata/pass_slots.json')
bad_test_data_file = os.path.join(os.path.dirname(__file__), 'testdata/pass_test_bad.json')
@pytest.fixture
def pass_requests_template(tmpdir):
filename = 'pass_requests.json'
source_path = os.path.join(os.path.dirname(__file__), 'testdata/', filename)
destFilePath = os.path.join(tmpdir, 'pass_requests.json')
shutil.copyfile(source_path, destFilePath)
return PassTemplate(destFilePath)
@pytest.fixture
def pass_slots_template(tmpdir):
filename = 'pass_slots.json'
source_path = os.path.join(os.path.dirname(__file__), 'testdata/', filename)
destFilePath = os.path.join(tmpdir, 'pass_requests.json')
shutil.copyfile(source_path, destFilePath)
return PassTemplate(destFilePath)
@pytest.fixture
def new_pass_request():
pass_request = json.loads('{\"Name\": \"InsertPass\",\"TemplateName\": \"InsertPassTemplate\"}')
return pass_request
@pytest.fixture
def new_slot():
slot = json.loads('{\"Name\": \"NewSlot\",\"SlotType\": \"Input\"}')
return slot
@pytest.fixture
def new_connection():
connection = json.loads('{\"LocalSlot\": \"color\", \"AttachmentRef\": { \"Pass\": \"Parent\", \"Attachment\": \"DepthStencil\"}}')
return connection
def test_PassTemplate_Initialize_BadPassTemplateData_ExceptionThrown():
with pytest.raises(KeyError):
PassTemplate(bad_test_data_file)
def test_PassTemplate_FindPass_Success(pass_requests_template):
assert pass_requests_template.find_pass('OpaquePass') == 0
assert pass_requests_template.find_pass('ImGuiPass') == 4
assert pass_requests_template.find_pass('NotExistPass') == -1
def test_PassTemplate_InsertPassRequest_AtBegining_Success(pass_requests_template, new_pass_request):
template = pass_requests_template
pass_count = template.get_pass_count()
template.insert_pass_request(0, new_pass_request)
assert template.find_pass(new_pass_request['Name']) == 0
assert template.get_pass_count() == pass_count+1
# verify the change is saved
template.save()
saved_tamplate = PassTemplate(template.file_path)
assert saved_tamplate.find_pass(new_pass_request['Name'])== 0
assert saved_tamplate.get_pass_count() == pass_count+1
def test_PassTemplate_InsertPassRequest_AtEnd_Success(pass_requests_template, new_pass_request):
template = pass_requests_template
pass_count = template.get_pass_count()
template.insert_pass_request(pass_count, new_pass_request)
assert template.find_pass(new_pass_request['Name']) == pass_count
assert template.get_pass_count() == pass_count+1
# verify the change is saved
template.save()
saved_tamplate = PassTemplate(template.file_path)
assert saved_tamplate.find_pass(new_pass_request['Name']) == pass_count
assert saved_tamplate.get_pass_count() == pass_count+1
def test_PassTemplate_InsertPassRequest_WithDuplicatedName_ExceptionThrown(pass_requests_template, new_pass_request):
template = pass_requests_template
# insert new pass request
template.insert_pass_request(0, new_pass_request)
pass_count = template.get_pass_count()
# exception when insert the same pass again
with pytest.raises(ValueError):
template.insert_pass_request(2, new_pass_request)
# pass count doesn't change
assert template.get_pass_count() == pass_count
def test_PassTemplate_InsertPassRequest_WithBadData_ExceptionThrown(pass_requests_template):
template = pass_requests_template
pass_count = template.get_pass_count()
bad_pass_request = json.loads('{\"name\":\"value\"}')
with pytest.raises(KeyError):
template.insert_pass_request(2, bad_pass_request)
assert template.get_pass_count() == pass_count
def test_PassTemplate_InsertPassRequest_AtOutOfRange_AppendSuccess(pass_requests_template, new_pass_request):
template = pass_requests_template
pass_count = template.get_pass_count()
template.insert_pass_request(pass_count+2, new_pass_request)
assert template.find_pass(new_pass_request['Name']) == pass_count
assert template.get_pass_count() == pass_count+1
def test_PassTemplate_ReplaceReferencesAfter_Success(pass_requests_template):
# replace OpaquePass.DepthStencil with Parent.DepthStencil'
refPass = 'OpaquePass'
# there are 2 passes after OpaquePass which use OpaquePass.DepthStencil as attachment reference
assert pass_requests_template.replace_references_after(refPass, 'OpaquePass', 'DepthStencil', 'Parent', 'DepthStencil') == 2
# after the previous replacement, there it no OpaquePass.DepthStencil reference
refPass = 'TransparentPass'
assert pass_requests_template.replace_references_after(refPass, 'OpaquePass', 'DepthStencil', 'Parent', 'DepthStencil') == 0
# verify changes are saved
pass_requests_template.save()
saved_tamplate = PassTemplate(pass_requests_template.file_path)
assert saved_tamplate.replace_references_after('OpaquePass', 'OpaquePass', 'DepthStencil', 'Parent', 'DepthStencil') == 0
def test_PassTemplate_ReplaceReferencesFor_Success(pass_requests_template):
refPass = 'TransparentPass'
assert pass_requests_template.replace_references_for(refPass, 'OpaquePass', 'DepthStencil', 'Parent', 'DepthStencil') == 1
refPass = '2DPass'
assert pass_requests_template.replace_references_for(refPass, 'OpaquePass', 'DepthStencil', 'Parent', 'DepthStencil') == 0
# verify changes are saved
pass_requests_template.save()
saved_tamplate = PassTemplate(pass_requests_template.file_path)
# no reference of OpaquePass.DepthStencil in TransparentPass
assert saved_tamplate.replace_references_for('TransparentPass', 'OpaquePass', 'DepthStencil', 'Parent', 'DepthStencil') == 0
def test_PassTemplate_FindSlot_Success(pass_slots_template):
assert pass_slots_template.find_slot('Color') == -1
assert pass_slots_template.find_slot('DepthStencil') == 0
assert pass_slots_template.find_slot('ColorInputOutput') == 1
def test_PassTemplate_InsertSlot_AtBegining_Success(pass_slots_template, new_slot):
template = pass_slots_template
slot_count = template.get_slot_count()
depth_stencil_slot = template.find_slot('DepthStencil')
template.insert_slot(0, new_slot)
assert template.find_slot(new_slot['Name']) == 0
assert template.find_slot('DepthStencil') == depth_stencil_slot+1 # DepthStencil moved back by 1
assert template.get_slot_count() == slot_count+1
# verify the change is saved
template.save()
saved_tamplate = PassTemplate(template.file_path)
assert saved_tamplate.find_slot(new_slot['Name']) == 0
assert saved_tamplate.get_slot_count() == slot_count+1
def test_PassTemplate_InsertSlot_AtEnd_Success(pass_slots_template, new_slot):
template = pass_slots_template
slot_count = template.get_slot_count()
depth_stencil_slot = template.find_slot('DepthStencil')
template.insert_slot(slot_count, new_slot)
assert template.find_slot(new_slot['Name']) == slot_count
assert template.find_slot('DepthStencil') == depth_stencil_slot
assert template.get_slot_count() == slot_count+1
# verify the change is saved
template.save()
saved_tamplate = PassTemplate(template.file_path)
assert saved_tamplate.find_slot(new_slot['Name']) == slot_count
assert saved_tamplate.get_slot_count() == slot_count+1
def test_PassTemplate_AddSlot_GoodSlotData_Success(pass_slots_template, new_slot):
template = pass_slots_template
slot_count = template.get_slot_count()
depth_stencil_slot = template.find_slot('DepthStencil')
template.add_slot(new_slot)
assert template.find_slot(new_slot['Name']) == slot_count
assert template.find_slot('DepthStencil') == depth_stencil_slot
assert template.get_slot_count() == slot_count+1
# verify the change is saved
template.save()
saved_tamplate = PassTemplate(template.file_path)
assert saved_tamplate.find_slot(new_slot['Name']) == slot_count
assert saved_tamplate.get_slot_count() == slot_count+1
def test_PassTemplate_InsertSlot_OutOfRange_AppendSuccess(pass_slots_template, new_slot):
template = pass_slots_template
slot_count = template.get_slot_count()
template.insert_slot(slot_count+3, new_slot)
assert template.find_slot(new_slot['Name']) == slot_count
assert template.get_slot_count() == slot_count+1
def test_PassTemplate_AddDuplicateSlot_ExceptionThrown(pass_slots_template, new_slot):
template = pass_slots_template
slot_count = template.get_slot_count()
template.add_slot(new_slot)
with pytest.raises(ValueError):
template.insert_slot(0, new_slot)
with pytest.raises(ValueError):
template.add_slot(new_slot)
def test_PassTemplate_InsertOrAddSlot_WithBadSlotData_ExceptionThrown(pass_slots_template):
template = pass_slots_template
slot_count = template.get_slot_count()
bad_slot = json.loads('{\"slot\": \"xxx\"}')
with pytest.raises(KeyError):
template.insert_slot(0, bad_slot)
with pytest.raises(KeyError):
template.add_slot(bad_slot)
def test_PassReqeuest_Initialize_WithExistPassReqeuestFromPassTemplate_Success(pass_requests_template):
template = pass_requests_template
request = PassRequest(template.get_pass_request('OpaquePass'))
connection_count = request.get_connection_count()
assert connection_count == 2
def test_PassTemplate_GetPassRequest_NotExist_ReturnNull(pass_requests_template):
assert not pass_requests_template.get_pass_request('NotExistPass')
def test_PassReqeuest_AddConnection_WithExistingConnections_Success(pass_requests_template, new_connection):
template = pass_requests_template
request = PassRequest(template.get_pass_request('OpaquePass'))
connection_count = request.get_connection_count()
request.add_connection(new_connection)
connection_count += 1
assert request.get_connection_count() == connection_count
# verify changes are saved
template.save()
saved_tamplate = PassTemplate(template.file_path)
saved_request = PassRequest(saved_tamplate.get_pass_request('OpaquePass'))
assert saved_request.get_connection_count() == connection_count
def test_PassReqeuest_AddConnection_WithNoExistingConnections_Success(pass_requests_template, new_connection):
template = pass_requests_template
request = PassRequest(template.get_pass_request('ImGuiPass'))
assert request.get_connection_count() == 0
request.add_connection(new_connection)
assert request.get_connection_count() == 1
# verify changes are saved
template.save()
saved_tamplate = PassTemplate(template.file_path)
saved_request = PassRequest(saved_tamplate.get_pass_request('ImGuiPass'))
assert saved_request.get_connection_count() == 1
def test_PassReqeuest_AddConnection_WithDuplicatedName_ExceptionThrown(pass_requests_template, new_connection):
template = pass_requests_template
request = PassRequest(template.get_pass_request('OpaquePass'))
request.add_connection(new_connection)
with pytest.raises(ValueError):
request.add_connection(new_connection)
def test_PassReqeuest_AddConnect_BadConnectionData_ExceptionThrown(pass_requests_template, new_connection):
template = pass_requests_template
request = PassRequest(template.get_pass_request('OpaquePass'))
bad_connection = json.loads('{\"xxx\": \"xxx\"}')
with pytest.raises(KeyError):
request.add_connection(bad_connection)
def test_PassTemplate_InsertSlot_ToEmptyList_Success(pass_requests_template, new_slot):
template = pass_requests_template
# test insert slot function to pass template which doesn't have any slots
slot_count = template.get_slot_count()
assert slot_count == 0
assert template.find_slot(new_slot['Name'])==-1
pass_requests_template.insert_slot(0, new_slot)
assert template.find_slot(new_slot['Name']) == 0
assert template.get_slot_count() == 1
# verify changes are saved
template.save()
saved_tamplate = PassTemplate(template.file_path)
assert saved_tamplate.get_slot_count() == 1
def test_PassTempalte_InsertPassRequest_ToEmptyList_Success(pass_slots_template, new_pass_request):
template = pass_slots_template
# test insert pass function to pass template which doesn't have any pass requests
pass_count = template.get_pass_count()
assert pass_count == 0
template.insert_pass_request(0, new_pass_request)
assert template.find_pass(new_pass_request['Name']) == 0
assert template.get_pass_count() == 1
# verify changes are saved
template.save()
saved_tamplate = PassTemplate(template.file_path)
assert saved_tamplate.get_pass_count() == 1
def test_PassTemplate_Save_Success(pass_requests_template):
pass_requests_template.save()
saved_tamplate = PassTemplate(pass_requests_template.file_path)
assert os.path.exists(pass_requests_template.file_path)
assert os.path.exists(pass_requests_template.file_path +'.backup')

@ -0,0 +1,53 @@
"""
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.
Unit tests for utils.py
"""
import pytest
import os
import atom_rpi_tools.utils as utils
def test_FindOrCopyFile_DestFileNotExist_CopySuccess(tmpdir):
# created dir and copied
filename = 'pass_requests.json'
source_path = os.path.join(os.path.dirname(__file__), 'testdata/', filename)
dest_path = os.path.join(tmpdir, 'testdata/', 'pass_requests.json')
assert not os.path.exists(dest_path)
utils.find_or_copy_file(dest_path, source_path)
assert os.path.exists(dest_path)
source_size = os.path.getsize(source_path)
dest_size = os.path.getsize(dest_path)
assert source_size == dest_size
def test_FindOrCopyFile_DestFileAlreadyExists_Skip(tmpdir):
# copy %cur_dir%/testdata/pass_requests.json to tempdir/testdata/pass_requests.json
filename = 'pass_requests.json'
source_path = os.path.join(os.path.dirname(__file__), 'testdata/', filename)
dest_path = os.path.join(tmpdir, 'testdata/', 'pass_requests.json')
utils.find_or_copy_file(dest_path, source_path)
# skip if dest_path already exists
assert os.path.exists(dest_path)
before_size = os.path.getsize(dest_path)
source_path = os.path.join(os.path.dirname(__file__), 'testdata/', 'pass_slots.json')
before_source_size = os.path.getsize(source_path)
assert before_size != source_path
utils.find_or_copy_file(dest_path, source_path)
after_size = os.path.getsize(dest_path)
assert before_size == after_size
def test_FindOrCopyFile_SourceFileNotExists_ExceptionThrown(tmpdir):
# report error if source doesn't exist
bad_source_path = 'notexist.dat'
dest_path = os.path.join(tmpdir, 'notexist.dat')
with pytest.raises(ValueError):
utils.find_or_copy_file(dest_path, bad_source_path)

@ -0,0 +1,116 @@
{
"Type": "JsonSerialization",
"Version": 1,
"ClassName": "PassAsset",
"ClassData": {
"PassTemplate": {
"Name": "PipelineTemplate",
"PassClass": "ParentPass",
"PassRequests": [
{
"Name": "OpaquePass",
"TemplateName": "OpaquePassTemplate",
"Connections": [
{
"LocalSlot": "DepthStencil",
"AttachmentRef": {
"Pass": "Parent",
"Attachment": "DepthStencil"
}
},
{
"LocalSlot": "ColorInputOutput",
"AttachmentRef": {
"Pass": "Parent",
"Attachment": "ColorInputOutput"
}
}
]
},
{
"Name": "TransparentPass",
"TemplateName": "TransparentPassTemplate",
"Enabled": true,
"Connections": [
{
"LocalSlot": "DepthStencil",
"AttachmentRef": {
"Pass": "OpaquePass",
"Attachment": "DepthStencil"
}
},
{
"LocalSlot": "ColorInputOutput",
"AttachmentRef": {
"Pass": "OpaquePass",
"Attachment": "Color"
}
}
],
"PassData": {
"$type": "RasterPassData",
"DrawListTag": "transparent",
"DrawListSortType": "KeyThenReverseDepth",
"PipelineViewTag": "MainCamera",
"PassSrgAsset": {
"FilePath": "shaderlib/atom/features/pbr/transparentpasssrg.azsli:PassSrg"
}
}
},
{
"Name": "AuxGeomPass",
"TemplateName": "AuxGeomPassTemplate",
"Enabled": true,
"Connections": [
{
"LocalSlot": "DepthStencil",
"AttachmentRef": {
"Pass": "OpaquePass",
"Attachment": "DepthStencil"
}
},
{
"LocalSlot": "ColorInputOutput",
"AttachmentRef": {
"Pass": "TransparentPass",
"Attachment": "ColorInputOutput"
}
}
],
"PassData": {
"$type": "RasterPassData",
"DrawListTag": "auxgeom",
"PipelineViewTag": "MainCamera"
}
},
{
"Name": "2DPass",
"TemplateName": "UIPassTemplate",
"Enabled": true,
"Connections": [
{
"LocalSlot": "ColorInputOutput",
"AttachmentRef": {
"Pass": "TransparentPass",
"Attachment": "ColorInputOutput"
}
}
],
"PassData": {
"$type": "RasterPassData",
"DrawListTag": "2dpass",
"PipelineViewTag": "MainCamera"
}
},
{
"Name": "ImGuiPass",
"TemplateName": "ImGuiPassTemplate",
"PassData": {
"$type": "ImGuiPassData",
"IsDefaultImGui": true
}
}
]
}
}
}

@ -0,0 +1,21 @@
{
"Type": "JsonSerialization",
"Version": 1,
"ClassName": "PassAsset",
"ClassData": {
"PassTemplate": {
"Name": "PipelineTemplate",
"PassClass": "ParentPass",
"Slots": [
{
"Name": "DepthStencil",
"SlotType": "InputOutput"
},
{
"Name": "ColorInputOutput",
"SlotType": "InputOutput"
}
]
}
}
}

@ -0,0 +1,6 @@
{
"Type": "JsonSerialization",
"Version": 1,
"ClassData": {
}
}

@ -0,0 +1,31 @@
"""
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.
"""
import os.path
from os import path
import shutil
import json
def find_or_copy_file(destFilePath, sourceFilePath):
if path.exists(destFilePath):
return
if not path.exists(sourceFilePath):
raise ValueError('find_or_copy_file: source file [', sourceFilePath, '] doesn\'t exist')
return
dstDir = path.dirname(destFilePath)
if not path.isdir(dstDir):
os.makedirs(dstDir)
shutil.copyfile(sourceFilePath, destFilePath)
def load_json_file(filePath):
file_stream = open(filePath, "r")
return json.load(file_stream)

@ -0,0 +1,33 @@
"""
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.
"""
import os
import platform
from setuptools import setup, find_packages
PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
PYTHON_64 = platform.architecture()[0] == '64bit'
if __name__ == '__main__':
if not PYTHON_64:
raise RuntimeError("32-bit Python is not a supported platform.")
with open(os.path.join(PROJECT_ROOT, 'README.txt')) as f:
long_description = f.read()
setup(
name="atom_rpi_tools",
version="1.0.0",
description='Python interface to Atom RPI tools',
long_description=long_description,
packages=find_packages(exclude=['tests'])
)

@ -29,7 +29,8 @@ ly_add_target(
Legacy::CryCommon
Gem::Atom_RHI.Reflect
Gem::Atom_RPI.Public
Gem::Atom_Bootstrap.Headers
PUBLIC
Gem::Atom_AtomBridge.Static
)
################################################################################

@ -27,11 +27,14 @@
#include <AzFramework/Scene/SceneSystemInterface.h>
#include <Atom/RPI.Public/DynamicDraw/DynamicDrawContext.h>
#include <AtomBridge/PerViewportDynamicDrawInterface.h>
namespace AZ
{
class FFont;
static constexpr char AtomFontDynamicDrawContextName[] = "AtomFont";
//! AtomFont is the font system manager.
//! AtomFont manages the lifetime of FFont instances, each of which represents an individual font (e.g Courier New Italic)
@ -90,13 +93,6 @@ namespace AZ
AzFramework::FontDrawInterface* GetFontDrawInterface(AzFramework::FontId fontId) const override;
AzFramework::FontDrawInterface* GetDefaultFontDrawInterface() const override;
void SceneAboutToBeRemoved(AzFramework::Scene& scene);
// Atom DynamicDraw interface management
AZ::RHI::Ptr<AZ::RPI::DynamicDrawContext> GetOrCreateDynamicDrawForScene(AZ::RPI::Scene* scene);
public:
void UnregisterFont(const char* fontName);
@ -108,8 +104,6 @@ namespace AZ
using FontFamilyMap = AZStd::unordered_map<AZStd::string, AZStd::weak_ptr<FontFamily>>;
using FontFamilyReverseLookupMap = AZStd::unordered_map<FontFamily*, FontFamilyMap::iterator>;
using SceneToDynamicDrawMap = AZStd::unordered_map<AZ::RPI::Scene*, AZ::RPI::Ptr<AZ::RPI::DynamicDrawContext>>;
private:
//! Convenience method for loading fonts
IFFont* LoadFont(const char* fontName);
@ -145,9 +139,6 @@ namespace AZ
int r_persistFontFamilies = 1; //!< Persist fonts for application lifetime to prevent unnecessary work; enabled by default.
AZStd::vector<FontFamilyPtr> m_persistedFontFamilies; //!< Stores persisted fonts (if "persist font families" is enabled)
SceneToDynamicDrawMap m_sceneToDynamicDrawMap;
AZStd::shared_mutex m_sceneToDynamicDrawMutex;
};
}
#endif

@ -42,11 +42,9 @@
#include <Atom/RPI.Public/Scene.h>
#include <Atom/RPI.Public/DynamicDraw/DynamicDrawInterface.h>
#include <Atom/RPI.Public/ViewportContextBus.h>
#include <Atom/RPI.Public/WindowContext.h>
#include <Atom/RPI.Public/Image/StreamingImage.h>
#include <Atom/Bootstrap/DefaultWindowBus.h>
#include <Atom/Bootstrap/BootstrapNotificationBus.h>
struct ISystem;
namespace AZ
@ -68,7 +66,6 @@ namespace AZ
: public IFFont
, public AZStd::intrusive_refcount<AZStd::atomic_uint, FontDeleter>
, public AzFramework::FontDrawInterface
, private AZ::Render::Bootstrap::NotificationBus::Handler
{
using ref_count = AZStd::intrusive_refcount<AZStd::atomic_uint, FontDeleter>;
friend FontDeleter;
@ -168,8 +165,8 @@ namespace AZ
struct FontShaderData
{
AZ::RHI::ShaderInputImageIndex m_imageInputIndex;
AZ::RHI::ShaderInputConstantIndex m_viewProjInputIndex;
AZ::RHI::ShaderInputNameIndex m_imageInputIndex = "m_texture";
AZ::RHI::ShaderInputNameIndex m_viewProjInputIndex = "m_worldToProj";
};
public:
@ -230,7 +227,6 @@ namespace AZ
private:
virtual ~FFont();
bool InitFont(AZ::RPI::Scene* renderScene);
bool InitTexture();
bool InitCache();
@ -281,8 +277,6 @@ namespace AZ
void ScaleCoord(const RHI::Viewport& viewport, float& x, float& y) const;
void OnBootstrapSceneReady(AZ::RPI::Scene* bootstrapScene) override;
RPI::WindowContextSharedPtr GetDefaultWindowContext() const;
RPI::ViewportContextPtr GetDefaultViewportContext() const;
@ -303,6 +297,8 @@ namespace AZ
string m_name;
string m_curPath;
AZ::Name m_dynamicDrawContextName = AZ::Name(AZ::AtomFontDynamicDrawContextName);
FontTexture* m_fontTexture = nullptr;
size_t m_fontBufferSize = 0;
@ -315,13 +311,6 @@ namespace AZ
AtomFont* m_atomFont = nullptr;
bool m_fontTexDirty = false;
enum class InitializationState : AZ::u8
{
Uninitialized,
Initializing,
Initialized
};
AZStd::atomic<InitializationState> m_fontInitializationState = InitializationState::Uninitialized;
FontEffects m_effects;
@ -356,6 +345,7 @@ namespace AZ
if (font && font->m_atomFont)
{
font->m_atomFont->UnregisterFont(font->m_name);
font->m_atomFont = nullptr;
}
delete font;

@ -354,17 +354,26 @@ AZ::AtomFont::AtomFont(ISystem* system)
#endif
AZ::Interface<AzFramework::FontQueryInterface>::Register(this);
m_sceneEventHandler = AzFramework::ISceneSystem::SceneEvent::Handler(
[this](AzFramework::ISceneSystem::EventType eventType, const AZStd::shared_ptr<AzFramework::Scene>& scene)
// register font per viewport dynamic draw context.
static const char* shaderFilepath = "Shaders/SimpleTextured.azshader";
AZ::AtomBridge::PerViewportDynamicDraw::Get()->RegisterDynamicDrawContext(
AZ::Name(AZ::AtomFontDynamicDrawContextName),
[](RPI::Ptr<RPI::DynamicDrawContext> drawContext)
{
if (eventType == AzFramework::ISceneSystem::EventType::ScenePendingRemoval)
{
SceneAboutToBeRemoved(*scene);
}
Data::Instance<RPI::Shader> shader = AZ::RPI::LoadShader(shaderFilepath);
AZ::RPI::ShaderOptionList shaderOptions;
shaderOptions.push_back(AZ::RPI::ShaderOption(AZ::Name("o_useColorChannels"), AZ::Name("false")));
shaderOptions.push_back(AZ::RPI::ShaderOption(AZ::Name("o_clamp"), AZ::Name("true")));
drawContext->InitShaderWithVariant(shader, &shaderOptions);
drawContext->InitVertexFormat(
{
{"POSITION", RHI::Format::R32G32B32_FLOAT},
{"COLOR", RHI::Format::B8G8R8A8_UNORM},
{"TEXCOORD0", RHI::Format::R32G32_FLOAT}
});
drawContext->EndInit();
});
auto sceneSystem = AzFramework::SceneSystemInterface::Get();
AZ_Assert(sceneSystem, "Font created before the scene system is available.");
sceneSystem->ConnectToEvents(m_sceneEventHandler);
}
AZ::AtomFont::~AtomFont()
@ -860,52 +869,5 @@ XmlNodeRef AZ::AtomFont::LoadFontFamilyXml(const char* fontFamilyName, string& o
return root;
}
void AZ::AtomFont::SceneAboutToBeRemoved(AzFramework::Scene& scene)
{
AZ::RPI::ScenePtr* rpiScene = scene.FindSubsystem<AZ::RPI::ScenePtr>();
if (rpiScene)
{
AZStd::lock_guard<AZStd::shared_mutex> lock(m_sceneToDynamicDrawMutex);
if (auto it = m_sceneToDynamicDrawMap.find(rpiScene->get()); it != m_sceneToDynamicDrawMap.end())
{
m_sceneToDynamicDrawMap.erase(it);
}
}
}
AZ::RHI::Ptr<AZ::RPI::DynamicDrawContext> AZ::AtomFont::GetOrCreateDynamicDrawForScene(AZ::RPI::Scene* scene)
{
static const char* shaderFilepath = "Shaders/SimpleTextured.azshader";
{
// shared lock while reading
AZStd::shared_lock<AZStd::shared_mutex> lock(m_sceneToDynamicDrawMutex);
if (auto it = m_sceneToDynamicDrawMap.find(scene); it != m_sceneToDynamicDrawMap.end())
{
return it->second;
}
}
// Create and initialize DynamicDrawContext for font draw
AZ::RHI::Ptr<AZ::RPI::DynamicDrawContext> dynamicDraw = RPI::DynamicDrawInterface::Get()->CreateDynamicDrawContext(scene);
Data::Instance<RPI::Shader> shader = AZ::RPI::LoadShader(shaderFilepath);
AZ::RPI::ShaderOptionList shaderOptions;
shaderOptions.push_back(AZ::RPI::ShaderOption(AZ::Name("o_useColorChannels"), AZ::Name("false")));
shaderOptions.push_back(AZ::RPI::ShaderOption(AZ::Name("o_clamp"), AZ::Name("true")));
dynamicDraw->InitShaderWithVariant(shader, &shaderOptions);
dynamicDraw->InitVertexFormat({{"POSITION", RHI::Format::R32G32B32_FLOAT}, {"COLOR", RHI::Format::B8G8R8A8_UNORM}, {"TEXCOORD0", RHI::Format::R32G32_FLOAT}});
dynamicDraw->EndInit();
// exclusive lock while writing
AZStd::lock_guard<AZStd::shared_mutex> lock(m_sceneToDynamicDrawMutex);
m_sceneToDynamicDrawMap.insert(AZStd::make_pair(scene, dynamicDraw));
return dynamicDraw;
}
#endif

@ -60,14 +60,7 @@ static const size_t MaxVerts = 8 * 1024; // 2048 quads
static const size_t MaxIndices = (MaxVerts * 6) / 4; // 6 indices per quad, 6/4 * MaxVerts
static const char DrawList2DPassName[] = "2dpass";
namespace ShaderInputs
{
static const char TextureIndexName[] = "m_texture";
static const char WorldToProjIndexName[] = "m_worldToProj";
static const char SamplerIndexName[] = "m_sampler";
}
AZ::FFont::FFont(AtomFont* atomFont, const char* fontName)
AZ::FFont::FFont(AZ::AtomFont* atomFont, const char* fontName)
: m_name(fontName)
, m_atomFont(atomFont)
{
@ -78,9 +71,14 @@ AZ::FFont::FFont(AtomFont* atomFont, const char* fontName)
FontEffect* effect = AddEffect("default");
effect->AddPass();
AddRef();
// Create cpu memory to cache the font draw data before submit
m_vertexBuffer = new SVF_P3F_C4B_T2F[MaxVerts];
m_indexBuffer = new u16[MaxIndices];
AZ::Render::Bootstrap::NotificationBus::Handler::BusConnect();
m_vertexCount = 0;
m_indexCount = 0;
AddRef();
}
AZ::RPI::ViewportContextPtr AZ::FFont::GetDefaultViewportContext() const
@ -98,55 +96,10 @@ AZ::RPI::WindowContextSharedPtr AZ::FFont::GetDefaultWindowContext() const
return {};
}
bool AZ::FFont::InitFont(AZ::RPI::Scene* renderScene)
{
if (!renderScene)
{
return false;
}
auto initializationState = InitializationState::Uninitialized;
// Do an atomic transition to Initializing if we're in the Uninitialized state.
// Otherwise, check the current state.
// If we're Initialized, there's no more work to be done, return true to indicate we're good to go.
// If we're Initializing (on another thread), return false to let the consumer know it's not safe for us to be used yet.
if (!m_fontInitializationState.compare_exchange_strong(initializationState, InitializationState::Initializing))
{
return initializationState == InitializationState::Initialized;
}
// Create and initialize DynamicDrawContext for font draw
AZ::RPI::Ptr<AZ::RPI::DynamicDrawContext> dynamicDraw = m_atomFont->GetOrCreateDynamicDrawForScene(renderScene);
// Save draw srg input indices for later use
Data::Instance<RPI::ShaderResourceGroup> drawSrg = dynamicDraw->NewDrawSrg();
const RHI::ShaderResourceGroupLayout* layout = drawSrg->GetAsset()->GetLayout();
m_fontShaderData.m_imageInputIndex = layout->FindShaderInputImageIndex(AZ::Name(ShaderInputs::TextureIndexName));
AZ_Error("AtomFont::FFont", m_fontShaderData.m_imageInputIndex.IsValid(), "Failed to find shader input constant %s.",
ShaderInputs::TextureIndexName);
m_fontShaderData.m_viewProjInputIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::WorldToProjIndexName));
AZ_Error("AtomFont::FFont", m_fontShaderData.m_viewProjInputIndex.IsValid(), "Failed to find shader input constant %s.",
ShaderInputs::WorldToProjIndexName);
// Create cpu memory to cache the font draw data before submit
m_vertexBuffer = new SVF_P3F_C4B_T2F[MaxVerts];
m_indexBuffer = new u16[MaxIndices];
m_vertexCount = 0;
m_indexCount = 0;
m_fontInitializationState = InitializationState::Initialized;
return true;
}
AZ::FFont::~FFont()
{
AZ_Assert(m_atomFont == nullptr, "The font should already be unregistered through a call to AZ::FFont::Release()");
AZ::Render::Bootstrap::NotificationBus::Handler::BusDisconnect();
delete[] m_vertexBuffer;
delete[] m_indexBuffer;
@ -303,7 +256,8 @@ void AZ::FFont::DrawStringUInternal(
const TextDrawContext& ctx)
{
// Lazily ensure we're initialized before attempting to render.
if (!viewportContext || !InitFont(viewportContext->GetRenderScene().get()))
// Validate that there is a render scene before attempting to init.
if (!viewportContext || !viewportContext->GetRenderScene())
{
return;
}
@ -323,12 +277,6 @@ void AZ::FFont::DrawStringUInternal(
return;
}
// if the font is about to be deleted then m_atomFont can be nullptr
if (!m_atomFont)
{
return;
}
const bool orthoMode = ctx.m_overrideViewProjMatrices;
const float viewX = viewport.m_minX;
@ -406,14 +354,17 @@ void AZ::FFont::DrawStringUInternal(
if (numQuads)
{
auto dynamicDraw = m_atomFont->GetOrCreateDynamicDrawForScene(viewportContext->GetRenderScene().get());
//setup per draw srg
auto drawSrg = dynamicDraw->NewDrawSrg();
drawSrg->SetConstant(m_fontShaderData.m_viewProjInputIndex, modelViewProjMat);
drawSrg->SetImageView(m_fontShaderData.m_imageInputIndex, m_fontStreamingImage->GetImageView());
drawSrg->Compile();
dynamicDraw->DrawIndexed(m_vertexBuffer, m_vertexCount, m_indexBuffer, m_indexCount, RHI::IndexFormat::Uint16, drawSrg);
AZ::RPI::Ptr<AZ::RPI::DynamicDrawContext> dynamicDraw = AZ::AtomBridge::PerViewportDynamicDraw::Get()->GetDynamicDrawContextForViewport(m_dynamicDrawContextName, viewportContext->GetId());
if (dynamicDraw)
{
//setup per draw srg
auto drawSrg = dynamicDraw->NewDrawSrg();
drawSrg->SetConstant(m_fontShaderData.m_viewProjInputIndex, modelViewProjMat);
drawSrg->SetImageView(m_fontShaderData.m_imageInputIndex, m_fontStreamingImage->GetImageView());
drawSrg->Compile();
dynamicDraw->DrawIndexed(m_vertexBuffer, m_vertexCount, m_indexBuffer, m_indexCount, RHI::IndexFormat::Uint16, drawSrg);
}
m_indexCount = 0;
m_vertexCount = 0;
}
@ -694,12 +645,6 @@ uint32_t AZ::FFont::WriteTextQuadsToBuffers(SVF_P2F_C4B_T2F_F4B* verts, uint16_t
return numQuadsWritten;
}
// if the font is about to be deleted then m_atomFont can be nullptr
if (!m_atomFont)
{
return numQuadsWritten;
}
SVF_P2F_C4B_T2F_F4B* vertexData = verts;
uint16_t* indexData = indices;
size_t vertexOffset = 0;
@ -1523,7 +1468,7 @@ bool AZ::FFont::UpdateTexture()
{
using namespace AZ;
if (m_fontInitializationState != InitializationState::Initialized || !m_fontImage)
if (!m_fontImage)
{
return false;
}
@ -1591,7 +1536,7 @@ void AZ::FFont::Prepare(const char* str, bool updateTexture, const AtomFont::Gly
const bool rerenderGlyphs = m_sizeBehavior == SizeBehavior::Rerender;
const AtomFont::GlyphSize usedGlyphSize = rerenderGlyphs ? glyphSize : AtomFont::defaultGlyphSize;
bool texUpdateNeeded = m_fontTexture->PreCacheString(str, nullptr, m_sizeRatio, usedGlyphSize, m_fontHintParams) == 1 || m_fontTexDirty;
if (m_fontInitializationState == InitializationState::Initialized && updateTexture && texUpdateNeeded && m_fontImage)
if (updateTexture && texUpdateNeeded && m_fontImage)
{
UpdateTexture();
m_fontTexDirty = false;
@ -1625,12 +1570,6 @@ void AZ::FFont::ScaleCoord(const RHI::Viewport& viewport, float& x, float& y) co
y *= height / WindowScaleHeight;
}
void AZ::FFont::OnBootstrapSceneReady([[maybe_unused]] AZ::RPI::Scene* bootstrapScene)
{
InitFont(bootstrapScene);
}
static void SetCommonContextFlags(AZ::TextDrawContext& ctx, const AzFramework::TextDrawParameters& params)
{
if (params.m_hAlign == AzFramework::TextHorizontalAlignment::Center)

@ -78,11 +78,9 @@ namespace AZ
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<EditorMaterialComponentSlot>()
->Version(4, &EditorMaterialComponentSlot::ConvertVersion)
->Version(5, &EditorMaterialComponentSlot::ConvertVersion)
->Field("id", &EditorMaterialComponentSlot::m_id)
->Field("materialAsset", &EditorMaterialComponentSlot::m_materialAsset)
->Field("propertyOverrides", &EditorMaterialComponentSlot::m_propertyOverrides)
->Field("matModUvOverrides", &EditorMaterialComponentSlot::m_matModUvOverrides)
;
if (AZ::EditContext* editContext = serializeContext->GetEditContext())

@ -140,9 +140,16 @@ namespace AZ
AZ::Vector3 nonUniformScale = AZ::Vector3::CreateOne();
AZ::NonUniformScaleRequestBus::EventResult(nonUniformScale, GetEntityId(), &AZ::NonUniformScaleRequests::GetScale);
float t;
AZ::Vector3 ignoreNormal;
constexpr float rayLength = 1000.0f;
if (m_controller.GetModel()->RayIntersection(transform, nonUniformScale, src, dir * rayLength, t, ignoreNormal))
{
distance = rayLength * t;
return true;
}
return m_controller.GetModel()->RayIntersection(transform, nonUniformScale, src, dir, distance, ignoreNormal);
return false;
}
bool EditorMeshComponent::SupportsEditorRayIntersect()

@ -485,16 +485,12 @@ namespace AZ
m_transformInterface->GetWorldTM(), m_cachedNonUniformScale, ray.m_startWorldPosition,
ray.m_endWorldPosition - ray.m_startWorldPosition, t, normal))
{
// note: this is a temporary workaround to handle cases where model->RayIntersection
// returns negative distances, follow-up ATOM-15673
const auto absT = AZStd::abs(t);
// fill in ray result structure after successful intersection
const auto intersectionLine = (ray.m_endWorldPosition - ray.m_startWorldPosition);
result.m_uv = AZ::Vector2::CreateZero();
result.m_worldPosition = ray.m_startWorldPosition + intersectionLine * absT;
result.m_worldPosition = ray.m_startWorldPosition + intersectionLine * t;
result.m_worldNormal = normal;
result.m_distance = intersectionLine.GetLength() * absT;
result.m_distance = intersectionLine.GetLength() * t;
result.m_entityAndComponent = m_entityComponentIdPair;
}
}

@ -11,7 +11,6 @@
set(FILES
BuiltInPackages.cmake
FindClang.cmake
FindOpenGLInterface.cmake
FindRadTelemetry.cmake
FindVkValidation.cmake

@ -15,7 +15,11 @@ ly_set(LY_DEFAULT_INSTALL_COMPONENT Core)
file(RELATIVE_PATH runtime_output_directory ${CMAKE_BINARY_DIR} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
file(RELATIVE_PATH library_output_directory ${CMAKE_BINARY_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
set(install_output_folder "${CMAKE_INSTALL_PREFIX}/${runtime_output_directory}/${PAL_PLATFORM_NAME}/$<CONFIG>")
# Anywhere CMAKE_INSTALL_PREFIX is used, it has to be escaped so it is baked into the cmake_install.cmake script instead
# of baking the path. This is needed so `cmake --install --prefix <someprefix>` works regardless of the CMAKE_INSTALL_PREFIX
# used to generate the solution.
# CMAKE_INSTALL_PREFIX is still used when building the INSTALL target
set(install_output_folder "\${CMAKE_INSTALL_PREFIX}/${runtime_output_directory}/${PAL_PLATFORM_NAME}/$<CONFIG>")
#! ly_setup_target: Setup the data needed to re-create the cmake target commands for a single target

@ -167,6 +167,9 @@ endfunction()
# Add the projects here so the above function is found
foreach(project ${LY_PROJECTS})
file(REAL_PATH ${project} full_directory_path BASE_DIRECTORY ${CMAKE_SOURCE_DIR})
if(NOT LY_FIRST_PROJECT)
ly_set(LY_FIRST_PROJECT_PATH ${full_directory_path})
endif()
string(SHA256 full_directory_hash ${full_directory_path})
# Truncate the full_directory_hash down to 8 characters to avoid hitting the Windows 260 character path limit

Loading…
Cancel
Save