From d84bb6a72fc9c7928b1fb2aa20c5235d2c1691e6 Mon Sep 17 00:00:00 2001 From: Allen Jackson <23512001+jackalbe@users.noreply.github.com> Date: Fri, 15 Oct 2021 07:32:10 -0500 Subject: [PATCH 1/4] {LYN5384} splitting Blast builder and processor Python scripts (#4712) splitting the asset builder and asset processor Python scripts for the Blast processor This fixes a mulitiple build issue found while developing other scripts Signed-off-by: jackalbe <23512001+jackalbe@users.noreply.github.com> --- .../Editor/Scripts/blast_asset_builder.py | 125 +--------------- .../Editor/Scripts/blast_chunk_processor.py | 141 ++++++++++++++++++ 2 files changed, 144 insertions(+), 122 deletions(-) create mode 100644 Gems/Blast/Editor/Scripts/blast_chunk_processor.py diff --git a/Gems/Blast/Editor/Scripts/blast_asset_builder.py b/Gems/Blast/Editor/Scripts/blast_asset_builder.py index 06dc15f5c1..a570489ed1 100644 --- a/Gems/Blast/Editor/Scripts/blast_asset_builder.py +++ b/Gems/Blast/Editor/Scripts/blast_asset_builder.py @@ -95,7 +95,9 @@ def generate_assetinfo_product(request): outputFilename = os.path.join(request.tempDirPath, assetinfoFilename) # the only rule in it is to run this file again as a scene processor - currentScript = pathlib.Path(__file__).resolve() + currentScript = str(pathlib.Path(__file__).resolve()) + currentScript = currentScript.replace('\\', '/').lower() + currentScript = currentScript.replace('blast_asset_builder.py', 'blast_chunk_processor.py') aDict = {"values": [{"$type": "ScriptProcessorRule", "scriptFilename": f"{currentScript}"}]} jsonString = json.dumps(aDict) jsonFile = open(outputFilename, "w") @@ -167,124 +169,3 @@ try: pythonAssetBuilderHandler = register_asset_builder() except: pythonAssetBuilderHandler = None - -# -# SceneAPI Processor -# -blastChunksAssetType = azlmbr.math.Uuid_CreateString('{993F0B0F-37D9-48C6-9CC2-E27D3F3E343E}', 0) - -def export_chunk_asset(scene, outputDirectory, platformIdentifier, productList): - import azlmbr.scene - import azlmbr.object - import azlmbr.paths - import json, os - - jsonFilename = os.path.basename(scene.sourceFilename) - jsonFilename = os.path.join(outputDirectory, jsonFilename + '.blast_chunks') - - # prepare output folder - basePath, _ = os.path.split(jsonFilename) - outputPath = os.path.join(outputDirectory, basePath) - if not os.path.exists(outputPath): - os.makedirs(outputPath, False) - - # write out a JSON file with the chunk file info - with open(jsonFilename, "w") as jsonFile: - jsonFile.write(scene.manifest.ExportToJson()) - - exportProduct = azlmbr.scene.ExportProduct() - exportProduct.filename = jsonFilename - exportProduct.sourceId = scene.sourceGuid - exportProduct.assetType = blastChunksAssetType - exportProduct.subId = 101 - - exportProductList = azlmbr.scene.ExportProductList() - exportProductList.AddProduct(exportProduct) - return exportProductList - -def on_prepare_for_export(args): - try: - scene = args[0] # azlmbr.scene.Scene - outputDirectory = args[1] # string - platformIdentifier = args[2] # string - productList = args[3] # azlmbr.scene.ExportProductList - return export_chunk_asset(scene, outputDirectory, platformIdentifier, productList) - except: - log_exception_traceback() - -def get_mesh_node_names(sceneGraph): - import azlmbr.scene as sceneApi - import azlmbr.scene.graph - from scene_api import scene_data as sceneData - - meshDataList = [] - node = sceneGraph.get_root() - children = [] - - while node.IsValid(): - # store children to process after siblings - if sceneGraph.has_node_child(node): - children.append(sceneGraph.get_node_child(node)) - - # store any node that has mesh data content - nodeContent = sceneGraph.get_node_content(node) - if nodeContent is not None and nodeContent.CastWithTypeName('MeshData'): - if sceneGraph.is_node_end_point(node) is False: - nodeName = sceneData.SceneGraphName(sceneGraph.get_node_name(node)) - nodePath = nodeName.get_path() - if (len(nodeName.get_path())): - meshDataList.append(sceneData.SceneGraphName(sceneGraph.get_node_name(node))) - - # advance to next node - if sceneGraph.has_node_sibling(node): - node = sceneGraph.get_node_sibling(node) - elif children: - node = children.pop() - else: - node = azlmbr.scene.graph.NodeIndex() - - return meshDataList - -def update_manifest(scene): - import uuid, os - import azlmbr.scene as sceneApi - import azlmbr.scene.graph - from scene_api import scene_data as sceneData - - graph = sceneData.SceneGraph(scene.graph) - meshNameList = get_mesh_node_names(graph) - sceneManifest = sceneData.SceneManifest() - sourceFilenameOnly = os.path.basename(scene.sourceFilename) - sourceFilenameOnly = sourceFilenameOnly.replace('.','_') - - for activeMeshIndex in range(len(meshNameList)): - chunkName = meshNameList[activeMeshIndex] - chunkPath = chunkName.get_path() - meshGroupName = '{}_{}'.format(sourceFilenameOnly, chunkName.get_name()) - meshGroup = sceneManifest.add_mesh_group(meshGroupName) - meshGroup['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, sourceFilenameOnly + chunkPath)) + '}' - sceneManifest.mesh_group_select_node(meshGroup, chunkPath) - - return sceneManifest.export() - -sceneJobHandler = None - -def on_update_manifest(args): - try: - scene = args[0] - return update_manifest(scene) - except: - global sceneJobHandler - sceneJobHandler = None - log_exception_traceback() - -# try to create SceneAPI handler for processing -try: - import azlmbr.scene as sceneApi - if (sceneJobHandler == None): - sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler() - sceneJobHandler.connect() - sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest) - sceneJobHandler.add_callback('OnPrepareForExport', on_prepare_for_export) -except: - sceneJobHandler = None diff --git a/Gems/Blast/Editor/Scripts/blast_chunk_processor.py b/Gems/Blast/Editor/Scripts/blast_chunk_processor.py new file mode 100644 index 0000000000..d112f465e9 --- /dev/null +++ b/Gems/Blast/Editor/Scripts/blast_chunk_processor.py @@ -0,0 +1,141 @@ +""" +Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution. + +SPDX-License-Identifier: Apache-2.0 OR MIT +""" + +""" +This a Python Asset Builder script examines each .blast file to see if an +associated .fbx file needs to be processed by exporting all of its chunks +into a scene manifest + +This is also a SceneAPI script that executes from a foo.fbx.assetinfo scene +manifest that writes out asset chunk data for .blast files +""" +import os, traceback, binascii, sys, json, pathlib +import azlmbr.math +import azlmbr.asset +import azlmbr.asset.entity +import azlmbr.asset.builder +import azlmbr.bus + +# +# SceneAPI Processor +# +blastChunksAssetType = azlmbr.math.Uuid_CreateString('{993F0B0F-37D9-48C6-9CC2-E27D3F3E343E}', 0) + +def export_chunk_asset(scene, outputDirectory, platformIdentifier, productList): + import azlmbr.scene + import azlmbr.object + import azlmbr.paths + import json, os + + jsonFilename = os.path.basename(scene.sourceFilename) + jsonFilename = os.path.join(outputDirectory, jsonFilename + '.blast_chunks') + + # prepare output folder + basePath, _ = os.path.split(jsonFilename) + outputPath = os.path.join(outputDirectory, basePath) + if not os.path.exists(outputPath): + os.makedirs(outputPath, False) + + # write out a JSON file with the chunk file info + with open(jsonFilename, "w") as jsonFile: + jsonFile.write(scene.manifest.ExportToJson()) + + exportProduct = azlmbr.scene.ExportProduct() + exportProduct.filename = jsonFilename + exportProduct.sourceId = scene.sourceGuid + exportProduct.assetType = blastChunksAssetType + exportProduct.subId = 101 + + exportProductList = azlmbr.scene.ExportProductList() + exportProductList.AddProduct(exportProduct) + return exportProductList + +def on_prepare_for_export(args): + try: + scene = args[0] # azlmbr.scene.Scene + outputDirectory = args[1] # string + platformIdentifier = args[2] # string + productList = args[3] # azlmbr.scene.ExportProductList + return export_chunk_asset(scene, outputDirectory, platformIdentifier, productList) + except: + log_exception_traceback() + +def get_mesh_node_names(sceneGraph): + import azlmbr.scene as sceneApi + import azlmbr.scene.graph + from scene_api import scene_data as sceneData + + meshDataList = [] + node = sceneGraph.get_root() + children = [] + + while node.IsValid(): + # store children to process after siblings + if sceneGraph.has_node_child(node): + children.append(sceneGraph.get_node_child(node)) + + # store any node that has mesh data content + nodeContent = sceneGraph.get_node_content(node) + if nodeContent is not None and nodeContent.CastWithTypeName('MeshData'): + if sceneGraph.is_node_end_point(node) is False: + nodeName = sceneData.SceneGraphName(sceneGraph.get_node_name(node)) + nodePath = nodeName.get_path() + if (len(nodeName.get_path())): + meshDataList.append(sceneData.SceneGraphName(sceneGraph.get_node_name(node))) + + # advance to next node + if sceneGraph.has_node_sibling(node): + node = sceneGraph.get_node_sibling(node) + elif children: + node = children.pop() + else: + node = azlmbr.scene.graph.NodeIndex() + + return meshDataList + +def update_manifest(scene): + import uuid, os + import azlmbr.scene as sceneApi + import azlmbr.scene.graph + from scene_api import scene_data as sceneData + + graph = sceneData.SceneGraph(scene.graph) + meshNameList = get_mesh_node_names(graph) + sceneManifest = sceneData.SceneManifest() + sourceFilenameOnly = os.path.basename(scene.sourceFilename) + sourceFilenameOnly = sourceFilenameOnly.replace('.','_') + + for activeMeshIndex in range(len(meshNameList)): + chunkName = meshNameList[activeMeshIndex] + chunkPath = chunkName.get_path() + meshGroupName = '{}_{}'.format(sourceFilenameOnly, chunkName.get_name()) + meshGroup = sceneManifest.add_mesh_group(meshGroupName) + meshGroup['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, sourceFilenameOnly + chunkPath)) + '}' + sceneManifest.mesh_group_select_node(meshGroup, chunkPath) + + return sceneManifest.export() + +sceneJobHandler = None + +def on_update_manifest(args): + try: + scene = args[0] + return update_manifest(scene) + except: + global sceneJobHandler + sceneJobHandler = None + log_exception_traceback() + +# try to create SceneAPI handler for processing +try: + import azlmbr.scene as sceneApi + if (sceneJobHandler == None): + sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler() + sceneJobHandler.connect() + sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest) + sceneJobHandler.add_callback('OnPrepareForExport', on_prepare_for_export) +except: + sceneJobHandler = None From 969a55170e362e8d025c0354a825de0cafcb7f26 Mon Sep 17 00:00:00 2001 From: amzn-mike <80125227+amzn-mike@users.noreply.github.com> Date: Fri, 15 Oct 2021 07:33:01 -0500 Subject: [PATCH 2/4] Procedural Prefabs: Entity parenting fixes (#4669) * Parent top level entities to container entity when creating prefab Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> * Add to_json method to PythonProxyObject to allow serializing any AZ serialializable type Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> * Update scene_mesh_to_prefab.py to parent entities in a chain Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> * Remove redundant eval Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> * Improve error handling in ToJson Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> * Add maybe_unused for commonRoot since it's not used Signed-off-by: amzn-mike <80125227+amzn-mike@users.noreply.github.com> --- .../Editor/Scripts/scene_mesh_to_prefab.py | 30 +++++++++++++-- .../Prefab/PrefabSystemScriptingHandler.cpp | 27 ++++++++++++- .../Code/Source/PythonProxyObject.cpp | 38 +++++++++++++++++++ .../Code/Source/PythonProxyObject.h | 2 + 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py b/AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py index d5a7acfe91..4aeff06f18 100644 --- a/AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py +++ b/AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py @@ -74,6 +74,7 @@ def update_manifest(scene): source_filename_only = os.path.basename(clean_filename) created_entities = [] + previous_entity_id = azlmbr.entity.InvalidEntityId # Loop every mesh node in the scene for activeMeshIndex in range(len(mesh_name_list)): @@ -102,14 +103,33 @@ def update_manifest(scene): # The MeshGroup we created will be output as a product in the asset's path named mesh_group_name.azmodel # The assetHint will be converted to an AssetId later during prefab loading json_update = json.dumps({ - "Controller": { "Configuration": { "ModelAsset": { - "assetHint": os.path.join(source_relative_path, mesh_group_name) + ".azmodel" }}} - }); + "Controller": { "Configuration": { "ModelAsset": { + "assetHint": os.path.join(source_relative_path, mesh_group_name) + ".azmodel" }}} + }); # Apply the JSON above to the component we created result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, editor_mesh_component, json_update) if not result: - raise RuntimeError("UpdateComponentForEntity failed") + raise RuntimeError("UpdateComponentForEntity failed for Mesh component") + + # Get the transform component + transform_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", entity_id, "27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0") + + # Set this entity to be a child of the last entity we created + # This is just an example of how to do parenting and isn't necessarily useful to parent everything like this + if previous_entity_id is not None: + transform_json = json.dumps({ + "Parent Entity" : previous_entity_id.to_json() + }); + + # Apply the JSON update + result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, transform_component, transform_json) + + if not result: + raise RuntimeError("UpdateComponentForEntity failed for Transform component") + + # Update the last entity id for next time + previous_entity_id = entity_id # Keep track of the entity we set up, we'll add them all to the prefab we're creating later created_entities.append(entity_id) @@ -147,6 +167,8 @@ def on_update_manifest(args): except RuntimeError as err: print (f'ERROR - {err}') log_exception_traceback() + except: + log_exception_traceback() global sceneJobHandler sceneJobHandler = None diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemScriptingHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemScriptingHandler.cpp index d34e2cfc92..884c463bff 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemScriptingHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemScriptingHandler.cpp @@ -6,11 +6,14 @@ * */ +#include #include #include #include #include #include +#include +#include namespace AzToolsFramework::Prefab { @@ -61,9 +64,29 @@ namespace AzToolsFramework::Prefab entities.push_back(entity); } } - - auto prefab = m_prefabSystemComponentInterface->CreatePrefab(entities, {}, AZ::IO::PathView(AZStd::string_view(filePath))); + bool result = false; + [[maybe_unused]] AZ::EntityId commonRoot; + EntityList topLevelEntities; + AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(result, &AzToolsFramework::ToolsApplicationRequestBus::Events::FindCommonRootInactive, + entities, commonRoot, &topLevelEntities); + + auto containerEntity = AZStd::make_unique(); + + for (AZ::Entity* entity : topLevelEntities) + { + AzToolsFramework::Components::TransformComponent* transformComponent = + entity->FindComponent(); + + if (transformComponent) + { + transformComponent->SetParent(containerEntity->GetId()); + } + } + + auto prefab = m_prefabSystemComponentInterface->CreatePrefab( + entities, {}, AZ::IO::PathView(AZStd::string_view(filePath)), AZStd::move(containerEntity)); + if (!prefab) { AZ_Error("PrefabSystemComponenent", false, "Failed to create prefab %s", filePath.c_str()); diff --git a/Gems/EditorPythonBindings/Code/Source/PythonProxyObject.cpp b/Gems/EditorPythonBindings/Code/Source/PythonProxyObject.cpp index 706ca48156..df246d230d 100644 --- a/Gems/EditorPythonBindings/Code/Source/PythonProxyObject.cpp +++ b/Gems/EditorPythonBindings/Code/Source/PythonProxyObject.cpp @@ -17,10 +17,16 @@ #include #include +#include +#include #include +#include #include #include +#include +#include +#include namespace EditorPythonBindings { @@ -571,6 +577,37 @@ namespace EditorPythonBindings return false; } + pybind11::object PythonProxyObject::ToJson() + { + rapidjson::Document document; + AZ::JsonSerializerSettings settings; + settings.m_keepDefaults = true; + + auto resultCode = + AZ::JsonSerialization::Store(document, document.GetAllocator(), m_wrappedObject.m_address, nullptr, m_wrappedObject.m_typeId, settings); + + if (resultCode.GetProcessing() == AZ::JsonSerializationResult::Processing::Halted) + { + AZ_Error("PythonProxyObject", false, "Failed to serialize to json"); + return pybind11::cast(Py_None); + } + + AZStd::string jsonString; + AZ::Outcome outcome = AZ::JsonSerializationUtils::WriteJsonString(document, jsonString); + + if (!outcome.IsSuccess()) + { + AZ_Error("PythonProxyObject", false, "Failed to write json string: %s", outcome.GetError().c_str()); + return pybind11::cast(Py_None); + } + + jsonString.erase(AZStd::remove(jsonString.begin(), jsonString.end(), '\n'), jsonString.end()); + auto pythonCode = AZStd::string::format( + R"PYTHON(exec("import json") or json.loads("""%s"""))PYTHON", jsonString.c_str()); + + return pybind11::eval(pythonCode.c_str()); + } + bool PythonProxyObject::DoComparisonEvaluation(pybind11::object pythonOther, Comparison comparison) { bool invertLogic = false; @@ -912,6 +949,7 @@ namespace EditorPythonBindings .def("set_property", &PythonProxyObject::SetPropertyValue) .def("get_property", &PythonProxyObject::GetPropertyValue) .def("invoke", &PythonProxyObject::Invoke) + .def("to_json", &PythonProxyObject::ToJson) .def(Operator::s_isEqual, [](PythonProxyObject& self, pybind11::object rhs) { return self.DoEqualityEvaluation(rhs); diff --git a/Gems/EditorPythonBindings/Code/Source/PythonProxyObject.h b/Gems/EditorPythonBindings/Code/Source/PythonProxyObject.h index 5b611c32c0..a9d2fad5a0 100644 --- a/Gems/EditorPythonBindings/Code/Source/PythonProxyObject.h +++ b/Gems/EditorPythonBindings/Code/Source/PythonProxyObject.h @@ -58,6 +58,8 @@ namespace EditorPythonBindings //! Performs an equality operation to compare this object with another object bool DoEqualityEvaluation(pybind11::object pythonOther); + pybind11::object ToJson(); + //! Perform a comparison of a Python operator enum class Comparison { From 87533d80c11812c14b1262b151493f3be655739e Mon Sep 17 00:00:00 2001 From: srikappa-amzn <82230713+srikappa-amzn@users.noreply.github.com> Date: Fri, 15 Oct 2021 18:25:26 +0530 Subject: [PATCH 3/4] Delay propagation for all template updates in detach prefab workflow (#4707) * Delay propagation for all template updates in detach prefab workflow Signed-off-by: srikappa-amzn * Some minor changes to the PrefabUndo constructor Signed-off-by: srikappa-amzn --- .../Prefab/PrefabPublicHandler.cpp | 6 +++--- .../AzToolsFramework/Prefab/PrefabUndo.cpp | 14 ++++---------- .../AzToolsFramework/Prefab/PrefabUndo.h | 8 ++++---- .../AzToolsFramework/Prefab/PrefabUndoHelpers.cpp | 4 ++-- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index 081655a166..ee34b628bb 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -1052,10 +1052,10 @@ namespace AzToolsFramework DuplicateNestedEntitiesInInstance(commonOwningInstance->get(), entities, instanceDomAfter, duplicatedEntityAndInstanceIds, duplicateEntityAliasMap); - PrefabUndoInstance* command = aznew PrefabUndoInstance("Entity/Instance duplication"); + PrefabUndoInstance* command = aznew PrefabUndoInstance("Entity/Instance duplication", false); command->SetParent(undoBatch.GetUndoBatch()); command->Capture(instanceDomBefore, instanceDomAfter, commonOwningInstance->get().GetTemplateId()); - command->RedoBatched(); + command->Redo(); DuplicateNestedInstancesInInstance(commonOwningInstance->get(), instances, instanceDomAfter, duplicatedEntityAndInstanceIds, newInstanceAliasToOldInstanceMap); @@ -1323,7 +1323,7 @@ namespace AzToolsFramework Prefab::PrefabDom instanceDomAfter; m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomAfter, parentInstance); - PrefabUndoInstance* command = aznew PrefabUndoInstance("Instance detachment"); + PrefabUndoInstance* command = aznew PrefabUndoInstance("Instance detachment", false); command->Capture(instanceDomBefore, instanceDomAfter, parentTemplateId); command->SetParent(undoBatch.GetUndoBatch()); { diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp index b298304e3b..6c96209d56 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp @@ -17,17 +17,16 @@ namespace AzToolsFramework { PrefabUndoBase::PrefabUndoBase(const AZStd::string& undoOperationName) : UndoSystem::URSequencePoint(undoOperationName) - , m_changed(true) - , m_templateId(InvalidTemplateId) { m_instanceToTemplateInterface = AZ::Interface::Get(); AZ_Assert(m_instanceToTemplateInterface, "Failed to grab instance to template interface"); } //PrefabInstanceUndo - PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName) + PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation) : PrefabUndoBase(undoOperationName) { + m_useImmediatePropagation = useImmediatePropagation; } void PrefabUndoInstance::Capture( @@ -43,17 +42,12 @@ namespace AzToolsFramework void PrefabUndoInstance::Undo() { - m_instanceToTemplateInterface->PatchTemplate(m_undoPatch, m_templateId, true); + m_instanceToTemplateInterface->PatchTemplate(m_undoPatch, m_templateId, m_useImmediatePropagation); } void PrefabUndoInstance::Redo() { - m_instanceToTemplateInterface->PatchTemplate(m_redoPatch, m_templateId, true); - } - - void PrefabUndoInstance::RedoBatched() - { - m_instanceToTemplateInterface->PatchTemplate(m_redoPatch, m_templateId); + m_instanceToTemplateInterface->PatchTemplate(m_redoPatch, m_templateId, m_useImmediatePropagation); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h index 8669024df7..0946a36951 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h @@ -29,14 +29,15 @@ namespace AzToolsFramework bool Changed() const override { return m_changed; } protected: - TemplateId m_templateId; + TemplateId m_templateId = InvalidTemplateId; PrefabDom m_redoPatch; PrefabDom m_undoPatch; InstanceToTemplateInterface* m_instanceToTemplateInterface = nullptr; - bool m_changed; + bool m_changed = true; + bool m_useImmediatePropagation = true; }; //! handles the addition and removal of entities from instances @@ -44,7 +45,7 @@ namespace AzToolsFramework : public PrefabUndoBase { public: - explicit PrefabUndoInstance(const AZStd::string& undoOperationName); + explicit PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation = true); void Capture( const PrefabDom& initialState, @@ -53,7 +54,6 @@ namespace AzToolsFramework void Undo() override; void Redo() override; - void RedoBatched(); }; //! handles entity updates, such as when the values on an entity change diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndoHelpers.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndoHelpers.cpp index 9803b55324..31a0c60bcb 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndoHelpers.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndoHelpers.cpp @@ -23,10 +23,10 @@ namespace AzToolsFramework PrefabDom instanceDomAfterUpdate; PrefabDomUtils::StoreInstanceInPrefabDom(instance, instanceDomAfterUpdate); - PrefabUndoInstance* state = aznew Prefab::PrefabUndoInstance(undoMessage); + PrefabUndoInstance* state = aznew Prefab::PrefabUndoInstance(undoMessage, false); state->Capture(instanceDomBeforeUpdate, instanceDomAfterUpdate, instance.GetTemplateId()); state->SetParent(undoBatch); - state->RedoBatched(); + state->Redo(); } LinkId CreateLink( From 9aafc51ff5a2551cabed9fe322917ed6842f1dda Mon Sep 17 00:00:00 2001 From: hultonha <82228511+hultonha@users.noreply.github.com> Date: Fri, 15 Oct 2021 15:25:27 +0100 Subject: [PATCH 4/4] Add first pass version of click feedback while in Focus Mode (#4693) * add first pass version of click feedback while in Focus Mode Signed-off-by: hultonha * add more WIP experimental feedback ideas for Focus Mode Signed-off-by: hultonha * small updates after UX feedback to improve focus mode feedback Signed-off-by: hultonha * refactor and improve invalid click feedback Signed-off-by: hultonha * update comments from review feedback Signed-off-by: hultonha --- .../ViewportSelection/EditorHelpers.cpp | 33 +++- .../ViewportSelection/EditorHelpers.h | 20 ++- .../EditorTransformComponentSelection.cpp | 2 + .../ViewportSelection/InvalidClicks.cpp | 142 ++++++++++++++++++ .../ViewportSelection/InvalidClicks.h | 108 +++++++++++++ .../aztoolsframework_files.cmake | 2 + .../AtomDebugDisplayViewportInterface.cpp | 3 +- 7 files changed, 297 insertions(+), 13 deletions(-) create mode 100644 Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/InvalidClicks.cpp create mode 100644 Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/InvalidClicks.h diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp index bb718d0dc9..6399a64635 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp @@ -9,6 +9,7 @@ #include "EditorHelpers.h" #include +#include #include #include #include @@ -123,6 +124,11 @@ namespace AzToolsFramework "EditorHelpers - " "Focus Mode Interface could not be found. " "Check that it is being correctly initialized."); + + AZStd::vector> invalidClicks; + invalidClicks.push_back(AZStd::make_unique("Not in focus")); + invalidClicks.push_back(AZStd::make_unique()); + m_invalidClicks = AZStd::make_unique(AZStd::move(invalidClicks)); } AZ::EntityId EditorHelpers::HandleMouseInteraction( @@ -186,13 +192,20 @@ namespace AzToolsFramework } } - // Verify if the entity Id corresponds to an entity that is focused; if not, halt selection. - if (!IsSelectableAccordingToFocusMode(entityIdUnderCursor)) + // verify if the entity Id corresponds to an entity that is focused; if not, halt selection. + if (entityIdUnderCursor.IsValid() && !IsSelectableAccordingToFocusMode(entityIdUnderCursor)) { + if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() && + mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down || + mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::DoubleClick) + { + m_invalidClicks->AddInvalidClick(mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates); + } + return AZ::EntityId(); } - // Container Entity support - if the entity that is being selected is part of a closed container, + // container entity support - if the entity that is being selected is part of a closed container, // change the selection to the container instead. if (ContainerEntityInterface* containerEntityInterface = AZ::Interface::Get()) { @@ -202,6 +215,12 @@ namespace AzToolsFramework return entityIdUnderCursor; } + void EditorHelpers::Display2d( + [[maybe_unused]] const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) + { + m_invalidClicks->Display2d(viewportInfo, debugDisplay); + } + void EditorHelpers::DisplayHelpers( const AzFramework::ViewportInfo& viewportInfo, const AzFramework::CameraState& cameraState, @@ -263,19 +282,19 @@ namespace AzToolsFramework } } - bool EditorHelpers::IsSelectableInViewport(AZ::EntityId entityId) + bool EditorHelpers::IsSelectableInViewport(const AZ::EntityId entityId) const { return IsSelectableAccordingToFocusMode(entityId) && IsSelectableAccordingToContainerEntities(entityId); } - bool EditorHelpers::IsSelectableAccordingToFocusMode(AZ::EntityId entityId) + bool EditorHelpers::IsSelectableAccordingToFocusMode(const AZ::EntityId entityId) const { return m_focusModeInterface->IsInFocusSubTree(entityId); } - bool EditorHelpers::IsSelectableAccordingToContainerEntities(AZ::EntityId entityId) + bool EditorHelpers::IsSelectableAccordingToContainerEntities(const AZ::EntityId entityId) const { - if (ContainerEntityInterface* containerEntityInterface = AZ::Interface::Get()) + if (const auto* containerEntityInterface = AZ::Interface::Get()) { return !containerEntityInterface->IsUnderClosedContainerEntity(entityId); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.h b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.h index 909a231635..6623221bdc 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.h @@ -11,6 +11,9 @@ #include #include #include +#include +#include +#include namespace AzFramework { @@ -58,20 +61,27 @@ namespace AzToolsFramework AzFramework::DebugDisplayRequests& debugDisplay, const AZStd::function& showIconCheck); + //! Handle 2d drawing for EditorHelper functionality. + void Display2d( + const AzFramework::ViewportInfo& viewportInfo, + AzFramework::DebugDisplayRequests& debugDisplay); + //! Returns whether the entityId can be selected in the viewport according //! to the current Editor Focus Mode and Container Entity setup. - bool IsSelectableInViewport(AZ::EntityId entityId); + bool IsSelectableInViewport(AZ::EntityId entityId) const; private: //! Returns whether the entityId can be selected in the viewport according //! to the current Editor Focus Mode setup. - bool IsSelectableAccordingToFocusMode(AZ::EntityId entityId); + bool IsSelectableAccordingToFocusMode(AZ::EntityId entityId) const; //! Returns whether the entityId can be selected in the viewport according - //! to the current Container Entityu setup. - bool IsSelectableAccordingToContainerEntities(AZ::EntityId entityId); + //! to the current Container Entity setup. + bool IsSelectableAccordingToContainerEntities(AZ::EntityId entityId) const; + + AZStd::unique_ptr m_invalidClicks; //!< Display for invalid click behavior. const EditorVisibleEntityDataCache* m_entityDataCache = nullptr; //!< Entity Data queried by the EditorHelpers. - const FocusModeInterface* m_focusModeInterface = nullptr; + const FocusModeInterface* m_focusModeInterface = nullptr; //!< API to interact with focus mode functionality. }; } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp index a37b84fdf8..c73742da4d 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp @@ -3560,6 +3560,8 @@ namespace AzToolsFramework DrawAxisGizmo(viewportInfo, debugDisplay); m_boxSelect.Display2d(viewportInfo, debugDisplay); + + m_editorHelpers->Display2d(viewportInfo, debugDisplay); } void EditorTransformComponentSelection::RefreshSelectedEntityIds() diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/InvalidClicks.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/InvalidClicks.cpp new file mode 100644 index 0000000000..dfff4da08f --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/InvalidClicks.cpp @@ -0,0 +1,142 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include + +AZ_CVAR(float, ed_invalidClickRadius, 10.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum invalid click radius to expand to"); +AZ_CVAR(float, ed_invalidClickDuration, 1.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "Duration to display the invalid click feedback"); +AZ_CVAR(float, ed_invalidClickMessageSize, 0.8f, nullptr, AZ::ConsoleFunctorFlags::Null, "Size of text for invalid message"); +AZ_CVAR( + float, + ed_invalidClickMessageVerticalOffset, + 30.0f, + nullptr, + AZ::ConsoleFunctorFlags::Null, + "Vertical offset from cursor of invalid click message"); + +namespace AzToolsFramework +{ + void ExpandingFadingCircles::Begin(const AzFramework::ScreenPoint& screenPoint) + { + FadingCircle fadingCircle; + fadingCircle.m_position = screenPoint; + fadingCircle.m_opacity = 1.0f; + fadingCircle.m_radius = 0.0f; + m_fadingCircles.push_back(fadingCircle); + } + + void ExpandingFadingCircles::Update(const float deltaTime) + { + for (auto& fadingCircle : m_fadingCircles) + { + fadingCircle.m_opacity = AZStd::max(fadingCircle.m_opacity - (deltaTime / ed_invalidClickDuration), 0.0f); + fadingCircle.m_radius += deltaTime * ed_invalidClickRadius; + } + + m_fadingCircles.erase( + AZStd::remove_if( + m_fadingCircles.begin(), m_fadingCircles.end(), + [](const FadingCircle& fadingCircle) + { + return fadingCircle.m_opacity <= 0.0f; + }), + m_fadingCircles.end()); + } + + bool ExpandingFadingCircles::Updating() + { + return !m_fadingCircles.empty(); + } + + void ExpandingFadingCircles::Display(const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) + { + const AZ::Vector2 viewportSize = AzToolsFramework::GetCameraState(viewportInfo.m_viewportId).m_viewportSize; + + for (const auto& fadingCircle : m_fadingCircles) + { + const auto position = AzFramework::Vector2FromScreenPoint(fadingCircle.m_position) / viewportSize; + debugDisplay.SetColor(AZ::Color(1.0f, 1.0f, 1.0f, fadingCircle.m_opacity)); + debugDisplay.DrawWireCircle2d(position, fadingCircle.m_radius * 0.005f, 0.0f); + } + } + + void FadingText::Begin(const AzFramework::ScreenPoint& screenPoint) + { + m_opacity = 1.0f; + m_invalidClickPosition = screenPoint; + } + + void FadingText::Update(const float deltaTime) + { + m_opacity -= deltaTime / ed_invalidClickDuration; + } + + bool FadingText::Updating() + { + return m_opacity >= 0.0f; + } + + void FadingText::Display( + [[maybe_unused]] const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) + { + if (constexpr float MinOpacity = 0.05f; m_opacity >= MinOpacity) + { + debugDisplay.SetColor(AZ::Color(1.0f, 1.0f, 1.0f, m_opacity)); + debugDisplay.Draw2dTextLabel( + aznumeric_cast(m_invalidClickPosition.m_x), + aznumeric_cast(m_invalidClickPosition.m_y) - ed_invalidClickMessageVerticalOffset, ed_invalidClickMessageSize, + m_message.c_str(), true); + } + } + + void InvalidClicks::AddInvalidClick(const AzFramework::ScreenPoint& screenPoint) + { + AZ::TickBus::Handler::BusConnect(); + + for (auto& invalidClickBehavior : m_invalidClickBehaviors) + { + invalidClickBehavior->Begin(screenPoint); + } + } + + void InvalidClicks::OnTick(const float deltaTime, [[maybe_unused]] const AZ::ScriptTimePoint time) + { + for (auto& invalidClickBehavior : m_invalidClickBehaviors) + { + invalidClickBehavior->Update(deltaTime); + } + + const auto updating = AZStd::any_of( + m_invalidClickBehaviors.begin(), m_invalidClickBehaviors.end(), + [](const auto& invalidClickBehavior) + { + return invalidClickBehavior->Updating(); + }); + + if (!updating && AZ::TickBus::Handler::BusIsConnected()) + { + AZ::TickBus::Handler::BusDisconnect(); + } + } + + void InvalidClicks::Display2d(const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) + { + debugDisplay.DepthTestOff(); + + for (const auto& invalidClickBehavior : m_invalidClickBehaviors) + { + invalidClickBehavior->Display(viewportInfo, debugDisplay); + } + + debugDisplay.DepthTestOn(); + } +} // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/InvalidClicks.h b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/InvalidClicks.h new file mode 100644 index 0000000000..55a6d614ea --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/InvalidClicks.h @@ -0,0 +1,108 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include + +namespace AzFramework +{ + class DebugDisplayRequests; + struct ViewportInfo; +} // namespace AzFramework + +namespace AzToolsFramework +{ + namespace ViewportInteraction + { + struct MouseInteractionEvent; + } + + //! An interface to provide invalid click feedback in the editor viewport. + class InvalidClick + { + public: + virtual ~InvalidClick() = default; + + //! Begin the feedback. + //! @param screenPoint The position of the click in screen coordinates. + virtual void Begin(const AzFramework::ScreenPoint& screenPoint) = 0; + //! Update the invalid click feedback + virtual void Update(float deltaTime) = 0; + //! Report if the click feedback is running or not (returning false will signal the TickBus can be disconnected from). + virtual bool Updating() = 0; + //! Display the click feedback in the viewport. + virtual void Display(const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) = 0; + }; + + //! Display expanding fading circles for every click of the mouse that is invalid. + class ExpandingFadingCircles : public InvalidClick + { + public: + void Begin(const AzFramework::ScreenPoint& screenPoint) override; + void Update(float deltaTime) override; + bool Updating() override; + void Display(const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) override; + + private: + //! Stores a circle representation with a lifetime to grow and fade out over time. + struct FadingCircle + { + AzFramework::ScreenPoint m_position; + float m_radius; + float m_opacity; + }; + + using FadingCircles = AZStd::vector; + FadingCircles m_fadingCircles; //!< Collection of fading circles to draw for clicks that have no effect. + }; + + //! Display fading text where an invalid click happened. + //! @note There is only one fading text, each click will update its position. + class FadingText : public InvalidClick + { + public: + explicit FadingText(AZStd::string message) + : m_message(AZStd::move(message)) + { + } + + void Begin(const AzFramework::ScreenPoint& screenPoint) override; + void Update(float deltaTime) override; + bool Updating() override; + void Display(const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) override; + + private: + AZStd::string m_message; //!< Message to display for fading text. + float m_opacity = 1.0f; //!< The opacity of the invalid click message. + AzFramework::ScreenPoint m_invalidClickPosition; //!< The position to display the invalid click message. + }; + + //! Interface to begin invalid click feedback (will run all added InvalidClick behaviors). + class InvalidClicks : private AZ::TickBus::Handler + { + public: + explicit InvalidClicks(AZStd::vector> invalidClickBehaviors) + : m_invalidClickBehaviors(AZStd::move(invalidClickBehaviors)) + { + } + + //! Add an invalid click and activate one or more of the added invalid click behaviors. + void AddInvalidClick(const AzFramework::ScreenPoint& screenPoint); + + //! Handle 2d drawing for EditorHelper functionality. + void Display2d(const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay); + + private: + //! AZ::TickBus overrides ... + void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; + + AZStd::vector> m_invalidClickBehaviors; //!< Invalid click behaviors to run. + }; +} // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake index 8dec7ab611..5db65f89f4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake @@ -553,6 +553,8 @@ set(FILES ViewportSelection/EditorTransformComponentSelectionRequestBus.cpp ViewportSelection/EditorVisibleEntityDataCache.h ViewportSelection/EditorVisibleEntityDataCache.cpp + ViewportSelection/InvalidClicks.h + ViewportSelection/InvalidClicks.cpp ViewportSelection/ViewportEditorModeTracker.cpp ViewportSelection/ViewportEditorModeTracker.h ToolsFileUtils/ToolsFileUtils.h diff --git a/Gems/AtomLyIntegration/AtomBridge/Code/Source/AtomDebugDisplayViewportInterface.cpp b/Gems/AtomLyIntegration/AtomBridge/Code/Source/AtomDebugDisplayViewportInterface.cpp index b45fbe05f6..39d8933863 100644 --- a/Gems/AtomLyIntegration/AtomBridge/Code/Source/AtomDebugDisplayViewportInterface.cpp +++ b/Gems/AtomLyIntegration/AtomBridge/Code/Source/AtomDebugDisplayViewportInterface.cpp @@ -1353,8 +1353,9 @@ namespace AZ::AtomBridge // if 2d draw need to project pos to screen first AzFramework::TextDrawParameters params; AZ::RPI::ViewportContextPtr viewportContext = GetViewportContext(); + const auto dpiScaleFactor = viewportContext->GetDpiScalingFactor(); params.m_drawViewportId = viewportContext->GetId(); // get the viewport ID so default viewport works - params.m_position = AZ::Vector3(x, y, 1.0f); + params.m_position = AZ::Vector3(x * dpiScaleFactor, y * dpiScaleFactor, 1.0f); params.m_color = m_rendState.m_color; params.m_scale = AZ::Vector2(size); params.m_hAlign = center ? AzFramework::TextHorizontalAlignment::Center : AzFramework::TextHorizontalAlignment::Left; //! Horizontal text alignment