merge from development

Signed-off-by: greerdv <greerdv@amazon.com>
monroegm-disable-blank-issue-2
greerdv 4 years ago
commit dbba71fe3a

@ -0,0 +1,124 @@
#
# 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
#
#
import os, traceback, binascii, sys, json, pathlib, logging
import azlmbr.math
import azlmbr.bus
from scene_helpers import *
#
# SceneAPI Processor
#
def update_manifest(scene):
import uuid
import azlmbr.scene as sceneApi
import azlmbr.scene.graph
from scene_api import scene_data as sceneData
graph = sceneData.SceneGraph(scene.graph)
# Get a list of all the mesh nodes, as well as all the nodes
mesh_name_list, all_node_paths = get_mesh_node_names(graph)
mesh_name_list.sort(key=lambda node: str.casefold(node.get_path()))
scene_manifest = sceneData.SceneManifest()
clean_filename = scene.sourceFilename.replace('.', '_')
# Compute the filename of the scene file
source_basepath = scene.watchFolder
source_relative_path = os.path.dirname(os.path.relpath(clean_filename, source_basepath))
source_filename_only = os.path.basename(clean_filename)
created_entities = []
previous_entity_id = azlmbr.entity.InvalidEntityId
first_mesh = True
# Make a list of mesh node paths
mesh_path_list = list(map(lambda node: node.get_path(), mesh_name_list))
# Assume the first mesh is the main mesh
main_mesh = mesh_name_list[0]
mesh_path = main_mesh.get_path()
# Create a unique mesh group name using the filename + node name
mesh_group_name = '{}_{}'.format(source_filename_only, main_mesh.get_name())
# Remove forbidden filename characters from the name since this will become a file on disk later
mesh_group_name = "".join(char for char in mesh_group_name if char not in "|<>:\"/?*\\")
# Add the MeshGroup to the manifest and give it a unique ID
mesh_group = scene_manifest.add_mesh_group(mesh_group_name)
mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, source_filename_only + mesh_path)) + '}'
# Set our current node as the only node that is included in this MeshGroup
scene_manifest.mesh_group_select_node(mesh_group, mesh_path)
# Explicitly remove all other nodes to prevent implicit inclusions
for node in mesh_path_list:
if node != mesh_path:
scene_manifest.mesh_group_unselect_node(mesh_group, node)
# Create a LOD rule
lod_rule = scene_manifest.mesh_group_add_lod_rule(mesh_group)
# Loop all the mesh nodes after the first
for x in mesh_path_list[1:]:
# Add a new LOD level
lod = scene_manifest.lod_rule_add_lod(lod_rule)
# Select the current mesh for this LOD level
scene_manifest.lod_select_node(lod, x)
# Unselect every other mesh for this LOD level
for y in mesh_path_list:
if y != x:
scene_manifest.lod_unselect_node(lod, y)
# Create an editor entity
entity_id = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "CreateEditorReadyEntity", mesh_group_name)
# Add an EditorMeshComponent to the entity
editor_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", entity_id, "AZ::Render::EditorMeshComponent")
# Set the ModelAsset assetHint to the relative path of the input asset + the name of the MeshGroup we just created + the azmodel extension
# 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" }}}
});
# 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 for Mesh component")
create_prefab(scene_manifest, source_filename_only, [entity_id])
# Convert the manifest to a JSON string and return it
new_manifest = scene_manifest.export()
return new_manifest
sceneJobHandler = None
def on_update_manifest(args):
try:
scene = args[0]
return update_manifest(scene)
except RuntimeError as err:
print (f'ERROR - {err}')
log_exception_traceback()
except:
log_exception_traceback()
global sceneJobHandler
sceneJobHandler = None
# 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)
except:
sceneJobHandler = None

@ -0,0 +1,95 @@
"""
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
"""
import traceback, logging, json
from typing import Tuple, List
import azlmbr.bus
from scene_api import scene_data as sceneData
from scene_api.scene_data import SceneGraphName
def log_exception_traceback():
"""
Outputs an exception stacktrace.
"""
data = traceback.format_exc()
logger = logging.getLogger('python')
logger.error(data)
def sanitize_name_for_disk(name: str):
"""
Removes illegal filename characters from a string.
:param name: String to clean.
:return: Name with illegal characters removed.
"""
return "".join(char for char in name if char not in "|<>:\"/?*\\")
def get_mesh_node_names(scene_graph: sceneData.SceneGraph) -> Tuple[List[SceneGraphName], List[str]]:
"""
Returns a tuple of all the mesh nodes as well as all the node paths
:param scene_graph: Scene graph to search
:return: Tuple of [Mesh Nodes, All Node Paths]
"""
import azlmbr.scene as sceneApi
import azlmbr.scene.graph
mesh_data_list = []
node = scene_graph.get_root()
children = []
paths = []
while node.IsValid():
# store children to process after siblings
if scene_graph.has_node_child(node):
children.append(scene_graph.get_node_child(node))
node_name = sceneData.SceneGraphName(scene_graph.get_node_name(node))
paths.append(node_name.get_path())
# store any node that has mesh data content
node_content = scene_graph.get_node_content(node)
if node_content.CastWithTypeName('MeshData'):
if scene_graph.is_node_end_point(node) is False:
if len(node_name.get_path()):
mesh_data_list.append(sceneData.SceneGraphName(scene_graph.get_node_name(node)))
# advance to next node
if scene_graph.has_node_sibling(node):
node = scene_graph.get_node_sibling(node)
elif children:
node = children.pop()
else:
node = azlmbr.scene.graph.NodeIndex()
return mesh_data_list, paths
def create_prefab(scene_manifest: sceneData.SceneManifest, prefab_name: str, entities: list) -> None:
prefab_filename = prefab_name + ".prefab"
created_template_id = azlmbr.prefab.PrefabSystemScriptingBus(azlmbr.bus.Broadcast, "CreatePrefab", entities,
prefab_filename)
if created_template_id is None or created_template_id == azlmbr.prefab.InvalidTemplateId:
raise RuntimeError("CreatePrefab {} failed".format(prefab_filename))
# Convert the prefab to a JSON string
output = azlmbr.prefab.PrefabLoaderScriptingBus(azlmbr.bus.Broadcast, "SaveTemplateToString", created_template_id)
if output is not None and output.IsSuccess():
json_string = output.GetValue()
uuid = azlmbr.math.Uuid_CreateRandom().ToString()
json_result = json.loads(json_string)
# Add a PrefabGroup to the manifest and store the JSON on it
scene_manifest.add_prefab_group(prefab_name, uuid, json_result)
else:
raise RuntimeError(
"SaveTemplateToString failed for template id {}, prefab {}".format(created_template_id, prefab_filename))

@ -5,55 +5,16 @@
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
import os, traceback, binascii, sys, json, pathlib
import azlmbr.math
import azlmbr.bus
import azlmbr.math
from scene_helpers import *
#
# SceneAPI Processor
#
def log_exception_traceback():
exc_type, exc_value, exc_tb = sys.exc_info()
data = traceback.format_exception(exc_type, exc_value, exc_tb)
print(str(data))
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 = []
paths = []
while node.IsValid():
# store children to process after siblings
if sceneGraph.has_node_child(node):
children.append(sceneGraph.get_node_child(node))
nodeName = sceneData.SceneGraphName(sceneGraph.get_node_name(node))
paths.append(nodeName.get_path())
# store any node that has mesh data content
nodeContent = sceneGraph.get_node_content(node)
if nodeContent.CastWithTypeName('MeshData'):
if sceneGraph.is_node_end_point(node) is False:
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, paths
def add_material_component(entity_id):
# Create an override AZ::Render::EditorMaterialComponent
editor_material_component = azlmbr.entity.EntityUtilityBus(
@ -64,24 +25,24 @@ def add_material_component(entity_id):
# this fills out the material asset to a known product AZMaterial asset relative path
json_update = json.dumps({
"Controller": { "Configuration": { "materials": [
{
"Key": {},
"Value": { "MaterialAsset":{
"assetHint": "materials/basic_grey.azmaterial"
}}
}]
}}
});
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, editor_material_component, json_update)
"Controller": {"Configuration": {"materials": [
{
"Key": {},
"Value": {"MaterialAsset": {
"assetHint": "materials/basic_grey.azmaterial"
}}
}]
}}
})
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id,
editor_material_component, json_update)
if not result:
raise RuntimeError("UpdateComponentForEntity for editor_material_component failed")
def update_manifest(scene):
import json
import uuid, os
import azlmbr.scene as sceneApi
import azlmbr.scene.graph
from scene_api import scene_data as sceneData
@ -89,9 +50,9 @@ def update_manifest(scene):
# Get a list of all the mesh nodes, as well as all the nodes
mesh_name_list, all_node_paths = get_mesh_node_names(graph)
scene_manifest = sceneData.SceneManifest()
clean_filename = scene.sourceFilename.replace('.', '_')
# Compute the filename of the scene file
source_basepath = scene.watchFolder
source_relative_path = os.path.dirname(os.path.relpath(clean_filename, source_basepath))
@ -108,31 +69,39 @@ def update_manifest(scene):
# Create a unique mesh group name using the filename + node name
mesh_group_name = '{}_{}'.format(source_filename_only, mesh_name.get_name())
# Remove forbidden filename characters from the name since this will become a file on disk later
mesh_group_name = "".join(char for char in mesh_group_name if char not in "|<>:\"/?*\\")
mesh_group_name = sanitize_name_for_disk(mesh_group_name)
# Add the MeshGroup to the manifest and give it a unique ID
mesh_group = scene_manifest.add_mesh_group(mesh_group_name)
mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, source_filename_only + mesh_path)) + '}'
# Set our current node as the only node that is included in this MeshGroup
scene_manifest.mesh_group_select_node(mesh_group, mesh_path)
scene_manifest.mesh_group_add_comment(mesh_group, "Hello World")
# Explicitly remove all other nodes to prevent implicit inclusions
for node in all_node_paths:
if node != mesh_path:
scene_manifest.mesh_group_unselect_node(mesh_group, node)
scene_manifest.mesh_group_add_cloth_rule(mesh_group, mesh_path, "Col0", 1, "Col0", 2, "Col0", 2, 3)
scene_manifest.mesh_group_add_advanced_mesh_rule(mesh_group, True, False, True, "Col0")
scene_manifest.mesh_group_add_skin_rule(mesh_group, 3, 0.002)
scene_manifest.mesh_group_add_tangent_rule(mesh_group, 1, 0)
# Create an editor entity
entity_id = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "CreateEditorReadyEntity", mesh_group_name)
# Add an EditorMeshComponent to the entity
editor_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", entity_id, "AZ::Render::EditorMeshComponent")
# Set the ModelAsset assetHint to the relative path of the input asset + the name of the MeshGroup we just created + the azmodel extension
# 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
editor_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName",
entity_id, "AZ::Render::EditorMeshComponent")
# Set the ModelAsset assetHint to the relative path of the input asset + the name of the MeshGroup we just
# created + the azmodel extension 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)
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id,
editor_mesh_component, json_update)
if not result:
raise RuntimeError("UpdateComponentForEntity failed for Mesh component")
@ -143,17 +112,19 @@ def update_manifest(scene):
add_material_component(entity_id)
# Get the transform component
transform_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", entity_id, "27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0")
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()
});
"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)
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id,
transform_component, transform_json)
if not result:
raise RuntimeError("UpdateComponentForEntity failed for Transform component")
@ -165,37 +136,23 @@ def update_manifest(scene):
created_entities.append(entity_id)
# Create a prefab with all our entities
prefab_filename = source_filename_only + ".prefab"
created_template_id = azlmbr.prefab.PrefabSystemScriptingBus(azlmbr.bus.Broadcast, "CreatePrefab", created_entities, prefab_filename)
if created_template_id == azlmbr.prefab.InvalidTemplateId:
raise RuntimeError("CreatePrefab {} failed".format(prefab_filename))
# Convert the prefab to a JSON string
output = azlmbr.prefab.PrefabLoaderScriptingBus(azlmbr.bus.Broadcast, "SaveTemplateToString", created_template_id)
if output.IsSuccess():
jsonString = output.GetValue()
uuid = azlmbr.math.Uuid_CreateRandom().ToString()
jsonResult = json.loads(jsonString)
# Add a PrefabGroup to the manifest and store the JSON on it
scene_manifest.add_prefab_group(source_filename_only, uuid, jsonResult)
else:
raise RuntimeError("SaveTemplateToString failed for template id {}, prefab {}".format(created_template_id, prefab_filename))
create_prefab(scene_manifest, source_filename_only, created_entities)
# Convert the manifest to a JSON string and return it
new_manifest = scene_manifest.export()
return new_manifest
sceneJobHandler = None
def on_update_manifest(args):
try:
scene = args[0]
return update_manifest(scene)
except RuntimeError as err:
print (f'ERROR - {err}')
print(f'ERROR - {err}')
log_exception_traceback()
except:
log_exception_traceback()
@ -203,10 +160,12 @@ def on_update_manifest(args):
global sceneJobHandler
sceneJobHandler = None
# try to create SceneAPI handler for processing
try:
import azlmbr.scene as sceneApi
if (sceneJobHandler == None):
if sceneJobHandler is None:
sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
sceneJobHandler.connect()
sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)

@ -149,11 +149,14 @@ class AtomComponentProperties:
def display_mapper(property: str = 'name') -> str:
"""
Display Mapper component properties.
- 'LDR color Grading LUT' is the Low Definition Range (LDR) color grading for Look-up Textures (LUT) which is
an Asset.id value corresponding to a lighting asset file.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Display Mapper',
'LDR color Grading LUT': 'Controller|Configuration|LDR color Grading LUT',
}
return properties[property]

@ -5,6 +5,7 @@ For complete copyright and license terms please see the LICENSE at the root of t
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
class Tests:
camera_creation = (
"Camera Entity successfully created",
@ -39,6 +40,9 @@ class Tests:
is_hidden = (
"Entity is hidden",
"Entity was not hidden")
ldr_color_grading_lut = (
"LDR color Grading LUT asset set",
"LDR color Grading LUT asset could not be set")
entity_deleted = (
"Entity deleted",
"Entity was not deleted")
@ -71,16 +75,19 @@ def AtomEditorComponents_DisplayMapper_AddedToEntity():
5) Enter/Exit game mode.
6) Test IsHidden.
7) Test IsVisible.
8) Delete Display Mapper entity.
9) UNDO deletion.
10) REDO deletion.
11) Look for errors and asserts.
8) Set LDR color Grading LUT asset.
9) Delete Display Mapper entity.
10) UNDO deletion.
11) REDO deletion.
12) Look for errors and asserts.
:return: None
"""
import os
import azlmbr.legacy.general as general
from editor_python_test_tools.asset_utils import Asset
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties
@ -97,7 +104,7 @@ def AtomEditorComponents_DisplayMapper_AddedToEntity():
Report.critical_result(Tests.display_mapper_creation, display_mapper_entity.exists())
# 2. Add Display Mapper component to Display Mapper entity.
display_mapper_entity.add_component(AtomComponentProperties.display_mapper())
display_mapper_component = display_mapper_entity.add_component(AtomComponentProperties.display_mapper())
Report.critical_result(
Tests.display_mapper_component,
display_mapper_entity.has_component(AtomComponentProperties.display_mapper()))
@ -140,19 +147,29 @@ def AtomEditorComponents_DisplayMapper_AddedToEntity():
general.idle_wait_frames(1)
Report.result(Tests.is_visible, display_mapper_entity.is_visible() is True)
# 8. Delete Display Mapper entity.
# 8. Set LDR color Grading LUT asset.
display_mapper_asset_path = os.path.join("TestData", "test.lightingpreset.azasset")
display_mapper_asset = Asset.find_asset_by_path(display_mapper_asset_path, False)
display_mapper_component.set_component_property_value(
AtomComponentProperties.display_mapper("LDR color Grading LUT"), display_mapper_asset.id)
Report.result(
Tests.ldr_color_grading_lut,
display_mapper_component.get_component_property_value(
AtomComponentProperties.display_mapper("LDR color Grading LUT")) == display_mapper_asset.id)
# 9. Delete Display Mapper entity.
display_mapper_entity.delete()
Report.result(Tests.entity_deleted, not display_mapper_entity.exists())
# 9. UNDO deletion.
# 10. UNDO deletion.
general.undo()
Report.result(Tests.deletion_undo, display_mapper_entity.exists())
# 10. REDO deletion.
# 11. REDO deletion.
general.redo()
Report.result(Tests.deletion_redo, not display_mapper_entity.exists())
# 11. Look for errors and asserts.
# 12. Look for errors and asserts.
TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")

@ -26,8 +26,8 @@ INSTALL
It is recommended to set up these these tools with O3DE's CMake build commands.
Assuming CMake is already setup on your operating system, below are some sample build commands:
cd /path/to/od3e/
mkdir windows_vs2019
cd windows_vs2019
mkdir windows
cd windows
cmake .. -G "Visual Studio 16 2019" -DLY_PROJECTS=AutomatedTesting
To manually install the project in development mode using your own installed Python interpreter:

@ -459,3 +459,14 @@ class EditorEntity:
"""
new_translation = convert_to_azvector3(new_translation)
azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalTranslation", self.id, new_translation)
# Use this only when prefab system is enabled as it will fail otherwise.
def focus_on_owning_prefab(self) -> None:
"""
Focuses on the owning prefab instance of the given entity.
:param entity: The entity used to fetch the owning prefab to focus on.
"""
assert self.id.isValid(), "A valid entity id is required to focus on its owning prefab."
focus_prefab_result = azlmbr.prefab.PrefabFocusPublicRequestBus(bus.Broadcast, "FocusOnOwningPrefab", self.id)
assert focus_prefab_result.IsSuccess(), f"Prefab operation 'FocusOnOwningPrefab' failed. Error: {focus_prefab_result.GetError()}"

@ -34,6 +34,38 @@ class TestHelper:
# JIRA: SPEC-2880
# general.idle_wait_frames(1)
@staticmethod
def create_level(level_name: str) -> bool:
"""
:param level_name: The name of the level to be created
:return: True if ECreateLevelResult returns 0, False otherwise with logging to report reason
"""
Report.info(f"Creating level {level_name}")
# Use these hardcoded values to pass expected values for old terrain system until new create_level API is
# available
heightmap_resolution = 1024
heightmap_meters_per_pixel = 1
terrain_texture_resolution = 4096
use_terrain = False
result = general.create_level_no_prompt(level_name, heightmap_resolution, heightmap_meters_per_pixel,
terrain_texture_resolution, use_terrain)
# Result codes are ECreateLevelResult defined in CryEdit.h
if result == 1:
Report.info(f"{level_name} level already exists")
elif result == 2:
Report.info("Failed to create directory")
elif result == 3:
Report.info("Directory length is too long")
elif result != 0:
Report.info("Unknown error, failed to create level")
else:
Report.info(f"{level_name} level created successfully")
return result == 0
@staticmethod
def open_level(directory : str, level : str):
# type: (str, str) -> None

@ -61,3 +61,11 @@ class TestAutomation(TestAutomationBase):
def test_CreatePrefab_UnderAnotherPrefab(self, request, workspace, editor, launcher_platform):
from Prefab.tests.create_prefab import CreatePrefab_UnderAnotherPrefab as test_module
self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False)
def test_DeleteEntity_UnderAnotherPrefab(self, request, workspace, editor, launcher_platform):
from Prefab.tests.delete_entity import DeleteEntity_UnderAnotherPrefab as test_module
self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False)
def test_DeleteEntity_UnderLevelPrefab(self, request, workspace, editor, launcher_platform):
from Prefab.tests.delete_entity import DeleteEntity_UnderLevelPrefab as test_module
self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False)

@ -0,0 +1,52 @@
"""
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
"""
def DeleteEntity_UnderAnotherPrefab():
"""
Test description:
- Creates an entity.
- Creates a prefab out of the above entity.
- Focuses on the created prefab and destroys the entity within.
Checks that the entity is correctly destroyed.
"""
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.prefab_utils import Prefab
import Prefab.tests.PrefabTestUtils as prefab_test_utils
prefab_test_utils.open_base_tests_level()
PREFAB_FILE_NAME = 'some_prefab'
# Creates a new entity at the root level
entity = EditorEntity.create_editor_entity()
assert entity.id.IsValid(), "Couldn't create entity."
# Asserts if prefab creation doesn't succeed
child_prefab, child_instance = Prefab.create_prefab([entity], PREFAB_FILE_NAME)
child_entity_ids_inside_prefab = child_instance.get_direct_child_entities()
assert len(
child_entity_ids_inside_prefab) == 1, f"{len(child_entity_ids_inside_prefab)} entities found inside prefab" \
f" when there should have been just 1 entity"
child_entity_inside_prefab = child_entity_ids_inside_prefab[0]
child_entity_inside_prefab.focus_on_owning_prefab()
child_entity_inside_prefab.delete()
# Wait till prefab propagation finishes before validating entity deletion.
azlmbr.legacy.general.idle_wait_frames(1)
child_entity_ids_inside_prefab = child_instance.get_direct_child_entities()
assert len(
child_entity_ids_inside_prefab) == 0, f"{len(child_entity_ids_inside_prefab)} entities found inside prefab" \
f" when there should have been 0 entities"
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(DeleteEntity_UnderAnotherPrefab)

@ -0,0 +1,37 @@
"""
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
"""
def DeleteEntity_UnderLevelPrefab():
"""
Test description:
- Creates an entity.
- Destroys the created entity.
Checks that the entity is correctly destroyed.
"""
from editor_python_test_tools.editor_entity_utils import EditorEntity
import Prefab.tests.PrefabTestUtils as prefab_test_utils
prefab_test_utils.open_base_tests_level()
# Creates a new Entity at the root level
# Asserts if creation didn't succeed
entity = EditorEntity.create_editor_entity_at((100.0, 100.0, 100.0), name = "TestEntity")
assert entity.id.IsValid(), "Couldn't create entity"
level_container_entity = EditorEntity(entity.get_parent_id())
entity.delete()
# Wait till prefab propagation finishes before validating entity deletion.
azlmbr.legacy.general.idle_wait_frames(1)
level_container_child_entities_count = len(level_container_entity.get_children_ids())
assert level_container_child_entities_count == 0, f"The level still has {level_container_child_entities_count}" \
f" children when it should have 0."
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(DeleteEntity_UnderLevelPrefab)

@ -4,7 +4,7 @@ For complete copyright and license terms please see the LICENSE at the root of t
SPDX-License-Identifier: Apache-2.0 OR MIT
A fixture for Setting Up Asset Processor Batch workspace for tests in lmbr_test
A fixture for Setting Up Asset Processor Batch workspace for tests
"""
# Import builtin libraries

@ -4,7 +4,7 @@ For complete copyright and license terms please see the LICENSE at the root of t
SPDX-License-Identifier: Apache-2.0 OR MIT
A fixture for using the Asset Processor in lmbr_test, this will stop the asset processor after every test via
A fixture for using the Asset Processor, this will stop the asset processor after every test via
the teardown. Using the fixture at class level will stop the asset processor after the suite completes.
Using the fixture at test level will stop asset processor after the test completes. Calling this fixture as a test argument will still run the teardown to stop the Asset Processor.
"""

@ -103,6 +103,19 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS)
AZ::AssetBundlerBatch
)
ly_add_pytest(
NAME AssetPipelineTests.BundleMode
PATH ${CMAKE_CURRENT_LIST_DIR}/bundle_mode_tests.py
EXCLUDE_TEST_RUN_TARGET_FROM_IDE
TEST_SERIAL
TEST_SUITE periodic
RUNTIME_DEPENDENCIES
AZ::AssetProcessor
AZ::AssetBundlerBatch
Legacy::Editor
AutomatedTesting.Assets
)
ly_add_pytest(
NAME AssetPipelineTests.AssetBuilder
PATH ${CMAKE_CURRENT_LIST_DIR}/asset_builder_tests.py

@ -0,0 +1,20 @@
"""
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
"""
import azlmbr.bus
import azlmbr.editor
import azlmbr.legacy.general
import sys
# Print out the passed in bundle_path, so the outer test can verify this was sent in correctly
bundle_path = sys.argv[1]
print('Bundle mode test running with path {}'.format(sys.argv[1]))
# Turn on bundle mode. This will trigger some printouts that the outer test logic will validate.
azlmbr.legacy.general.set_cvar_integer("sys_report_files_not_found_in_paks", 1)
azlmbr.legacy.general.run_console(f"loadbundles {bundle_path}")
azlmbr.editor.EditorToolsApplicationRequestBus(azlmbr.bus.Broadcast, 'ExitNoPrompt')

@ -0,0 +1,93 @@
"""
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
"""
import os
import pytest
import logging
import sys
import time
pytest.importorskip('ly_test_tools')
import ly_test_tools.environment.file_system as fs
import ly_test_tools.environment.waiter as waiter
import ly_test_tools.log.log_monitor
from ..ap_fixtures.asset_processor_fixture import asset_processor as asset_processor
from ..ap_fixtures.bundler_batch_setup_fixture import bundler_batch_setup_fixture as bundler_batch_helper
from ..ap_fixtures.timeout_option_fixture import timeout_option_fixture as timeout
@pytest.mark.SUITE_periodic
@pytest.mark.parametrize('launcher_platform', ['windows_editor'])
@pytest.mark.parametrize('project', ['AutomatedTesting'])
@pytest.mark.parametrize('level', ['auto_test'])
class TestBundleMode(object):
def test_bundle_mode_with_levels_mounts_bundles_correctly(self, request, editor, level, launcher_platform,
asset_processor, workspace, bundler_batch_helper):
level_pak = os.path.join("levels", level, "level.pak")
bundles_folder = os.path.join(workspace.paths.project(), "Bundles")
bundle_request_path = os.path.join(bundles_folder, "bundle.pak")
bundle_result_path = os.path.join(bundles_folder,
bundler_batch_helper.platform_file_name(
"bundle.pak", workspace.asset_processor_platform))
# Create target 'Bundles' folder if it doesn't exist
if not os.path.exists(bundles_folder):
os.mkdir(bundles_folder)
# Delete target bundle file if it already exists
if os.path.exists(bundle_result_path):
fs.delete([bundle_result_path], True, False)
# Make asset list file to use in the bundle
bundler_batch_helper.call_assetLists(
addSeed=level_pak,
assetListFile=bundler_batch_helper["asset_info_file_request"],
)
# Make bundle in <project_folder>/Bundles
bundler_batch_helper.call_bundles(
assetListFile=bundler_batch_helper["asset_info_file_result"],
outputBundlePath=bundle_request_path,
maxSize="2048",
)
# Ensure the bundle was created
assert os.path.exists(bundle_result_path), f"Bundle was not created at location: {bundle_result_path}"
# The editor flips the slash direction in some of the printouts
bundle_result_path_editor_separator = bundle_result_path.replace('\\', '/')
expected_lines = [
# A beginning of test printout can help debug where failures occur, if this line is missing
# then the Editor didn't launch, didn't run the Python test, or didn't pass in the right parameter
f'Bundle mode test running with path {bundles_folder}',
# These printouts happen in response to the loadbundles call, and verify this bundle is actually loaded
f"[CONSOLE] Executing console command 'loadbundles {bundles_folder}'",
f'(BundlingSystem) - Loading bundles from {bundles_folder} of type .pak',
f'(Archive) - Opening archive file {bundle_result_path_editor_separator}',
]
unexpected_lines = []
timeout = 180
halt_on_unexpected = False
test_directory = os.path.join(os.path.dirname(__file__))
test_file = os.path.join(test_directory, 'bundle_mode_in_editor_tests.py')
editor.args.extend(['-NullRenderer', '-rhi=Null', "--skipWelcomeScreenDialog",
"--autotest_mode", "--runpythontest", test_file, "--runpythonargs", bundles_folder])
with editor.start(launch_ap=True):
editor_log_file = os.path.join(editor.workspace.paths.project_log(), 'Editor.log')
log_monitor = ly_test_tools.log.log_monitor.LogMonitor(editor, editor_log_file)
waiter.wait_for(
lambda: editor.is_alive(),
timeout,
exc=("Log file '{}' was never opened by another process.".format(editor_log_file)),
interval=1)
log_monitor.monitor_log_for_lines(expected_lines, unexpected_lines, halt_on_unexpected, timeout)
# Delete the bundle created and used in this test
fs.delete([bundle_result_path], True, False)

@ -7,60 +7,6 @@
#
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_FOUNDATION_TEST_SUPPORTED)
ly_add_pytest(
NAME AutomatedTesting::EditorTests_Main
TEST_SUITE main
TEST_SERIAL
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main.py
PYTEST_MARKS "not REQUIRES_gpu"
RUNTIME_DEPENDENCIES
Legacy::Editor
AZ::AssetProcessor
AutomatedTesting.Assets
COMPONENT
Editor
)
ly_add_pytest(
NAME AutomatedTesting::EditorTests_Main_GPU
TEST_SUITE main
TEST_SERIAL
TEST_REQUIRES gpu
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main.py
PYTEST_MARKS "REQUIRES_gpu"
RUNTIME_DEPENDENCIES
Legacy::Editor
AZ::AssetProcessor
AutomatedTesting.Assets
COMPONENT
Editor
)
ly_add_pytest(
NAME AutomatedTesting::EditorTests_Periodic
TEST_SUITE periodic
TEST_SERIAL
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Periodic.py
RUNTIME_DEPENDENCIES
Legacy::Editor
AZ::AssetProcessor
AutomatedTesting.Assets
COMPONENT
Editor
)
ly_add_pytest(
NAME AutomatedTesting::EditorTests_Sandbox
TEST_SUITE sandbox
TEST_SERIAL
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Sandbox.py
RUNTIME_DEPENDENCIES
Legacy::Editor
AZ::AssetProcessor
AutomatedTesting.Assets
COMPONENT
Editor
)
ly_add_pytest(
NAME AutomatedTesting::EditorTests_Main_Optimized

@ -49,7 +49,6 @@ class TestAutomationNoAutoTestMode(EditorTestSuite):
from .EditorScripts import AssetPicker_UI_UX as test_module
@pytest.mark.xfail(reason="Optimized tests are experimental, we will enable xfail and monitor them temporarily.")
@pytest.mark.SUITE_main
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("project", ["AutomatedTesting"])

@ -11,24 +11,10 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_
## DynVeg ##
ly_add_pytest(
NAME AutomatedTesting::DynamicVegetationTests_Main
NAME AutomatedTesting::DynamicVegetationTests_Main_Optimized
TEST_SERIAL
TEST_SUITE main
PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg/TestSuite_Main.py
RUNTIME_DEPENDENCIES
AZ::AssetProcessor
Legacy::Editor
AutomatedTesting.GameLauncher
AutomatedTesting.Assets
COMPONENT
LargeWorlds
)
ly_add_pytest(
NAME AutomatedTesting::DynamicVegetationTests_Periodic
TEST_SERIAL
TEST_SUITE periodic
PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg/TestSuite_Periodic.py
PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg/TestSuite_Main_Optimized.py
RUNTIME_DEPENDENCIES
AZ::AssetProcessor
Legacy::Editor
@ -37,7 +23,6 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_
COMPONENT
LargeWorlds
)
ly_add_pytest(
NAME AutomatedTesting::DynamicVegetationTests_Periodic_Optimized
TEST_SERIAL
@ -52,20 +37,6 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_
LargeWorlds
)
ly_add_pytest(
NAME AutomatedTesting::DynamicVegetationTests_Main_Optimized
TEST_SERIAL
TEST_SUITE main
PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg/TestSuite_Main_Optimized.py
RUNTIME_DEPENDENCIES
AZ::AssetProcessor
Legacy::Editor
AutomatedTesting.Assets
AutomatedTesting.GameLauncher
COMPONENT
LargeWorlds
)
## LandscapeCanvas ##
ly_add_pytest(

@ -72,9 +72,9 @@ def DynamicSliceInstanceSpawner_Embedded_E2E():
# 1) Create a new, temporary level
lvl_name = "tmp_level"
helper.init_idle()
level_created = general.create_level_no_prompt(lvl_name, 1024, 1, 4096, False)
level_created = helper.create_level(lvl_name)
general.idle_wait(1.0)
Report.critical_result(Tests.level_created, level_created == 0)
Report.critical_result(Tests.level_created, level_created)
general.set_current_view_position(512.0, 480.0, 38.0)
# 2) Create a new entity with required vegetation area components and Script Canvas component for launcher test

@ -73,9 +73,9 @@ def DynamicSliceInstanceSpawner_External_E2E():
# 1) Create a new, temporary level
lvl_name = "tmp_level"
helper.init_idle()
level_created = general.create_level_no_prompt(lvl_name, 1024, 1, 4096, False)
level_created = helper.create_level(lvl_name)
general.idle_wait(1.0)
Report.critical_result(Tests.level_created, level_created == 0)
Report.critical_result(Tests.level_created, level_created)
general.set_current_view_position(512.0, 480.0, 38.0)
# 2) Create a new entity with required vegetation area components and switch the Vegetation Asset List Source

@ -76,9 +76,9 @@ def LayerBlender_E2E_Editor():
# 1) Create a new, temporary level
lvl_name = "tmp_level"
helper.init_idle()
level_created = general.create_level_no_prompt(lvl_name, 1024, 1, 4096, False)
level_created = helper.create_level(lvl_name)
general.idle_wait(1.0)
Report.critical_result(Tests.level_created, level_created == 0)
Report.critical_result(Tests.level_created, level_created)
general.set_current_view_position(500.49, 498.69, 46.66)
general.set_current_view_rotation(-42.05, 0.00, -36.33)

@ -12,13 +12,23 @@ import ly_test_tools.environment.file_system as file_system
from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, EditorParallelTest, EditorTestSuite
@pytest.mark.xfail(reason="Optimized tests are experimental, we will enable xfail and monitor them temporarily.")
@pytest.mark.SUITE_main
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("project", ["AutomatedTesting"])
class TestAutomation(EditorTestSuite):
enable_prefab_system = False
# Helpers for test asset cleanup
def cleanup_test_level(self, workspace):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")],
True, True)
def cleanup_test_slices(self, workspace):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "slices",
"TestSlice_1.slice")], True, True)
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "slices",
"TestSlice_2.slice")], True, True)
class test_DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks(EditorParallelTest):
from .EditorScripts import DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks as test_module
@ -38,10 +48,7 @@ class TestAutomation(EditorTestSuite):
class test_SpawnerSlices_SliceCreationAndVisibilityToggleWorks(EditorSingleTest):
# Custom teardown to remove slice asset created during test
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "slices",
"TestSlice_1.slice")], True, True)
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "slices",
"TestSlice_2.slice")], True, True)
TestAutomation.cleanup_test_slices(self, workspace)
from .EditorScripts import SpawnerSlices_SliceCreationAndVisibilityToggleWorks as test_module
class test_AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea(EditorParallelTest):
@ -152,23 +159,29 @@ class TestAutomation(EditorTestSuite):
class test_DynamicSliceInstanceSpawner_Embedded_E2E_Editor(EditorSingleTest):
from .EditorScripts import DynamicSliceInstanceSpawner_Embedded_E2E as test_module
# Custom teardown to remove test level created during test
# Custom setup/teardown to remove test level created during test
def setup(self, request, workspace, editor, editor_test_results, launcher_platform):
TestAutomation.cleanup_test_level(self, workspace)
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")],
True, True)
TestAutomation.cleanup_test_level(self, workspace)
class test_DynamicSliceInstanceSpawner_External_E2E_Editor(EditorSingleTest):
from .EditorScripts import DynamicSliceInstanceSpawner_External_E2E as test_module
# Custom teardown to remove test level created during test
# Custom setup/teardown to remove test level created during test
def setup(self, request, workspace, editor, editor_test_results, launcher_platform):
TestAutomation.cleanup_test_level(self, workspace)
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")],
True, True)
TestAutomation.cleanup_test_level(self, workspace)
class test_LayerBlender_E2E_Editor(EditorSingleTest):
from .EditorScripts import LayerBlender_E2E_Editor as test_module
# Custom teardown to remove test level created during test
# Custom setup/teardown to remove test level created during test
def setup(self, request, workspace, editor, editor_test_results, launcher_platform):
TestAutomation.cleanup_test_level(self, workspace)
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")],
True, True)
TestAutomation.cleanup_test_level(self, workspace)

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e169277bca473325281d5fe043cffc9196bd3ef46f6bffbea6e0b5e3b7194a1
size 62700

@ -0,0 +1,8 @@
{
"values": [
{
"$type": "ScriptProcessorRule",
"scriptFilename": "Editor/Scripts/auto_lod.py"
}
]
}

@ -472,7 +472,7 @@ void EditorViewportWidget::Update()
{
auto start = std::chrono::steady_clock::now();
m_entityVisibilityQuery.UpdateVisibility(GetCameraState());
m_entityVisibilityQuery.UpdateVisibility(m_renderViewport->GetCameraState());
if (ed_visibility_logTiming)
{
@ -714,7 +714,7 @@ void EditorViewportWidget::RenderAll()
m_debugDisplay->DepthTestOff();
m_manipulatorManager->DrawManipulators(
*m_debugDisplay, GetCameraState(),
*m_debugDisplay, m_renderViewport->GetCameraState(),
BuildMouseInteractionInternal(
AztfVi::MouseButtons(AztfVi::TranslateMouseButtons(QGuiApplication::mouseButtons())), keyboardModifiers,
BuildMousePick(WidgetToViewport(mapFromGlobal(QCursor::pos())))));
@ -878,31 +878,11 @@ void EditorViewportWidget::OnMenuSelectCurrentCamera()
}
}
AzFramework::CameraState EditorViewportWidget::GetCameraState()
{
return m_renderViewport->GetCameraState();
}
AZ::Vector3 EditorViewportWidget::PickTerrain(const AzFramework::ScreenPoint& point)
{
return LYVec3ToAZVec3(ViewToWorld(AzToolsFramework::ViewportInteraction::QPointFromScreenPoint(point), nullptr, true));
}
float EditorViewportWidget::TerrainHeight(const AZ::Vector2& position)
{
return GetIEditor()->GetTerrainElevation(position.GetX(), position.GetY());
}
void EditorViewportWidget::FindVisibleEntities(AZStd::vector<AZ::EntityId>& visibleEntitiesOut)
{
visibleEntitiesOut.assign(m_entityVisibilityQuery.Begin(), m_entityVisibilityQuery.End());
}
AzFramework::ScreenPoint EditorViewportWidget::ViewportWorldToScreen(const AZ::Vector3& worldPosition)
{
return m_renderViewport->ViewportWorldToScreen(worldPosition);
}
QWidget* EditorViewportWidget::GetWidgetForViewportContextMenu()
{
return this;
@ -2132,7 +2112,7 @@ bool EditorViewportWidget::GetActiveCameraState(AzFramework::CameraState& camera
{
if (m_pPrimaryViewport == this)
{
cameraState = GetCameraState();
cameraState = m_renderViewport->GetCameraState();
return true;
}

@ -205,8 +205,6 @@ private:
void* GetSystemCursorConstraintWindow() const override;
// AzToolsFramework::MainEditorViewportInteractionRequestBus overrides ...
AZ::Vector3 PickTerrain(const AzFramework::ScreenPoint& point) override;
float TerrainHeight(const AZ::Vector2& position) override;
bool ShowingWorldSpace() override;
QWidget* GetWidgetForViewportContextMenu() override;
@ -293,9 +291,6 @@ private:
// This switches the active camera to the next one in the list of (default, all custom cams).
void CycleCamera();
AzFramework::CameraState GetCameraState();
AzFramework::ScreenPoint ViewportWorldToScreen(const AZ::Vector3& worldPosition);
QPoint WidgetToViewport(const QPoint& point) const;
QPoint ViewportToWidget(const QPoint& point) const;
QSize WidgetToViewport(const QSize& size) const;

@ -1396,7 +1396,7 @@ void SandboxIntegrationManager::ContextMenu_NewEntity()
{
worldPosition = AzToolsFramework::FindClosestPickIntersection(
view->GetViewportId(), AzFramework::ScreenPointFromVector2(m_contextMenuViewPoint), AzToolsFramework::EditorPickRayLength,
GetDefaultEntityPlacementDistance());
AzToolsFramework::GetDefaultEntityPlacementDistance());
}
CreateNewEntityAtPosition(worldPosition);

@ -75,17 +75,16 @@ void CImageEx::ReverseUpDown()
}
uint32* pPixData = GetData();
uint32* pReversePix = new uint32[GetWidth() * GetHeight()];
for (int i = GetHeight() - 1, i2 = 0; i >= 0; i--, i2++)
const int height = GetHeight();
const int width = GetWidth();
for (int i = 0; i < height / 2; i++)
{
for (int k = 0; k < GetWidth(); k++)
for (int j = 0; j < width; j++)
{
pReversePix[i2 * GetWidth() + k] = pPixData[i * GetWidth() + k];
AZStd::swap(pPixData[i * width + j], pPixData[(height - 1 - i) * width + j]);
}
}
Attach(pReversePix, GetWidth(), GetHeight());
}
void CImageEx::FillAlpha(unsigned char value)

@ -14,14 +14,11 @@
// Qt
#include <QPainter>
// AzCore
#include <AzCore/Console/IConsole.h>
// AzQtComponents
#include <AzQtComponents/DragAndDrop/ViewportDragAndDrop.h>
// AzToolsFramework
#include <AzToolsFramework/API/ComponentEntitySelectionBus.h>
#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
@ -41,19 +38,6 @@
#undef LoadCursor
#endif
AZ_CVAR(
float,
ed_defaultEntityPlacementDistance,
10.0f,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"The default distance to place an entity from the camera if no intersection is found");
float GetDefaultEntityPlacementDistance()
{
return ed_defaultEntityPlacementDistance;
}
//////////////////////////////////////////////////////////////////////
// Viewport drag and drop support
//////////////////////////////////////////////////////////////////////
@ -63,7 +47,7 @@ void QtViewport::BuildDragDropContext(
{
context.m_hitLocation = AzToolsFramework::FindClosestPickIntersection(
viewportId, AzToolsFramework::ViewportInteraction::ScreenPointFromQPoint(point), AzToolsFramework::EditorPickRayLength,
GetDefaultEntityPlacementDistance());
AzToolsFramework::GetDefaultEntityPlacementDistance());
}
void QtViewport::dragEnterEvent(QDragEnterEvent* event)

@ -87,9 +87,6 @@ enum EStdCursor
STD_CURSOR_LAST,
};
//! The default distance an entity is placed from the camera if there is no intersection
SANDBOX_API float GetDefaultEntityPlacementDistance();
AZ_PUSH_DISABLE_DLL_EXPORT_BASECLASS_WARNING
class SANDBOX_API CViewport
: public IDisplayViewport

@ -325,13 +325,13 @@ namespace AZ
T& operator*() const
{
AZ_Assert(m_assetData, "Asset is not loaded");
AZ_Assert(m_assetData, "Asset %s (%s) is not loaded", m_assetId.ToString<AZStd::string>().c_str(), m_assetHint.c_str());
return *Get();
}
T* operator->() const
{
AZ_Assert(m_assetData, "Asset is not loaded");
AZ_Assert(m_assetData, "Asset %s (%s) is not loaded", m_assetId.ToString<AZStd::string>().c_str(), m_assetHint.c_str());
return Get();
}

@ -32,7 +32,16 @@ namespace AZ
template <typename BASE_TYPE, ThreadSafety THREAD_SAFETY>
inline void ConsoleDataWrapper<BASE_TYPE, THREAD_SAFETY>::operator =(const BASE_TYPE& rhs)
{
const BASE_TYPE currentValue = this->m_value;
// Do the value assignment outside new value check.
// Client code can supply a type for m_value that overrides the operator= function and trigger side effects
// in the operator= function body. Doing the assignment outside the value change check avoids those side
// effects not being triggered because AzCore believes the value wouldn't change.
this->m_value = rhs;
if (currentValue != rhs)
{
InvokeCallback();
}
}
template <typename BASE_TYPE, ThreadSafety THREAD_SAFETY>

@ -78,7 +78,8 @@ namespace AZ::IO
// native format observers
//! Returns string_view stored within the PathView
constexpr AZStd::string_view Native() const noexcept;
constexpr const AZStd::string_view& Native() const noexcept;
constexpr AZStd::string_view& Native() noexcept;
//! Conversion operator to retrieve string_view stored within the PathView
constexpr explicit operator AZStd::string_view() const noexcept;

@ -101,7 +101,11 @@ namespace AZ::IO
}
// native format observers
constexpr auto PathView::Native() const noexcept -> AZStd::string_view
constexpr auto PathView::Native() const noexcept -> const AZStd::string_view&
{
return m_path;
}
constexpr auto PathView::Native() noexcept -> AZStd::string_view&
{
return m_path;
}

@ -16,7 +16,7 @@
//
// When AZ_CRC("My string") is used by default it will map to AZ::Crc32("My string").
// We do have a pro-processor program which will precompute the crc for you and
// transform that macro to AZ_CRC("My string",0xabcdef00) this will expand to just 0xabcdef00.
// transform that macro to AZ_CRC("My string", 0x18fbd270) this will expand to just 0x18fbd270.
// This will remove completely the "My string" from your executable, it will add it to a database and so on.
// WHen you want to update the string, just change the string.
// If you don't run the precompile step the code should still run fine, except it will be slower,
@ -24,7 +24,7 @@
// a constant expression.
// For example
// switch(id) {
// case AZ_CRC("My string",0xabcdef00): {} break; // this will compile fine
// case AZ_CRC("My string",0x18fbd270): {} break; // this will compile fine
// case AZ_CRC("My string"): {} break; // this will cause "error C2051: case expression not constant"
// }
// So it's you choice what you do, depending on your needs.

@ -9,6 +9,7 @@
#include <AzCore/Console/IConsole.h>
#include <AzCore/Console/ConsoleFunctor.h>
#include <AzCore/IO/ByteContainerStream.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Settings/SettingsRegistryConsoleUtils.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/StringFunc/StringFunc.h>
@ -36,7 +37,7 @@ namespace AZ::SettingsRegistryConsoleUtils
combinedKeyValueCommand.c_str());
AZ::Debug::Trace::Output("SettingsRegistry", setOutput.c_str());
}
};
}
static void ConsoleRemoveSettingsRegistryValue(SettingsRegistryInterface& settingsRegistry, const ConsoleCommandContainer& commandArgs)
{
@ -57,7 +58,7 @@ namespace AZ::SettingsRegistryConsoleUtils
AZ::Debug::Trace::Output("SettingsRegistry", removeOutput.c_str());
}
}
};
}
static void ConsoleDumpSettingsRegistryValue(SettingsRegistryInterface& settingsRegistry, const ConsoleCommandContainer& commandArgs)
{
@ -88,13 +89,39 @@ namespace AZ::SettingsRegistryConsoleUtils
}
AZ::Debug::Trace::Output("SettingsRegistry", outputString.c_str());
};
}
static void ConsoleDumpAllSettingsRegistryValues(SettingsRegistryInterface& settingsRegistry,
[[maybe_unused]] const ConsoleCommandContainer& commandArgs)
{
ConsoleDumpSettingsRegistryValue(settingsRegistry, { "" });
};
}
static void ConsoleMergeFileToSettingsRegistry(SettingsRegistryInterface& settingsRegistry, const ConsoleCommandContainer& commandArgs)
{
if (commandArgs.empty())
{
AZ_Error("SettingsRegistryConsoleUtils", false, "Command %s requires a <file path> argument to locate json file to merge",
SettingsRegistryMergeFile);
return;
}
auto commandArgumentsIter = commandArgs.begin();
// Extract the JSON pointer path from the argument list
AZStd::string_view filePath{ *commandArgumentsIter++ };
AZ::SettingsRegistryInterface::FixedValueString jsonAnchorPath;
AZ::StringFunc::Join(jsonAnchorPath, commandArgumentsIter, commandArgs.end(), ' ');
const auto mergeFormat = AZ::IO::PathView(filePath).Extension() != ".setregpatch" ? AZ::SettingsRegistryInterface::Format::JsonMergePatch : AZ::SettingsRegistryInterface::Format::JsonPatch;
if (settingsRegistry.MergeSettingsFile(filePath, mergeFormat, jsonAnchorPath))
{
const auto mergeFileOutput = AZ::SettingsRegistryInterface::FixedValueString::format(
R"(Merged json file "%*.s" anchored to json path "%s" into the global settings registry)" "\n",
AZ_STRING_ARG(filePath), jsonAnchorPath.c_str());
AZ::Debug::Trace::Output("SettingsRegistry", mergeFileOutput.c_str());
}
}
[[nodiscard]] ConsoleFunctorHandle RegisterAzConsoleCommands(SettingsRegistryInterface& registry, AZ::IConsole& azConsole)
{
@ -115,6 +142,11 @@ namespace AZ::SettingsRegistryConsoleUtils
resultHandle.m_consoleFunctors.emplace_back(azConsole, SettingsRegistryDumpAll,
R"(Dumps all values from the global settings registry)" "\n",
ConsoleFunctorFlags::Null, AZ::TypeId::CreateNull(), registry, &ConsoleDumpAllSettingsRegistryValues);
resultHandle.m_consoleFunctors.emplace_back(azConsole, SettingsRegistryMergeFile,
R"(Merges File into the global settings registry)" "\n"
R"(@param file-path - path to JSON formatted file to merge)" "\n"
R"(@param anchor-path - JSON path to anchor merge operation. Defaults to "")" "\n",
ConsoleFunctorFlags::Null, AZ::TypeId::CreateNull(), registry, &ConsoleMergeFileToSettingsRegistry);
return resultHandle;
}

@ -14,15 +14,16 @@
namespace AZ::SettingsRegistryConsoleUtils
{
//! Only 4 console command are registered for the settings registry
//! "regset", "regremove", "regdump", "regdumpall"
//! The following console command are registered for the settings registry
//! "regset", "regremove", "regdump", "regdumpall", "regset-file"
//! The value should be increased if more commands are needed
inline constexpr size_t MaxSettingsRegistryConsoleFunctors = 4;
inline constexpr size_t MaxSettingsRegistryConsoleFunctors = 5;
inline constexpr const char* SettingsRegistrySet = "sr_regset";
inline constexpr const char* SettingsRegistryRemove = "sr_regremove";
inline constexpr const char* SettingsRegistryDump = "sr_regdump";
inline constexpr const char* SettingsRegistryDumpAll = "sr_regdumpall";
inline constexpr const char* SettingsRegistryMergeFile = "sr_regset_file";
// RAII structure which owns the instances of the Settings Registry Console commands
// registered with an AZ Console
@ -51,6 +52,10 @@ namespace AZ::SettingsRegistryConsoleUtils
//!
//! "sr_regdumpall" accepts 0 arguments and dumps the entire settings registry
//! NOTE: this might result in a large amount of output to the console
//!
//! "sr_regset_file" accepts 1 or 2 arguments - <file-path> [<anchor json path>]
//! Merges the json formatted file <file path> into the settings registry underneath the root anchor ""
//! or <anchor json path> if supplied
[[nodiscard]] ConsoleFunctorHandle RegisterAzConsoleCommands(SettingsRegistryInterface& registry, AZ::IConsole& azConsole);
}

@ -19,9 +19,6 @@
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/Settings/CommandLine.h>
#include <AzCore/std/string/conversions.h>
#include <AzCore/std/string/wildcard.h>
#include <AzCore/std/tuple.h>
#include <AzCore/std/containers/variant.h>
#include <AzCore/Utils/Utils.h>
#include <cinttypes>
@ -983,7 +980,7 @@ namespace AZ::SettingsRegistryMergeUtils
// code in the loop makes calls that mutates the `commandLine` instance, invalidating the iterators. Making a copy
// ensures that the iterators remain valid.
// NOLINTNEXTLINE(performance-unnecessary-value-param)
void MergeSettingsToRegistry_CommandLine(SettingsRegistryInterface& registry, AZ::CommandLine commandLine, bool executeCommands)
void MergeSettingsToRegistry_CommandLine(SettingsRegistryInterface& registry, AZ::CommandLine commandLine, bool executeRegdumpCommands)
{
// Iterate over all the command line options in order to parse the --regset and --regremove
// arguments in the order they were supplied
@ -998,18 +995,44 @@ namespace AZ::SettingsRegistryMergeUtils
continue;
}
}
else if (commandArgument.m_option == "regset-file")
{
AZStd::string_view fileArg(commandArgument.m_value);
AZStd::string_view jsonAnchorPath;
// double colons is treated as the separator for an anchor path
// single colon cannot be used as it is used in Windows paths
if (auto anchorPathIndex = AZ::StringFunc::Find(fileArg, "::");
anchorPathIndex != AZStd::string_view::npos)
{
jsonAnchorPath = fileArg.substr(anchorPathIndex + 2);
fileArg = fileArg.substr(0, anchorPathIndex);
}
if (!fileArg.empty())
{
AZ::IO::PathView filePath(fileArg);
const auto mergeFormat = filePath.Extension() != ".setregpatch"
? AZ::SettingsRegistryInterface::Format::JsonMergePatch
: AZ::SettingsRegistryInterface::Format::JsonPatch;
if (!registry.MergeSettingsFile(filePath.Native(), mergeFormat, jsonAnchorPath))
{
AZ_Warning("SettingsRegistryMergeUtils", false, R"(Merging of file "%.*s" to the Settings Registry has failed at anchor "%.*s".)",
AZ_STRING_ARG(filePath.Native()), AZ_STRING_ARG(jsonAnchorPath));
continue;
}
}
}
else if (commandArgument.m_option == "regremove")
{
if (!registry.Remove(commandArgument.m_value))
{
AZ_Warning("SettingsRegistryMergeUtils", false, "Unable to remove value at JSON Pointer %s for --regremove.",
commandArgument.m_value.data());
commandArgument.m_value.c_str());
continue;
}
}
}
if (executeCommands)
if (executeRegdumpCommands)
{
constexpr bool prettifyOutput = true;
const size_t regdumpSwitchValues = commandLine.GetNumSwitchValues("regdump");

@ -157,6 +157,22 @@ namespace AZ::Statistics
}
}
void GetAllStatistics(AZStd::vector<NamedRunningStatistic*>& stats)
{
for (auto& iter : m_profilers)
{
iter.second.m_profiler.GetStatsManager().GetAllStatistics(stats);
}
}
void GetAllStatisticsOfUnits(AZStd::vector<NamedRunningStatistic*>& stats, const char* units)
{
for (auto& iter : m_profilers)
{
iter.second.m_profiler.GetStatsManager().GetAllStatisticsOfUnits(stats, units);
}
}
private:
struct ProfilerInfo
{

@ -56,13 +56,25 @@ namespace AZ
void GetAllStatistics(AZStd::vector<NamedRunningStatistic*>& vector)
{
for (auto const& it : m_statistics)
for (const auto& it : m_statistics)
{
NamedRunningStatistic* stat = it.second;
vector.push_back(stat);
}
}
void GetAllStatisticsOfUnits(AZStd::vector<NamedRunningStatistic*>& vector, const char* units)
{
for (const auto& it : m_statistics)
{
NamedRunningStatistic* stat = it.second;
if (stat->GetUnits() == units)
{
vector.push_back(stat);
}
}
}
//! Helper method to apply units to statistics with empty units string.
AZ::u32 ApplyUnits(const AZStd::string& units)
{

@ -26,6 +26,7 @@
#include <AzCore/std/functional.h>
#include <AzCore/std/parallel/condition_variable.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/UnitTest/Mocks/MockFileIOBase.h>
#include <AZTestShared/Utils/Utils.h>
#include <Streamer/IStreamerMock.h>
#include <Tests/Asset/BaseAssetManagerTest.h>
@ -131,8 +132,8 @@ namespace UnitTest
* This will test the aspect of the system where ObjectStreams and asset jobs loading dependent
* assets will do the work in their own thread.
*/
class AssetJobsFloodTest
: public BaseAssetManagerTest
class AssetJobsFloodTest : public DisklessAssetManagerBase
{
public:
TestAssetManager* m_testAssetManager{ nullptr };
@ -183,15 +184,14 @@ namespace UnitTest
void SetUp() override
{
BaseAssetManagerTest::SetUp();
DisklessAssetManagerBase::SetUp();
SetupTest();
}
void TearDown() override
{
TearDownTest();
AssetManager::Destroy();
BaseAssetManagerTest::TearDown();
DisklessAssetManagerBase::TearDown();
}
void SetupAssets()
@ -257,9 +257,9 @@ namespace UnitTest
AssetWithSerializedData ap2;
AssetWithSerializedData ap3;
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset4.txt", AZ::DataStream::ST_XML, &ap1, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset5.txt", AZ::DataStream::ST_XML, &ap2, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset6.txt", AZ::DataStream::ST_XML, &ap3, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset4.txt", &ap1, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset5.txt", &ap2, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset6.txt", &ap3, m_serializeContext));
AssetWithAssetReference assetWithPreload1;
AssetWithAssetReference assetWithPreload2;
@ -273,11 +273,11 @@ namespace UnitTest
noLoadAsset.m_asset = m_testAssetManager->CreateAsset<AssetWithSerializedData>(MyAsset2Id, AssetLoadBehavior::NoLoad);
EXPECT_EQ(m_assetHandlerAndCatalog->m_numCreations, 4);
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset1.txt", AZ::DataStream::ST_XML, &assetWithPreload1, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset2.txt", AZ::DataStream::ST_XML, &assetWithPreload2, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset3.txt", AZ::DataStream::ST_XML, &assetWithPreload3, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "DelayLoadAsset.txt", AZ::DataStream::ST_XML, &delayedAsset, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "NoLoadAsset.txt", AZ::DataStream::ST_XML, &noLoadAsset, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset1.txt", &assetWithPreload1, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset2.txt", &assetWithPreload2, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset3.txt", &assetWithPreload3, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("DelayLoadAsset.txt", &delayedAsset, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("NoLoadAsset.txt", &noLoadAsset, m_serializeContext));
AssetWithQueueAndPreLoadReferences preLoadRoot;
AssetWithQueueAndPreLoadReferences preLoadA;
@ -297,16 +297,16 @@ namespace UnitTest
preLoadBrokenA.m_preLoad = m_testAssetManager->CreateAsset<AssetWithAssetReference>(PreloadBrokenDepBId, AssetLoadBehavior::PreLoad);
preLoadBrokenB.m_preLoad = m_testAssetManager->CreateAsset<AssetWithAssetReference>(PreloadAssetNoDataId, AssetLoadBehavior::PreLoad);
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "PreLoadRoot.txt", AZ::DataStream::ST_XML, &preLoadRoot, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "PreLoadA.txt", AZ::DataStream::ST_XML, &preLoadA, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "PreLoadB.txt", AZ::DataStream::ST_XML, &noRefs, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "PreLoadC.txt", AZ::DataStream::ST_XML, &noRefs, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "QueueLoadA.txt", AZ::DataStream::ST_XML, &queueLoadA, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "QueueLoadB.txt", AZ::DataStream::ST_XML, &noRefs, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "QueueLoadC.txt", AZ::DataStream::ST_XML, &noRefs, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "PreLoadBrokenA.txt", AZ::DataStream::ST_XML, &preLoadBrokenA, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "PreLoadBrokenB.txt", AZ::DataStream::ST_XML, &preLoadBrokenB, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "PreLoadNoData.txt", AZ::DataStream::ST_XML, &noRefs, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("PreLoadRoot.txt", &preLoadRoot, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("PreLoadA.txt", &preLoadA, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("PreLoadB.txt", &noRefs, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("PreLoadC.txt", &noRefs, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("QueueLoadA.txt", &queueLoadA, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("QueueLoadB.txt", &noRefs, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("QueueLoadC.txt", &noRefs, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("PreLoadBrokenA.txt", &preLoadBrokenA, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("PreLoadBrokenB.txt", &preLoadBrokenB, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("PreLoadNoData.txt", &noRefs, m_serializeContext));
AssetWithQueueAndPreLoadReferences circularA;
AssetWithQueueAndPreLoadReferences circularB;
@ -318,43 +318,15 @@ namespace UnitTest
circularC.m_preLoad = m_testAssetManager->CreateAsset<AssetWithAssetReference>(CircularBId, AssetLoadBehavior::PreLoad);
circularD.m_preLoad = circularC.m_preLoad;
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "CircularA.txt", AZ::DataStream::ST_XML, &circularA, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "CircularB.txt", AZ::DataStream::ST_XML, &circularB, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "CircularC.txt", AZ::DataStream::ST_XML, &circularC, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "CircularD.txt", AZ::DataStream::ST_XML, &circularD, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("CircularA.txt", &circularA, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("CircularB.txt", &circularB, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("CircularC.txt", &circularC, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("CircularD.txt", &circularD, m_serializeContext));
m_assetHandlerAndCatalog->m_numCreations = 0;
}
}
void TearDownTest()
{
DeleteAssetFromDisk(GetTestFolderPath() + "TestAsset4.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "TestAsset5.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "TestAsset6.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "TestAsset1.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "TestAsset2.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "TestAsset3.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "DelayLoadAsset.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "NoLoadAsset.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "PreLoadRoot.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "PreLoadA.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "PreLoadB.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "PreLoadC.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "QueueLoadA.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "QueueLoadB.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "QueueLoadC.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "PreLoadBrokenA.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "PreLoadBrokenB.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "PreLoadNoData.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "CircularA.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "CircularB.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "CircularC.txt");
DeleteAssetFromDisk(GetTestFolderPath() + "CircularD.txt");
}
void CheckFinishedCreationsAndDestructions()
{
// Make sure asset jobs have finished before validating the number of destroyed assets, because it's possible that the asset job
@ -367,7 +339,7 @@ namespace UnitTest
};
static constexpr AZStd::chrono::seconds MaxDispatchTimeoutSeconds = BaseAssetManagerTest::DefaultTimeoutSeconds * 12;
template <typename Pred>
bool DispatchEventsUntilCondition(AZ::Data::AssetManager& assetManager, Pred&& conditionPredicate,
AZStd::chrono::seconds logIntervalSeconds = BaseAssetManagerTest::DefaultTimeoutSeconds,
@ -608,7 +580,7 @@ namespace UnitTest
AZ::Data::AssetData::AssetStatus expected_base_status = AZ::Data::AssetData::AssetStatus::Ready;
EXPECT_EQ(baseStatus, expected_base_status);
}
TEST_F(AssetJobsFloodTest, RapidAcquireAndRelease)
{
auto assetUuids = {
@ -641,7 +613,7 @@ namespace UnitTest
{
Asset<AssetWithAssetReference> asset1 =
m_testAssetManager->GetAsset(assetUuid, azrtti_typeid<AssetWithAssetReference>(), AZ::Data::AssetLoadBehavior::PreLoad);
if (checkLoaded)
{
asset1.BlockUntilLoadComplete();
@ -714,8 +686,8 @@ namespace UnitTest
AssetWithSerializedData ap;
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "a.txt", AZ::DataStream::ST_XML, &ap, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "b.txt", AZ::DataStream::ST_XML, &ap, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("a.txt", &ap, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("b.txt", &ap, m_serializeContext));
}
auto& assetManager = AssetManager::Instance();
@ -778,7 +750,7 @@ namespace UnitTest
* Verify that loads without using the Asset Container still work correctly
*/
class AssetContainerDisableTest
: public BaseAssetManagerTest
: public DisklessAssetManagerBase
{
public:
static inline const AZ::Uuid MyAsset1Id{ "{5B29FE2B-6B41-48C9-826A-C723951B0560}" };
@ -797,7 +769,7 @@ namespace UnitTest
void SetUp() override
{
BaseAssetManagerTest::SetUp();
DisklessAssetManagerBase::SetUp();
SetupTest();
}
@ -807,7 +779,7 @@ namespace UnitTest
AssetManager::Instance().UnregisterHandler(m_assetHandlerAndCatalog);
delete m_assetHandlerAndCatalog;
AssetManager::Destroy();
BaseAssetManagerTest::TearDown();
DisklessAssetManagerBase::TearDown();
}
void SetupAssets()
@ -849,9 +821,9 @@ namespace UnitTest
AssetWithSerializedData ap2;
AssetWithSerializedData ap3;
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset4.txt", AZ::DataStream::ST_XML, &ap1, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset5.txt", AZ::DataStream::ST_XML, &ap2, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset6.txt", AZ::DataStream::ST_XML, &ap3, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset4.txt", &ap1, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset5.txt", &ap2, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset6.txt", &ap3, m_serializeContext));
AssetWithAssetReference assetWithPreload1;
AssetWithAssetReference assetWithPreload2;
@ -862,9 +834,9 @@ namespace UnitTest
assetWithPreload3.m_asset = m_testAssetManager->CreateAsset<AssetWithSerializedData>(MyAsset6Id, AssetLoadBehavior::PreLoad);
EXPECT_EQ(m_assetHandlerAndCatalog->m_numCreations, 3);
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset1.txt", AZ::DataStream::ST_XML, &assetWithPreload1, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset2.txt", AZ::DataStream::ST_XML, &assetWithPreload2, m_serializeContext));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset3.txt", AZ::DataStream::ST_XML, &assetWithPreload3, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset1.txt", &assetWithPreload1, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset2.txt", &assetWithPreload2, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset3.txt", &assetWithPreload3, m_serializeContext));
m_assetHandlerAndCatalog->m_numCreations = 0;
}
@ -2014,11 +1986,12 @@ namespace UnitTest
CheckFinishedCreationsAndDestructions();
m_assetHandlerAndCatalog->AssetCatalogRequestBus::Handler::BusDisconnect();
}
/**
* Run multiple threads that get and release assets simultaneously to test AssetManager's thread safety
*/
class AssetJobsMultithreadedTest
: public BaseAssetManagerTest
: public DisklessAssetManagerBase
{
public:
static inline const AZ::Uuid MyAsset1Id{ "{5B29FE2B-6B41-48C9-826A-C723951B0560}" };
@ -2028,6 +2001,7 @@ namespace UnitTest
static inline const AZ::Uuid MyAsset5Id{ "{D9CDAB04-D206-431E-BDC0-1DD615D56197}" };
static inline const AZ::Uuid MyAsset6Id{ "{B2F139C3-5032-4B52-ADCA-D52A8F88E043}" };
// Initialize the Job Manager with 2 threads for the Asset Manager to use.
size_t GetNumJobManagerThreads() const override { return 2; }
@ -2078,9 +2052,9 @@ namespace UnitTest
AssetWithSerializedData ap2;
AssetWithSerializedData ap3;
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset4.txt", AZ::DataStream::ST_XML, &ap1, &context));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset5.txt", AZ::DataStream::ST_XML, &ap2, &context));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset6.txt", AZ::DataStream::ST_XML, &ap3, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset4.txt", &ap1, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset5.txt", &ap2, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset6.txt", &ap3, &context));
AssetWithAssetReference assetWithPreload1;
AssetWithAssetReference assetWithPreload2;
@ -2089,9 +2063,9 @@ namespace UnitTest
assetWithPreload2.m_asset = AssetManager::Instance().CreateAsset<AssetWithSerializedData>(MyAsset5Id, AssetLoadBehavior::PreLoad);
assetWithPreload3.m_asset = AssetManager::Instance().CreateAsset<AssetWithSerializedData>(MyAsset6Id, AssetLoadBehavior::PreLoad);
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset1.txt", AZ::DataStream::ST_XML, &assetWithPreload1, &context));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset2.txt", AZ::DataStream::ST_XML, &assetWithPreload2, &context));
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset3.txt", AZ::DataStream::ST_XML, &assetWithPreload3, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset1.txt", &assetWithPreload1, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset2.txt", &assetWithPreload2, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset3.txt", &assetWithPreload3, &context));
EXPECT_TRUE(assetHandlerAndCatalog->m_numCreations == 3);
assetHandlerAndCatalog->m_numCreations = 0;
@ -2191,22 +2165,22 @@ namespace UnitTest
// A will be saved to disk with MyAsset1Id
AssetWithAssetReference a;
a.m_asset = AssetManager::Instance().CreateAsset<AssetWithSerializedData>(MyAsset2Id);
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset1.txt", AZ::DataStream::ST_XML, &a, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset1.txt", &a, &context));
AssetWithAssetReference b;
b.m_asset = AssetManager::Instance().CreateAsset<AssetWithSerializedData>(MyAsset3Id);
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset2.txt", AZ::DataStream::ST_XML, &b, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset2.txt", &b, &context));
AssetWithAssetReference c;
c.m_asset = AssetManager::Instance().CreateAsset<AssetWithSerializedData>(MyAsset4Id);
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset3.txt", AZ::DataStream::ST_XML, &c, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset3.txt", &c, &context));
AssetWithAssetReference d;
d.m_asset = AssetManager::Instance().CreateAsset<AssetWithSerializedData>(MyAsset5Id);
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset4.txt", AZ::DataStream::ST_XML, &d, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset4.txt", &d, &context));
AssetWithAssetReference e;
e.m_asset = AssetManager::Instance().CreateAsset<AssetWithSerializedData>(MyAsset6Id);
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset5.txt", AZ::DataStream::ST_XML, &e, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset5.txt", &e, &context));
AssetWithAssetReference f;
f.m_asset = AssetManager::Instance().CreateAsset<AssetWithSerializedData>(MyAsset1Id); // refer back to asset1
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset6.txt", AZ::DataStream::ST_XML, &f, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset6.txt", &f, &context));
EXPECT_TRUE(assetHandlerAndCatalog->m_numCreations == 6);
assetHandlerAndCatalog->m_numCreations = 0;
@ -2347,26 +2321,26 @@ namespace UnitTest
// AssetD is MYASSETD
AssetWithSerializedData d;
d.m_data = 42;
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset4.txt", AZ::DataStream::ST_XML, &d, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset4.txt", &d, &context));
// AssetC is MYASSETC
AssetWithAssetReference c;
c.m_asset = db.CreateAsset<AssetWithSerializedData>(AssetId(MyAssetDId)); // point at D
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset3.txt", AZ::DataStream::ST_XML, &c, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset3.txt", &c, &context));
// AssetB is MYASSETB
AssetWithAssetReference b;
b.m_asset = db.CreateAsset<AssetWithAssetReference>(AssetId(MyAssetCId)); // point at C
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset2.txt", AZ::DataStream::ST_XML, &b, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset2.txt", &b, &context));
// AssetA will be written to disk as MYASSETA
AssetWithAssetReference a;
a.m_asset = db.CreateAsset<AssetWithAssetReference>(AssetId(MyAssetBId)); // point at B
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "TestAsset1.txt", AZ::DataStream::ST_XML, &a, &context));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("TestAsset1.txt", &a, &context));
}
const size_t numThreads = 4;
AZStd::atomic_int threadCount(numThreads);
constexpr size_t NumThreads = 4;
AZStd::atomic_int threadCount(NumThreads);
AZStd::condition_variable cv;
AZStd::vector<AZStd::thread> threads;
AZStd::atomic_bool keepDispatching(true);
@ -2381,7 +2355,7 @@ namespace UnitTest
AZStd::thread dispatchThread(dispatch);
for (size_t threadIdx = 0; threadIdx < numThreads; ++threadIdx)
for (size_t threadIdx = 0; threadIdx < NumThreads; ++threadIdx)
{
threads.emplace_back([&threadCount, &db, &cv]()
{
@ -2569,7 +2543,6 @@ namespace UnitTest
#if AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
TEST_F(AssetJobsMultithreadedTest, DISABLED_ParallelDeepAssetReferences)
#else
// temporarily disabled until sporadic failures can be root caused
TEST_F(AssetJobsMultithreadedTest, ParallelDeepAssetReferences)
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
{
@ -2577,7 +2550,7 @@ namespace UnitTest
}
class AssetManagerTests
: public BaseAssetManagerTest
: public DisklessAssetManagerBase
{
protected:
static inline const AZ::Uuid MyAsset1Id{ "{5B29FE2B-6B41-48C9-826A-C723951B0560}" };
@ -2592,7 +2565,7 @@ namespace UnitTest
void SetUp() override
{
BaseAssetManagerTest::SetUp();
DisklessAssetManagerBase::SetUp();
m_console = AZStd::make_unique<AZ::Console>();
AZ::Interface<AZ::IConsole>::Register(m_console.get());
@ -2631,7 +2604,7 @@ namespace UnitTest
AssetManager::Destroy();
AZ::Interface<AZ::IConsole>::Unregister(m_console.get());
m_console = nullptr;
BaseAssetManagerTest::TearDown();
DisklessAssetManagerBase::TearDown();
}
};
@ -2982,7 +2955,7 @@ namespace UnitTest
* the middle of loading. The tests help ensure that assets can't get stuck in perpetual loading states.
**/
class AssetManagerClearAssetReferenceTests
: public BaseAssetManagerTest
: public DisklessAssetManagerBase
{
protected:
static inline const AZ::Uuid RootAssetId{ "{AB13F568-C676-41FE-A7E9-341F71A78104}" };
@ -3001,7 +2974,7 @@ namespace UnitTest
void SetUp() override
{
BaseAssetManagerTest::SetUp();
DisklessAssetManagerBase::SetUp();
// create the database
AssetManager::Descriptor desc;
@ -3039,21 +3012,18 @@ namespace UnitTest
// Create and save the dependent asset first, so that we can get a reference to it.
AssetWithSerializedData dependentBlockingAsset;
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "DependentPreloadBlockingAsset.txt",
AZ::DataStream::ST_XML, &dependentBlockingAsset, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("DependentPreloadBlockingAsset.txt", &dependentBlockingAsset, m_serializeContext));
AssetWithAssetReference dependentAsset;
dependentAsset.m_asset = AssetManager::Instance().CreateAsset<AssetWithAssetReference>(
NestedDependentPreloadBlockingAssetId, AssetLoadBehavior::PreLoad);
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "DependentPreloadAsset.txt",
AZ::DataStream::ST_XML, &dependentAsset, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("DependentPreloadAsset.txt", &dependentAsset, m_serializeContext));
// Create and save the top-level asset.
AssetWithAssetReference rootAsset;
rootAsset.m_asset = AssetManager::Instance().CreateAsset<AssetWithAssetReference>(
DependentPreloadAssetId, AssetLoadBehavior::PreLoad);
EXPECT_TRUE(AZ::Utils::SaveObjectToFile(GetTestFolderPath() + "RootAsset.txt",
AZ::DataStream::ST_XML, &rootAsset, m_serializeContext));
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile("RootAsset.txt", &rootAsset, m_serializeContext));
}
void TearDown() override
@ -3065,7 +3035,7 @@ namespace UnitTest
delete m_assetHandlerAndCatalog;
AssetManager::Destroy();
BaseAssetManagerTest::TearDown();
DisklessAssetManagerBase::TearDown();
}
};

@ -165,4 +165,254 @@ namespace UnitTest
EXPECT_FALSE(AssetManager::Instance().HasActiveJobsOrStreamerRequests());
}
MemoryStreamerWrapper::MemoryStreamerWrapper()
{
using ::testing::_;
using ::testing::NiceMock;
using ::testing::Return;
ON_CALL(m_mockStreamer, SuspendProcessing()).WillByDefault([this]()
{
m_suspended = true;
});
ON_CALL(m_mockStreamer, ResumeProcessing()).WillByDefault([this]()
{
AZStd::unique_lock lock(m_mutex);
m_suspended = false;
while (!m_processingQueue.empty())
{
FileRequestHandle requestHandle = m_processingQueue.front();
m_processingQueue.pop();
const auto& onCompleteCallback = GetReadRequest(requestHandle)->m_callback;
if (onCompleteCallback)
{
onCompleteCallback(requestHandle);
}
}
});
ON_CALL(m_mockStreamer, Read(_, ::testing::An<IStreamerTypes::RequestMemoryAllocator&>(), _, _, _, _))
.WillByDefault(
[this](
[[maybe_unused]] AZStd::string_view relativePath, IStreamerTypes::RequestMemoryAllocator& allocator, size_t size,
AZStd::chrono::microseconds deadline, IStreamerTypes::Priority priority, [[maybe_unused]] size_t offset)
{
AZStd::unique_lock lock(m_mutex);
ReadRequest request;
// Save off the requested deadline and priority
request.m_deadline = deadline;
request.m_priority = priority;
request.m_data = allocator.Allocate(size, size, 8);
const auto* virtualFile = FindFile(relativePath);
AZ_Assert(
virtualFile->size() == size, "Streamer read request size did not match size of saved file: %d vs %d (%.*s)",
virtualFile->size(), size,
relativePath.size(), relativePath.data());
AZ_Assert(size > 0, "Size is zero %.*s", relativePath.size(), relativePath.data());
memcpy(request.m_data.m_address, virtualFile->data(), size);
// Create a real file request result and return it
request.m_request = m_context.GetNewExternalRequest();
m_readRequests.push_back(request);
return request.m_request;
});
ON_CALL(m_mockStreamer, SetRequestCompleteCallback(_, _))
.WillByDefault([this](FileRequestPtr& request, AZ::IO::IStreamer::OnCompleteCallback callback) -> FileRequestPtr&
{
// Save off the callback just so that we can call it when the request is "done"
AZStd::unique_lock lock(m_mutex);
ReadRequest* readRequest = GetReadRequest(request);
readRequest->m_callback = callback;
return request;
});
ON_CALL(m_mockStreamer, QueueRequest(_))
.WillByDefault([this](const auto& fileRequest)
{
if (!m_suspended)
{
decltype(ReadRequest::m_callback) onCompleteCallback;
AZStd::unique_lock lock(m_mutex);
ReadRequest* readRequest = GetReadRequest(fileRequest);
onCompleteCallback = readRequest->m_callback;
if (onCompleteCallback)
{
onCompleteCallback(fileRequest);
m_readRequests.erase(readRequest);
}
}
else
{
AZStd::unique_lock lock(m_mutex);
m_processingQueue.push(fileRequest);
}
});
ON_CALL(m_mockStreamer, GetRequestStatus(_))
.WillByDefault([]([[maybe_unused]] FileRequestHandle request)
{
// Return whatever request status has been set in this class
return IO::IStreamerTypes::RequestStatus::Completed;
});
ON_CALL(m_mockStreamer, GetReadRequestResult(_, _, _, _))
.WillByDefault([this](
[[maybe_unused]] FileRequestHandle request, void*& buffer, AZ::u64& numBytesRead,
IStreamerTypes::ClaimMemory claimMemory)
{
// Make sure the requestor plans to free the data buffer we allocated.
EXPECT_EQ(claimMemory, IStreamerTypes::ClaimMemory::Yes);
AZStd::unique_lock lock(m_mutex);
ReadRequest* readRequest = GetReadRequest(request);
// Provide valid data buffer results.
numBytesRead = readRequest->m_data.m_size;
buffer = readRequest->m_data.m_address;
return true;
});
ON_CALL(m_mockStreamer, RescheduleRequest(_, _, _))
.WillByDefault([this](IO::FileRequestPtr target, AZStd::chrono::microseconds newDeadline, IO::IStreamerTypes::Priority newPriority)
{
AZStd::unique_lock lock(m_mutex);
ReadRequest* readRequest = GetReadRequest(target);
readRequest->m_deadline = newDeadline;
readRequest->m_priority = newPriority;
return target;
});
}
ReadRequest* MemoryStreamerWrapper::GetReadRequest(FileRequestHandle request)
{
auto itr = AZStd::find_if(
m_readRequests.begin(), m_readRequests.end(),
[request](const ReadRequest& searchItem) -> bool
{
return (searchItem.m_request == request);
});
return itr;
}
AZStd::vector<char>* MemoryStreamerWrapper::FindFile(AZStd::string_view path)
{
auto itr = m_virtualFiles.find(path);
if (itr == m_virtualFiles.end())
{
// Path didn't work as-is, does it have the test folder prefixed? If so try removing it
if (AZ::StringFunc::StartsWith(path, GetTestFolderPath()))
{
AZStd::string_view pathWithoutFolder = path;
pathWithoutFolder = AZ::StringFunc::LStrip(pathWithoutFolder, GetTestFolderPath().c_str());
itr = m_virtualFiles.find(pathWithoutFolder);
}
else // Path isn't prefixed, so try adding it
{
itr = m_virtualFiles.find(GetTestFolderPath().append(path));
}
}
if (itr != m_virtualFiles.end())
{
return &itr->second;
}
// Currently no test expects a file not to exist so we assert to make it easy to quickly find where something went wrong
// If we ever need to test for a non-existent file this assert should just be conditionally disabled for that specific test
AZ_Assert(false, "Failed to find virtual file %*.s", path.size(), path.data())
return nullptr;
}
void DisklessAssetManagerBase::SetUp()
{
using ::testing::_;
using ::testing::NiceMock;
using ::testing::Return;
BaseAssetManagerTest::SetUp();
ON_CALL(m_fileIO, Size(::testing::Matcher<const char*>(::testing::_), _))
.WillByDefault(
[this](const char* path, u64& size)
{
AZStd::scoped_lock lock(m_streamerWrapper->m_mutex);
const auto* file = m_streamerWrapper->FindFile(path);
if (file)
{
size = file->size();
return ResultCode::Success;
}
AZ_Error("DisklessAssetManagerBase", false, "Failed to find virtual file %.*s", path);
return ResultCode::Error;
});
m_prevFileIO = IO::FileIOBase::GetInstance();
IO::FileIOBase::SetInstance(nullptr);
IO::FileIOBase::SetInstance(&m_fileIO);
}
void DisklessAssetManagerBase::TearDown()
{
IO::FileIOBase::SetInstance(nullptr);
IO::FileIOBase::SetInstance(m_prevFileIO);
BaseAssetManagerTest::TearDown();
}
IO::IStreamer* DisklessAssetManagerBase::CreateStreamer()
{
m_streamerWrapper = AZStd::make_unique<MemoryStreamerWrapper>();
return &(m_streamerWrapper->m_mockStreamer);
}
void DisklessAssetManagerBase::DestroyStreamer(IO::IStreamer*)
{
m_streamerWrapper = nullptr;
}
void DisklessAssetManagerBase::WriteAssetToDisk(const AZStd::string& assetName, const AZStd::string&)
{
AZStd::string assetFileName = GetTestFolderPath() + assetName;
AssetWithCustomData asset;
EXPECT_TRUE(m_streamerWrapper->WriteMemoryFile(assetFileName, &asset, m_serializeContext));
}
void DisklessAssetManagerBase::DeleteAssetFromDisk(const AZStd::string&)
{
}
}

@ -20,7 +20,8 @@
#include <Tests/Asset/TestAssetTypes.h>
#include <Tests/SerializeContextFixture.h>
#include <Tests/TestCatalog.h>
#include <AzCore/UnitTest/Mocks/MockFileIOBase.h>
#include <Streamer/IStreamerMock.h>
namespace UnitTest
{
@ -58,7 +59,11 @@ namespace UnitTest
// Subclasses can optionally override the streamer creation and destruction
virtual IO::IStreamer* CreateStreamer() { return aznew IO::Streamer(AZStd::thread_desc{}, StreamerComponent::CreateStreamerStack()); }
virtual void DestroyStreamer(IO::IStreamer* streamer) { delete streamer; }
virtual void DestroyStreamer(IO::IStreamer* streamer)
{
delete streamer;
streamer = nullptr;
}
void SetUp() override;
void TearDown() override;
@ -66,8 +71,8 @@ namespace UnitTest
static void SuppressTraceOutput(bool suppress);
// Helper methods to create and destroy actual assets on the disk for true end-to-end asset loading.
void WriteAssetToDisk(const AZStd::string& assetName, const AZStd::string& assetIdGuid);
void DeleteAssetFromDisk(const AZStd::string& assetName);
virtual void WriteAssetToDisk(const AZStd::string& assetName, const AZStd::string& assetIdGuid);
virtual void DeleteAssetFromDisk(const AZStd::string& assetName);
void BlockUntilAssetJobsAreComplete();
@ -82,4 +87,57 @@ namespace UnitTest
AZStd::vector<AZStd::string> m_assetsWritten;
};
struct ReadRequest
{
AZStd::chrono::milliseconds m_deadline{};
AZ::IO::IStreamerTypes::Priority m_priority{};
IO::IStreamerTypes::RequestMemoryAllocatorResult m_data{ nullptr, 0, IO::IStreamerTypes::MemoryType::ReadWrite };
AZ::IO::IStreamer::OnCompleteCallback m_callback;
IO::FileRequestPtr m_request;
};
struct MemoryStreamerWrapper
{
MemoryStreamerWrapper();
~MemoryStreamerWrapper() = default;
ReadRequest* GetReadRequest(IO::FileRequestHandle request);
template<typename TObject>
bool WriteMemoryFile(const AZStd::string& filePath, TObject* object, AZ::SerializeContext* context)
{
auto& buffer = m_virtualFiles[filePath];
ByteContainerStream stream(&buffer);
return AZ::Utils::SaveObjectToStream(stream, DataStream::StreamType::ST_XML, object, context);
}
AZStd::vector<char>* FindFile(AZStd::string_view path);
::testing::NiceMock<StreamerMock> m_mockStreamer;
IO::StreamerContext m_context;
AZStd::atomic_bool m_suspended{ false };
AZStd::recursive_mutex m_mutex;
AZStd::queue<FileRequestHandle> m_processingQueue; // Keeps tracks of requests that have been queued while processing is suspended
AZStd::vector<ReadRequest> m_readRequests;
AZStd::unordered_map<AZStd::string, AZStd::vector<char>> m_virtualFiles;
};
struct DisklessAssetManagerBase : BaseAssetManagerTest
{
void SetUp() override;
void TearDown() override;
IO::IStreamer* CreateStreamer() override;
void DestroyStreamer(IO::IStreamer*) override;
void WriteAssetToDisk(const AZStd::string& assetName, const AZStd::string& assetIdGuid) override;
void DeleteAssetFromDisk(const AZStd::string& assetName) override;
AZStd::unique_ptr<MemoryStreamerWrapper> m_streamerWrapper;
::testing::NiceMock<MockFileIOBase> m_fileIO;
IO::FileIOBase* m_prevFileIO{};
};
}

@ -539,6 +539,22 @@ tags=tools,renderer,metal)"
EXPECT_STREQ("Bat", commandLine.GetMiscValue(2).c_str());
}
TEST_F(SettingsRegistryMergeUtilsCommandLineFixture, RegsetFileArgument_DoesNotMergeNUL)
{
AZStd::string regsetFile = AZ::IO::SystemFile::GetNullFilename();
AZ::CommandLine commandLine;
commandLine.Parse({ "--regset-file", regsetFile });
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_CommandLine(*m_registry, commandLine, false);
// Add a settings path to anchor loaded settings underneath
regsetFile = AZStd::string::format("%s::/AnchorPath/Of/Settings", AZ::IO::SystemFile::GetNullFilename());
commandLine.Parse({ "--regset-file", regsetFile });
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_CommandLine(*m_registry, commandLine, false);
EXPECT_EQ(AZ::SettingsRegistryInterface::Type::NoType, m_registry->GetType("/AnchorPath/Of/Settings"));
}
using SettingsRegistryAncestorDescendantOrEqualPathFixture = SettingsRegistryMergeUtilsCommandLineFixture;
TEST_F(SettingsRegistryAncestorDescendantOrEqualPathFixture, ValidateThatAncestorOrDescendantOrPathWithTheSameValue_Succeeds)

@ -167,7 +167,8 @@ namespace UnitTest
if (!info.m_streamName.empty())
{
AZStd::string fullName = GetTestFolderPath() + info.m_streamName;
info.m_dataLen = static_cast<size_t>(IO::SystemFile::Length(fullName.c_str()));
IO::FileIOBase* io = IO::FileIOBase::GetInstance();
io->Size(fullName.c_str(), info.m_dataLen);
}
else
{
@ -187,8 +188,11 @@ namespace UnitTest
if (!info.m_streamName.empty())
{
IO::FileIOBase* io = AZ::IO::FileIOBase::GetInstance();
AZStd::string fullName = GetTestFolderPath() + info.m_streamName;
info.m_dataLen = static_cast<size_t>(IO::SystemFile::Length(fullName.c_str()));
io->Size(fullName.c_str(), info.m_dataLen);
}
else
{

@ -6,16 +6,17 @@
*
*/
#include "CommunicatorTracePrinter.h"
#include "ProcessCommunicatorTracePrinter.h"
CommunicatorTracePrinter::CommunicatorTracePrinter(AzFramework::ProcessCommunicator* communicator, const char* window) :
ProcessCommunicatorTracePrinter::ProcessCommunicatorTracePrinter(AzFramework::ProcessCommunicator* communicator, const char* window) :
m_communicator(communicator),
m_window(window)
{
m_stringBeingConcatenated.reserve(1024);
}
CommunicatorTracePrinter::~CommunicatorTracePrinter()
ProcessCommunicatorTracePrinter::~ProcessCommunicatorTracePrinter()
{
// flush stdout
WriteCurrentString(false);
@ -24,7 +25,7 @@ CommunicatorTracePrinter::~CommunicatorTracePrinter()
WriteCurrentString(true);
}
void CommunicatorTracePrinter::Pump()
void ProcessCommunicatorTracePrinter::Pump()
{
if (m_communicator->IsValid())
{
@ -42,7 +43,7 @@ void CommunicatorTracePrinter::Pump()
}
}
void CommunicatorTracePrinter::ParseDataBuffer(AZ::u32 readSize, bool isFromStdErr)
void ProcessCommunicatorTracePrinter::ParseDataBuffer(AZ::u32 readSize, bool isFromStdErr)
{
if (readSize > AZ_ARRAY_SIZE(m_streamBuffer))
{
@ -67,7 +68,7 @@ void CommunicatorTracePrinter::ParseDataBuffer(AZ::u32 readSize, bool isFromStdE
}
}
void CommunicatorTracePrinter::WriteCurrentString(bool isFromStdErr)
void ProcessCommunicatorTracePrinter::WriteCurrentString(bool isFromStdErr)
{
AZStd::string& bufferToUse = isFromStdErr ? m_errorStringBeingConcatenated : m_stringBeingConcatenated;

@ -10,20 +10,21 @@
#include <AzFramework/Process/ProcessCommunicator.h>
//! CommunicatorTracePrinter listens to stderr and stdout of a running process and writes its output to the AZ_Trace system
//! ProcessCommunicatorTracePrinter listens to stderr and stdout of a running process and writes its output to the AZ_Trace system
//! Importantly, it does not do any blocking operations.
class CommunicatorTracePrinter
class ProcessCommunicatorTracePrinter
{
public:
CommunicatorTracePrinter(AzFramework::ProcessCommunicator* communicator, const char* window);
~CommunicatorTracePrinter();
ProcessCommunicatorTracePrinter(AzFramework::ProcessCommunicator* communicator, const char* window);
~ProcessCommunicatorTracePrinter();
// call this periodically to drain the buffers and write them.
//! Call this periodically to drain the buffers and write them.
void Pump();
// drains the buffer into the string thats being built, then traces the string when it hits a newline.
//! Drains the buffer into the string that's being built, then traces the string when it hits a newline.
void ParseDataBuffer(AZ::u32 readSize, bool isFromStdErr);
//! Prints the current buffer to AZ_Error or AZ_TracePrintf so that it can be picked up by AZ::Debug::Trace
void WriteCurrentString(bool isFromStdError);
private:

@ -26,10 +26,11 @@ namespace AzFramework::Terrain
->Event("GetSurfaceWeights", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetSurfaceWeights)
->Event("GetSurfaceWeightsFromVector2",
&AzFramework::Terrain::TerrainDataRequestBus::Events::GetSurfaceWeightsFromVector2)
->Event("GetIsHole", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetIsHole)
->Event("GetIsHoleFromFloats", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetIsHoleFromFloats)
->Event("GetSurfacePoint", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetSurfacePoint)
->Event("GetSurfacePoint", &AzFramework::Terrain::TerrainDataRequestBus::Events::BehaviorContextGetSurfacePoint)
->Event("GetSurfacePointFromVector2",
&AzFramework::Terrain::TerrainDataRequestBus::Events::GetSurfacePointFromVector2)
&AzFramework::Terrain::TerrainDataRequestBus::Events::BehaviorContextGetSurfacePointFromVector2)
->Event("GetTerrainAabb", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainAabb)
->Event("GetTerrainHeightQueryResolution",
&AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainHeightQueryResolution)

@ -52,8 +52,8 @@ namespace AzFramework
virtual void SetTerrainAabb(const AZ::Aabb& worldBounds) = 0;
//! Returns terrains height in meters at location x,y.
//! @terrainExistsPtr: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside a terrain HOLE then *terrainExistsPtr will become false,
//! otherwise *terrainExistsPtr will become true.
//! @terrainExistsPtr: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside
//! a terrain HOLE then *terrainExistsPtr will become false, otherwise *terrainExistsPtr will become true.
virtual float GetHeight(const AZ::Vector3& position, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual float GetHeightFromVector2(
const AZ::Vector2& position, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
@ -68,8 +68,7 @@ namespace AzFramework
// Given an XY coordinate, return the surface normal.
//! @terrainExists: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside a
//! terrain HOLE then *terrainExistsPtr will be set to false,
//! otherwise *terrainExistsPtr will be set to true.
//! terrain HOLE then *terrainExistsPtr will be set to false, otherwise *terrainExistsPtr will be set to true.
virtual AZ::Vector3 GetNormal(
const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual AZ::Vector3 GetNormalFromVector2(
@ -78,8 +77,8 @@ namespace AzFramework
float x, float y, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
//! Given an XY coordinate, return the max surface type and weight.
//! @terrainExists: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside a terrain HOLE then *terrainExistsPtr will be set to false,
//! otherwise *terrainExistsPtr will be set to true.
//! @terrainExists: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside
//! a terrain HOLE then *terrainExistsPtr will be set to false, otherwise *terrainExistsPtr will be set to true.
virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeight(
const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromVector2(
@ -87,8 +86,8 @@ namespace AzFramework
virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromFloats(
float x, float y, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
//! Given an XY coordinate, return the set of surface types and weights. The Vector3 input position version is defined to ignore
//! the input Z value.
//! Given an XY coordinate, return the set of surface types and weights. The Vector3 input position version is defined to
//! ignore the input Z value.
virtual void GetSurfaceWeights(
const AZ::Vector3& inPosition,
SurfaceData::SurfaceTagWeightList& outSurfaceWeights,
@ -106,13 +105,14 @@ namespace AzFramework
Sampler sampleFilter = Sampler::DEFAULT,
bool* terrainExistsPtr = nullptr) const = 0;
//! Convenience function for low level systems that can't do a reverse lookup from Crc to string. Everyone else should use GetMaxSurfaceWeight or GetMaxSurfaceWeightFromFloats.
//! Convenience function for low level systems that can't do a reverse lookup from Crc to string. Everyone else should use
//! GetMaxSurfaceWeight or GetMaxSurfaceWeightFromFloats.
//! Not available in the behavior context.
//! Returns nullptr if the position is inside a hole or outside of the terrain boundaries.
virtual const char* GetMaxSurfaceName(
const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
//! Given an XY coordinate, return all terrain information at that location. The Vector3 input position version is defined
//! Given an XY coordinate, return all terrain information at that location. The Vector3 input position version is defined
//! to ignore the input Z value.
virtual void GetSurfacePoint(
const AZ::Vector3& inPosition,
@ -130,6 +130,27 @@ namespace AzFramework
SurfaceData::SurfacePoint& outSurfacePoint,
Sampler sampleFilter = Sampler::DEFAULT,
bool* terrainExistsPtr = nullptr) const = 0;
private:
// Private variations of the GetSurfacePoint API exposed to BehaviorContext that returns a value instead of
// using an "out" parameter. The "out" parameter is useful for reusing memory allocated in SurfacePoint when
// using the public API, but can't easily be used from Script Canvas.
SurfaceData::SurfacePoint BehaviorContextGetSurfacePoint(
const AZ::Vector3& inPosition,
Sampler sampleFilter = Sampler::DEFAULT) const
{
SurfaceData::SurfacePoint result;
GetSurfacePoint(inPosition, result, sampleFilter);
return result;
}
SurfaceData::SurfacePoint BehaviorContextGetSurfacePointFromVector2(
const AZ::Vector2& inPosition,
Sampler sampleFilter = Sampler::DEFAULT) const
{
SurfaceData::SurfacePoint result;
GetSurfacePointFromVector2(inPosition, result, sampleFilter);
return result;
}
};
using TerrainDataRequestBus = AZ::EBus<TerrainDataRequests>;

@ -143,6 +143,13 @@ namespace AzFramework
return vsync_interval;
}
bool NativeWindow::SetSyncInterval(uint32_t newSyncInterval)
{
vsync_interval = newSyncInterval;
return true;
}
/*static*/ bool NativeWindow::GetFullScreenStateOfDefaultWindow()
{
NativeWindowHandle defaultWindowHandle = nullptr;

@ -132,6 +132,7 @@ namespace AzFramework
void ToggleFullScreenState() override;
float GetDpiScaleFactor() const override;
uint32_t GetSyncInterval() const override;
bool SetSyncInterval(uint32_t newSyncInterval) override;
uint32_t GetDisplayRefreshRate() const override;
//! Get the full screen state of the default window.

@ -78,6 +78,10 @@ namespace AzFramework
//! Returns the sync interval which tells the drivers the number of v-blanks to synchronize with
virtual uint32_t GetSyncInterval() const = 0;
//! Sets the sync interval which tells the drivers the number of v-blanks to synchronize with
//! Returns if the sync interval was succesfully set
virtual bool SetSyncInterval(uint32_t newSyncInterval) = 0;
//! Returns the refresh rate of the main display
virtual uint32_t GetDisplayRefreshRate() const = 0;
};

@ -277,6 +277,8 @@ set(FILES
Process/ProcessWatcher.cpp
Process/ProcessWatcher.h
Process/ProcessCommon_fwd.h
Process/ProcessCommunicatorTracePrinter.cpp
Process/ProcessCommunicatorTracePrinter.h
ProjectManager/ProjectManager.h
ProjectManager/ProjectManager.cpp
Render/GameIntersectorComponent.h

@ -36,6 +36,7 @@ namespace UnitTest
MOCK_METHOD0(ToggleFullScreenState, void());
MOCK_CONST_METHOD0(GetDpiScaleFactor, float());
MOCK_CONST_METHOD0(GetSyncInterval, uint32_t());
MOCK_METHOD1(SetSyncInterval, bool(uint32_t));
MOCK_CONST_METHOD0(GetDisplayRefreshRate, uint32_t());
};
} // namespace UnitTest

@ -39,6 +39,10 @@ namespace AzManipulatorTestFramework
DerivedDispatcherT* MouseLButtonDown();
//! Set the left mouse button up.
DerivedDispatcherT* MouseLButtonUp();
//! Set the middle mouse button down.
DerivedDispatcherT* MouseMButtonDown();
//! Set the middle mouse button up.
DerivedDispatcherT* MouseMButtonUp();
//! Send a double click event.
DerivedDispatcherT* MouseLButtonDoubleClick();
//! Set the keyboard modifier button down.
@ -73,6 +77,8 @@ namespace AzManipulatorTestFramework
virtual void CameraStateImpl(const AzFramework::CameraState& cameraState) = 0;
virtual void MouseLButtonDownImpl() = 0;
virtual void MouseLButtonUpImpl() = 0;
virtual void MouseMButtonDownImpl() = 0;
virtual void MouseMButtonUpImpl() = 0;
virtual void MouseLButtonDoubleClickImpl() = 0;
virtual void MousePositionImpl(const AzFramework::ScreenPoint& position) = 0;
virtual void KeyboardModifierDownImpl(const AzToolsFramework::ViewportInteraction::KeyboardModifier& keyModifier) = 0;
@ -183,6 +189,22 @@ namespace AzManipulatorTestFramework
return static_cast<DerivedDispatcherT*>(this);
}
template<typename DerivedDispatcherT>
DerivedDispatcherT* ActionDispatcher<DerivedDispatcherT>::MouseMButtonDown()
{
Log("Mouse middle button down");
MouseMButtonDownImpl();
return static_cast<DerivedDispatcherT*>(this);
}
template<typename DerivedDispatcherT>
DerivedDispatcherT* ActionDispatcher<DerivedDispatcherT>::MouseMButtonUp()
{
Log("Mouse middle button up");
MouseMButtonUpImpl();
return static_cast<DerivedDispatcherT*>(this);
}
template<typename DerivedDispatcherT>
DerivedDispatcherT* ActionDispatcher<DerivedDispatcherT>::MouseLButtonDoubleClick()
{

@ -8,6 +8,7 @@
#pragma once
#include <AzFramework/Viewport/ViewportId.h>
#include <AzToolsFramework/Manipulators/ManipulatorBus.h>
#include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
@ -40,7 +41,7 @@ namespace AzManipulatorTestFramework
//! Set the angular step.
virtual void SetAngularStep(float step) = 0;
//! Get the viewport id.
virtual int GetViewportId() const = 0;
virtual AzFramework::ViewportId GetViewportId() const = 0;
//! Updates the visibility state.
//! Updates which entities are currently visible given the current camera state.
virtual void UpdateVisibility() = 0;

@ -30,7 +30,7 @@ namespace AzManipulatorTestFramework
private:
AZStd::shared_ptr<CustomManipulatorManager> m_customManager;
std::unique_ptr<ViewportInteraction> m_viewportInteraction;
std::unique_ptr<DirectCallManipulatorManager> m_manipulatorManager;
AZStd::unique_ptr<ViewportInteraction> m_viewportInteraction;
AZStd::unique_ptr<DirectCallManipulatorManager> m_manipulatorManager;
};
} // namespace AzManipulatorTestFramework

@ -25,7 +25,7 @@ namespace AzManipulatorTestFramework
using MouseInteractionEvent = AzToolsFramework::ViewportInteraction::MouseInteractionEvent;
public:
explicit ImmediateModeActionDispatcher(ManipulatorViewportInteraction& viewportManipulatorInteraction);
explicit ImmediateModeActionDispatcher(ManipulatorViewportInteraction& manipulatorViewportInteraction);
~ImmediateModeActionDispatcher();
//! Clear the current event state.
@ -55,13 +55,15 @@ namespace AzManipulatorTestFramework
AZStd::chrono::milliseconds EditorViewportInputTimeNow() override;
protected:
// ActionDispatcher ...
// ActionDispatcher overrides ...
void SetSnapToGridImpl(bool enabled) override;
void SetStickySelectImpl(bool enabled) override;
void GridSizeImpl(float size) override;
void CameraStateImpl(const AzFramework::CameraState& cameraState) override;
void MouseLButtonDownImpl() override;
void MouseLButtonUpImpl() override;
void MouseMButtonDownImpl() override;
void MouseMButtonUpImpl() override;
void MouseLButtonDoubleClickImpl() override;
void MousePositionImpl(const AzFramework::ScreenPoint& position) override;
void KeyboardModifierDownImpl(const KeyboardModifier& keyModifier) override;
@ -82,7 +84,7 @@ namespace AzManipulatorTestFramework
const MouseInteractionEvent* GetMouseInteractionEvent() const;
mutable AZStd::unique_ptr<MouseInteractionEvent> m_event;
ManipulatorViewportInteraction& m_viewportManipulatorInteraction;
ManipulatorViewportInteraction& m_manipulatorViewportInteraction;
//! Current time that ticks up after each call to EditorViewportInputTimeNow.
AZStd::chrono::milliseconds m_timeNow = AZStd::chrono::milliseconds(0);

@ -33,7 +33,7 @@ namespace AzManipulatorTestFramework
void SetAngularSnapping(bool enabled) override;
void SetGridSize(float size) override;
void SetAngularStep(float step) override;
int GetViewportId() const override;
AzFramework::ViewportId GetViewportId() const override;
void UpdateVisibility() override;
void SetStickySelect(bool enabled) override;
AZ::Vector3 DefaultEditorCameraPosition() const override;
@ -60,9 +60,10 @@ namespace AzManipulatorTestFramework
void FindVisibleEntities(AZStd::vector<AZ::EntityId>& visibleEntities) override;
private:
static constexpr AzFramework::ViewportId m_viewportId = 1234; //!< Arbitrary viewport id for manipulator tests.
AzFramework::EntityVisibilityQuery m_entityVisibilityQuery;
AZStd::unique_ptr<NullDebugDisplayRequests> m_nullDebugDisplayRequests;
const int m_viewportId = 1234; // Arbitrary viewport id for manipulator tests
AzFramework::CameraState m_cameraState;
bool m_gridSnapping = false;
bool m_angularSnapping = false;

@ -29,8 +29,8 @@ namespace AzManipulatorTestFramework
using KeyboardModifier = AzToolsFramework::ViewportInteraction::KeyboardModifier;
using MouseInteractionEvent = AzToolsFramework::ViewportInteraction::MouseInteractionEvent;
ImmediateModeActionDispatcher::ImmediateModeActionDispatcher(ManipulatorViewportInteraction& viewportManipulatorInteraction)
: m_viewportManipulatorInteraction(viewportManipulatorInteraction)
ImmediateModeActionDispatcher::ImmediateModeActionDispatcher(ManipulatorViewportInteraction& manipulatorViewportInteraction)
: m_manipulatorViewportInteraction(manipulatorViewportInteraction)
{
AzToolsFramework::ViewportInteraction::EditorModifierKeyRequestBus::Handler::BusConnect();
AzToolsFramework::ViewportInteraction::EditorViewportInputTimeNowRequestBus::Handler::BusConnect();
@ -48,34 +48,34 @@ namespace AzManipulatorTestFramework
// mouse down and mouse up event, to match the editor behavior we insert this event
// to ensure the tests are simulating the same environment as the editor
GetMouseInteractionEvent()->m_mouseEvent = AzToolsFramework::ViewportInteraction::MouseEvent::Move;
m_viewportManipulatorInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event);
m_manipulatorViewportInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event);
}
void ImmediateModeActionDispatcher::SetSnapToGridImpl(const bool enabled)
{
m_viewportManipulatorInteraction.GetViewportInteraction().SetGridSnapping(enabled);
m_manipulatorViewportInteraction.GetViewportInteraction().SetGridSnapping(enabled);
}
void ImmediateModeActionDispatcher::SetStickySelectImpl(const bool enabled)
{
m_viewportManipulatorInteraction.GetViewportInteraction().SetStickySelect(enabled);
m_manipulatorViewportInteraction.GetViewportInteraction().SetStickySelect(enabled);
}
void ImmediateModeActionDispatcher::GridSizeImpl(const float size)
{
m_viewportManipulatorInteraction.GetViewportInteraction().SetGridSize(size);
m_manipulatorViewportInteraction.GetViewportInteraction().SetGridSize(size);
}
void ImmediateModeActionDispatcher::CameraStateImpl(const AzFramework::CameraState& cameraState)
{
m_viewportManipulatorInteraction.GetViewportInteraction().SetCameraState(cameraState);
m_manipulatorViewportInteraction.GetViewportInteraction().SetCameraState(cameraState);
}
void ImmediateModeActionDispatcher::MouseLButtonDownImpl()
{
ToggleOn(GetMouseInteractionEvent()->m_mouseInteraction.m_mouseButtons.m_mouseButtons, MouseButton::Left);
GetMouseInteractionEvent()->m_mouseEvent = AzToolsFramework::ViewportInteraction::MouseEvent::Down;
m_viewportManipulatorInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event);
m_manipulatorViewportInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event);
// the mouse position will be the same as the previous event, thus the delta will be 0
MouseMoveAfterButton();
}
@ -83,17 +83,35 @@ namespace AzManipulatorTestFramework
void ImmediateModeActionDispatcher::MouseLButtonUpImpl()
{
GetMouseInteractionEvent()->m_mouseEvent = AzToolsFramework::ViewportInteraction::MouseEvent::Up;
m_viewportManipulatorInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event);
m_manipulatorViewportInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event);
ToggleOff(GetMouseInteractionEvent()->m_mouseInteraction.m_mouseButtons.m_mouseButtons, MouseButton::Left);
// the mouse position will be the same as the previous event, thus the delta will be 0
MouseMoveAfterButton();
}
void ImmediateModeActionDispatcher::MouseMButtonDownImpl()
{
ToggleOn(GetMouseInteractionEvent()->m_mouseInteraction.m_mouseButtons.m_mouseButtons, MouseButton::Middle);
GetMouseInteractionEvent()->m_mouseEvent = AzToolsFramework::ViewportInteraction::MouseEvent::Down;
m_manipulatorViewportInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event);
// the mouse position will be the same as the previous event, thus the delta will be 0
MouseMoveAfterButton();
}
void ImmediateModeActionDispatcher::MouseMButtonUpImpl()
{
GetMouseInteractionEvent()->m_mouseEvent = AzToolsFramework::ViewportInteraction::MouseEvent::Up;
m_manipulatorViewportInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event);
ToggleOff(GetMouseInteractionEvent()->m_mouseInteraction.m_mouseButtons.m_mouseButtons, MouseButton::Middle);
// the mouse position will be the same as the previous event, thus the delta will be 0
MouseMoveAfterButton();
}
void ImmediateModeActionDispatcher::MouseLButtonDoubleClickImpl()
{
GetMouseInteractionEvent()->m_mouseEvent = AzToolsFramework::ViewportInteraction::MouseEvent::DoubleClick;
ToggleOn(GetMouseInteractionEvent()->m_mouseInteraction.m_mouseButtons.m_mouseButtons, MouseButton::Left);
m_viewportManipulatorInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event);
m_manipulatorViewportInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event);
ToggleOff(GetMouseInteractionEvent()->m_mouseInteraction.m_mouseButtons.m_mouseButtons, MouseButton::Left);
// the mouse position will be the same as the previous event, thus the delta will be 0
MouseMoveAfterButton();
@ -101,10 +119,10 @@ namespace AzManipulatorTestFramework
void ImmediateModeActionDispatcher::MousePositionImpl(const AzFramework::ScreenPoint& position)
{
const auto cameraState = m_viewportManipulatorInteraction.GetViewportInteraction().GetCameraState();
const auto cameraState = m_manipulatorViewportInteraction.GetViewportInteraction().GetCameraState();
GetMouseInteractionEvent()->m_mouseInteraction.m_mousePick = BuildMousePick(position, cameraState);
GetMouseInteractionEvent()->m_mouseEvent = AzToolsFramework::ViewportInteraction::MouseEvent::Move;
m_viewportManipulatorInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event);
m_manipulatorViewportInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event);
}
void ImmediateModeActionDispatcher::KeyboardModifierDownImpl(const KeyboardModifier& keyModifier)
@ -144,7 +162,7 @@ namespace AzManipulatorTestFramework
{
m_event = AZStd::unique_ptr<MouseInteractionEvent>(AZStd::make_unique<MouseInteractionEvent>());
m_event->m_mouseInteraction.m_interactionId.m_viewportId =
m_viewportManipulatorInteraction.GetViewportInteraction().GetViewportId();
m_manipulatorViewportInteraction.GetViewportInteraction().GetViewportId();
}
return m_event.get();
@ -178,12 +196,12 @@ namespace AzManipulatorTestFramework
void ImmediateModeActionDispatcher::ExpectManipulatorBeingInteractedImpl()
{
EXPECT_TRUE(m_viewportManipulatorInteraction.GetManipulatorManager().ManipulatorBeingInteracted());
EXPECT_TRUE(m_manipulatorViewportInteraction.GetManipulatorManager().ManipulatorBeingInteracted());
}
void ImmediateModeActionDispatcher::ExpectManipulatorNotBeingInteractedImpl()
{
EXPECT_FALSE(m_viewportManipulatorInteraction.GetManipulatorManager().ManipulatorBeingInteracted());
EXPECT_FALSE(m_manipulatorViewportInteraction.GetManipulatorManager().ManipulatorBeingInteracted());
}
ImmediateModeActionDispatcher* ImmediateModeActionDispatcher::ResetEvent()

@ -135,20 +135,20 @@ namespace AzManipulatorTestFramework
m_angularStep = step;
}
int ViewportInteraction::GetViewportId() const
AzFramework::ViewportId ViewportInteraction::GetViewportId() const
{
return m_viewportId;
}
AZ::Vector3 ViewportInteraction::ViewportScreenToWorld([[maybe_unused]] const AzFramework::ScreenPoint& screenPosition)
{
return AZ::Vector3::CreateZero();
return AzFramework::ScreenToWorld(screenPosition, m_cameraState);
}
AzToolsFramework::ViewportInteraction::ProjectedViewportRay ViewportInteraction::ViewportScreenToWorldRay(
[[maybe_unused]] const AzFramework::ScreenPoint& screenPosition)
{
return {};
return AzToolsFramework::ViewportInteraction::ViewportScreenToWorldRay(m_cameraState, screenPosition);
}
float ViewportInteraction::DeviceScalingFactor()

@ -53,7 +53,6 @@ static void OptimizedSetParent(QWidget* widget, QWidget* parent)
namespace AzQtComponents
{
static const FancyDockingDropZoneConstants g_FancyDockingConstants;
// Constant for the threshold in pixels for snapping to edges while dragging for docking
static const int g_snapThresholdInPixels = 15;
@ -155,7 +154,7 @@ namespace AzQtComponents
// Timer for updating our hovered drop zone opacity
QObject::connect(m_dropZoneHoverFadeInTimer, &QTimer::timeout, this, &FancyDocking::onDropZoneHoverFadeInUpdate);
m_dropZoneHoverFadeInTimer->setInterval(g_FancyDockingConstants.dropZoneHoverFadeUpdateIntervalMS);
m_dropZoneHoverFadeInTimer->setInterval(FancyDockingDropZoneConstants::dropZoneHoverFadeUpdateIntervalMS);
QIcon dragIcon = QIcon(QStringLiteral(":/Cursors/Grabbing.svg"));
m_dragCursor = QCursor(dragIcon.pixmap(16), 5, 2);
}
@ -333,13 +332,13 @@ namespace AzQtComponents
*/
void FancyDocking::onDropZoneHoverFadeInUpdate()
{
const qreal dropZoneHoverOpacity = g_FancyDockingConstants.dropZoneHoverFadeIncrement + m_dropZoneState.dropZoneHoverOpacity();
const qreal dropZoneHoverOpacity = FancyDockingDropZoneConstants::dropZoneHoverFadeIncrement + m_dropZoneState.dropZoneHoverOpacity();
// Once we've reached the full drop zone opacity, cut it off in case we
// went over and stop the timer
if (dropZoneHoverOpacity >= g_FancyDockingConstants.dropZoneOpacity)
if (dropZoneHoverOpacity >= FancyDockingDropZoneConstants::dropZoneOpacity)
{
m_dropZoneState.setDropZoneHoverOpacity(g_FancyDockingConstants.dropZoneOpacity);
m_dropZoneState.setDropZoneHoverOpacity(FancyDockingDropZoneConstants::dropZoneOpacity);
m_dropZoneHoverFadeInTimer->stop();
}
else
@ -792,12 +791,12 @@ namespace AzQtComponents
QPoint mainWindowTopLeft = multiscreenMapFromGlobal(mainWindow->mapToGlobal(mainWindowRect.topLeft()));
QPoint mainWindowTopRight = multiscreenMapFromGlobal(mainWindow->mapToGlobal(mainWindowRect.topRight()));
QPoint mainWindowBottomLeft = multiscreenMapFromGlobal(mainWindow->mapToGlobal(mainWindowRect.bottomLeft()));
QSize absoluteLeftRightSize(g_FancyDockingConstants.absoluteDropZoneSizeInPixels, mainWindowRect.height());
QSize absoluteLeftRightSize(FancyDockingDropZoneConstants::absoluteDropZoneSizeInPixels, mainWindowRect.height());
QRect absoluteLeftDropZone(mainWindowTopLeft, absoluteLeftRightSize);
QRect absoluteRightDropZone(mainWindowTopRight - QPoint(g_FancyDockingConstants.absoluteDropZoneSizeInPixels, 0), absoluteLeftRightSize);
QSize absoluteTopBottomSize(mainWindowRect.width(), g_FancyDockingConstants.absoluteDropZoneSizeInPixels);
QRect absoluteRightDropZone(mainWindowTopRight - QPoint(FancyDockingDropZoneConstants::absoluteDropZoneSizeInPixels, 0), absoluteLeftRightSize);
QSize absoluteTopBottomSize(mainWindowRect.width(), FancyDockingDropZoneConstants::absoluteDropZoneSizeInPixels);
QRect absoluteTopDropZone(mainWindowTopLeft, absoluteTopBottomSize);
QRect absoluteBottomDropZone(mainWindowBottomLeft - QPoint(0, g_FancyDockingConstants.absoluteDropZoneSizeInPixels), absoluteTopBottomSize);
QRect absoluteBottomDropZone(mainWindowBottomLeft - QPoint(0, FancyDockingDropZoneConstants::absoluteDropZoneSizeInPixels), absoluteTopBottomSize);
// If the drop target is a main window, then we will only show the absolute
// drop zone if the cursor is in that zone already
@ -986,16 +985,16 @@ namespace AzQtComponents
switch (m_dropZoneState.absoluteDropZoneArea())
{
case Qt::LeftDockWidgetArea:
dockRect.setX(dockRect.x() + g_FancyDockingConstants.absoluteDropZoneSizeInPixels);
dockRect.setX(dockRect.x() + FancyDockingDropZoneConstants::absoluteDropZoneSizeInPixels);
break;
case Qt::RightDockWidgetArea:
dockRect.setWidth(dockRect.width() - g_FancyDockingConstants.absoluteDropZoneSizeInPixels);
dockRect.setWidth(dockRect.width() - FancyDockingDropZoneConstants::absoluteDropZoneSizeInPixels);
break;
case Qt::TopDockWidgetArea:
dockRect.setY(dockRect.y() + g_FancyDockingConstants.absoluteDropZoneSizeInPixels);
dockRect.setY(dockRect.y() + FancyDockingDropZoneConstants::absoluteDropZoneSizeInPixels);
break;
case Qt::BottomDockWidgetArea:
dockRect.setHeight(dockRect.height() - g_FancyDockingConstants.absoluteDropZoneSizeInPixels);
dockRect.setHeight(dockRect.height() - FancyDockingDropZoneConstants::absoluteDropZoneSizeInPixels);
break;
}
@ -1034,15 +1033,15 @@ namespace AzQtComponents
// Set the drop zone width/height to the default, but if the dock widget
// width and/or height is below the threshold, then switch to scaling them
// down accordingly
int dropZoneWidth = g_FancyDockingConstants.dropZoneSizeInPixels;
if (dockWidth < g_FancyDockingConstants.minDockSizeBeforeDropZoneScalingInPixels)
int dropZoneWidth = FancyDockingDropZoneConstants::dropZoneSizeInPixels;
if (dockWidth < FancyDockingDropZoneConstants::minDockSizeBeforeDropZoneScalingInPixels)
{
dropZoneWidth = aznumeric_cast<int>(dockWidth * g_FancyDockingConstants.dropZoneScaleFactor);
dropZoneWidth = aznumeric_cast<int>(dockWidth * FancyDockingDropZoneConstants::dropZoneScaleFactor);
}
int dropZoneHeight = g_FancyDockingConstants.dropZoneSizeInPixels;
if (dockHeight < g_FancyDockingConstants.minDockSizeBeforeDropZoneScalingInPixels)
int dropZoneHeight = FancyDockingDropZoneConstants::dropZoneSizeInPixels;
if (dockHeight < FancyDockingDropZoneConstants::minDockSizeBeforeDropZoneScalingInPixels)
{
dropZoneHeight = aznumeric_cast<int>(dockHeight * g_FancyDockingConstants.dropZoneScaleFactor);
dropZoneHeight = aznumeric_cast<int>(dockHeight * FancyDockingDropZoneConstants::dropZoneScaleFactor);
}
// Calculate the inner corners to be used when constructing the drop zone polygons
@ -1078,7 +1077,7 @@ namespace AzQtComponents
int innerDropZoneWidth = m_dropZoneState.innerDropZoneRect().width();
int innerDropZoneHeight = m_dropZoneState.innerDropZoneRect().height();
int centerDropZoneDiameter = (innerDropZoneWidth < innerDropZoneHeight) ? innerDropZoneWidth : innerDropZoneHeight;
centerDropZoneDiameter = aznumeric_cast<int>(centerDropZoneDiameter * g_FancyDockingConstants.centerTabDropZoneScale);
centerDropZoneDiameter = aznumeric_cast<int>(centerDropZoneDiameter * FancyDockingDropZoneConstants::centerTabDropZoneScale);
// Setup our center tab drop zone
const QSize centerDropZoneSize(centerDropZoneDiameter, centerDropZoneDiameter);
@ -1986,7 +1985,7 @@ namespace AzQtComponents
// hasn't faded in all the way yet, then ignore the drop zone area
// which will make the widget floating
bool modifiedKeyPressed = FancyDockingDropZoneWidget::CheckModifierKey();
if (modifiedKeyPressed || m_dropZoneState.dropZoneHoverOpacity() != g_FancyDockingConstants.dropZoneOpacity)
if (modifiedKeyPressed || m_dropZoneState.dropZoneHoverOpacity() != FancyDockingDropZoneConstants::dropZoneOpacity)
{
area = Qt::NoDockWidgetArea;
}
@ -3026,7 +3025,7 @@ namespace AzQtComponents
{
bool modifiedKeyPressed = FancyDockingDropZoneWidget::CheckModifierKey();
m_ghostWidget->setWindowOpacity(modifiedKeyPressed ? 1.0f : g_FancyDockingConstants.draggingDockWidgetOpacity);
m_ghostWidget->setWindowOpacity(modifiedKeyPressed ? 1.0f : FancyDockingDropZoneConstants::draggingDockWidgetOpacity);
m_ghostWidget->setPixmap(m_state.dockWidgetScreenGrab.screenGrab, m_state.placeholder(), m_state.placeholderScreen());
}
}

@ -19,26 +19,6 @@
namespace AzQtComponents
{
static const FancyDockingDropZoneConstants g_Constants;
FancyDockingDropZoneConstants::FancyDockingDropZoneConstants()
{
draggingDockWidgetOpacity = 0.6;
dropZoneOpacity = 0.4;
dropZoneSizeInPixels = 40;
minDockSizeBeforeDropZoneScalingInPixels = dropZoneSizeInPixels * 3;
dropZoneScaleFactor = 0.25;
centerTabDropZoneScale = 0.5;
centerTabIconScale = 0.5;
dropZoneColor = QColor(155, 155, 155);
dropZoneBorderColor = Qt::black;
dropZoneBorderInPixels = 1;
absoluteDropZoneSizeInPixels = 25;
dockingTargetDelayMS = 110;
dropZoneHoverFadeUpdateIntervalMS = 20;
dropZoneHoverFadeIncrement = dropZoneOpacity / (dockingTargetDelayMS / dropZoneHoverFadeUpdateIntervalMS);
centerDropZoneIconPath = QString(":/stylesheet/img/UI20/docking/tabs_icon.svg");
}
FancyDockingDropZoneWidget::FancyDockingDropZoneWidget(QMainWindow* mainWindow, QWidget* coordinatesRelativeTo, QScreen* screen, FancyDockingDropZoneState* dropZoneState)
// NOTE: this will not work with multiple monitors if this widget has a parent. The floating drop zone
@ -154,7 +134,7 @@ namespace AzQtComponents
// Draw all of the normal drop zones if they exist (if a dock widget is hovered over)
painter.setPen(Qt::NoPen);
painter.setOpacity(g_Constants.dropZoneOpacity);
painter.setOpacity(FancyDockingDropZoneConstants::dropZoneOpacity);
auto dropZones = m_dropZoneState->dropZones();
for (auto it = dropZones.cbegin(); it != dropZones.cend(); ++it)
{
@ -189,7 +169,7 @@ namespace AzQtComponents
// Otherwise, set the normal color
else
{
painter.setBrush(g_Constants.dropZoneColor);
painter.setBrush(FancyDockingDropZoneConstants::dropZoneColor);
}
// negate the window position to offset everything by that much
@ -214,8 +194,8 @@ namespace AzQtComponents
// Scale the tabs icon based on the drop zone size and our specified offset
// Doing this through QIcon to make sure that SVG is rendered already in desired resolution
const QSize& dropZoneSize = dropZoneRect.size();
const QSize requestedIconSize = dropZoneSize * g_Constants.centerTabIconScale;
const QIcon dropZoneIcon = QIcon(g_Constants.centerDropZoneIconPath);
const QSize requestedIconSize = dropZoneSize * FancyDockingDropZoneConstants::centerTabIconScale;
const QIcon dropZoneIcon = QIcon(FancyDockingDropZoneConstants::centerDropZoneIconPath);
const QPixmap dropZonePixmap = dropZoneIcon.pixmap(requestedIconSize);
const QSize receivedIconSize = dropZoneIcon.actualSize(requestedIconSize);
@ -264,7 +244,7 @@ namespace AzQtComponents
}
else
{
painter.setBrush(g_Constants.dropZoneColor);
painter.setBrush(FancyDockingDropZoneConstants::dropZoneColor);
}
painter.drawRect(absoluteDropZoneRect);
@ -313,8 +293,8 @@ namespace AzQtComponents
const QPoint innerBottomRight = innerDropZoneRect.bottomRight();
// Draw the lines using the appropriate pen
QPen dropZoneBorderPen(g_Constants.dropZoneBorderColor);
dropZoneBorderPen.setWidth(g_Constants.dropZoneBorderInPixels);
QPen dropZoneBorderPen(FancyDockingDropZoneConstants::dropZoneBorderColor);
dropZoneBorderPen.setWidth(FancyDockingDropZoneConstants::dropZoneBorderInPixels);
painter.setPen(dropZoneBorderPen);
painter.setOpacity(1);
painter.drawLine(topLeft, innerTopLeft);

@ -28,63 +28,58 @@ class QPainter;
namespace AzQtComponents
{
struct AZ_QT_COMPONENTS_API FancyDockingDropZoneConstants
namespace FancyDockingDropZoneConstants
{
// Constant for the opacity of the screen grab for the dock widget being dragged
qreal draggingDockWidgetOpacity;
static constexpr qreal draggingDockWidgetOpacity = 0.6;
// Constant for the opacity of the normal drop zones
qreal dropZoneOpacity;
static constexpr qreal dropZoneOpacity = 0.4;
// Constant for the default drop zone size (in pixels)
int dropZoneSizeInPixels;
static constexpr int dropZoneSizeInPixels = 40;
// Constant for the dock width/height size (in pixels) before we need to start
// scaling down the drop zone sizes, or else they will overlap with the center
// tab icon or each other
int minDockSizeBeforeDropZoneScalingInPixels;
static constexpr int minDockSizeBeforeDropZoneScalingInPixels = dropZoneSizeInPixels * 3;
// Constant for the factor by which we must scale down the drop zone sizes once
// the dock width/height size is too small
qreal dropZoneScaleFactor;
static constexpr qreal dropZoneScaleFactor = 0.25;
// Constant for the percentage to scale down the inner drop zone rectangle for the center tab drop zone
qreal centerTabDropZoneScale;
static constexpr qreal centerTabDropZoneScale = 0.5;
// Constant for the percentage to scale down the center tab drop zone for the center tab icon
qreal centerTabIconScale;
static constexpr qreal centerTabIconScale = 0.5;
// Constant for the drop zone hotspot default color
QColor dropZoneColor;
static const QColor dropZoneColor = QColor(155, 155, 155);
// Constant for the drop zone border color
QColor dropZoneBorderColor;
static const QColor dropZoneBorderColor = Qt::black;
// Constant for the border width in pixels separating the drop zones
int dropZoneBorderInPixels;
static constexpr int dropZoneBorderInPixels = 1;
// Constant for the border width in pixels separating the drop zones
int absoluteDropZoneSizeInPixels;
static constexpr int absoluteDropZoneSizeInPixels = 25;
// Constant for the delay (in milliseconds) before a drop zone becomes active
// once it is hovered over
int dockingTargetDelayMS;
static constexpr int dockingTargetDelayMS = 110;
// Constant for the rate at which we will update (fade in) the drop zone opacity
// when hovered over (in milliseconds)
int dropZoneHoverFadeUpdateIntervalMS;
static constexpr int dropZoneHoverFadeUpdateIntervalMS = 20;
// Constant for the incremental opacity increase for the hovered drop zone
// that will fade in to the full drop zone opacity in the desired time
qreal dropZoneHoverFadeIncrement;
static constexpr qreal dropZoneHoverFadeIncrement = dropZoneOpacity / (dockingTargetDelayMS / dropZoneHoverFadeUpdateIntervalMS);
// Constant for the path to the center drop zone tabs icon
QString centerDropZoneIconPath;
FancyDockingDropZoneConstants();
FancyDockingDropZoneConstants(const FancyDockingDropZoneConstants&) = delete;
FancyDockingDropZoneConstants& operator=(const FancyDockingDropZoneConstants&) = delete;
static const QString centerDropZoneIconPath = QStringLiteral(":/stylesheet/img/UI20/docking/tabs_icon.svg");
};
class FancyDockingDropZoneState

@ -13,8 +13,6 @@
#define AZ_TRAIT_UNIT_TEST_ENTITY_ID_GEN_TEST_COUNT 10000
#define AZ_TRAIT_UNIT_TEST_DILLER_TRIGGER_EVENT_COUNT 100000
#define AZ_TRAIT_DISABLE_ALL_SAVE_DATA_TESTS true
#define AZ_TRAIT_DISABLE_FAILED_AP_CONNECTION_TESTS true
#define AZ_TRAIT_DISABLE_FAILED_ASSET_LOAD_TESTS true

@ -30,6 +30,7 @@
#include <AzToolsFramework/ContainerEntity/ContainerEntitySystemComponent.h>
#include <AzToolsFramework/Entity/EditorEntityContextComponent.h>
#include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
#include <AzToolsFramework/Entity/ReadOnly/ReadOnlyEntitySystemComponent.h>
#include <AzToolsFramework/FocusMode/FocusModeSystemComponent.h>
#include <AzToolsFramework/Slice/SliceMetadataEntityContextComponent.h>
#include <AzToolsFramework/Prefab/PrefabSystemComponent.h>
@ -268,6 +269,7 @@ namespace AzToolsFramework
azrtti_typeid<Components::EditorEntityUiSystemComponent>(),
azrtti_typeid<FocusModeSystemComponent>(),
azrtti_typeid<ContainerEntitySystemComponent>(),
azrtti_typeid<ReadOnlyEntitySystemComponent>(),
azrtti_typeid<SliceMetadataEntityContextComponent>(),
azrtti_typeid<Prefab::PrefabSystemComponent>(),
azrtti_typeid<EditorEntityFixupComponent>(),

@ -30,6 +30,7 @@ namespace AzToolsFramework
connect(m_filterModel, &QAbstractItemModel::rowsRemoved, this, &AssetBrowserTableModel::UpdateTableModelMaps);
connect(m_filterModel, &QAbstractItemModel::layoutChanged, this, &AssetBrowserTableModel::UpdateTableModelMaps);
connect(m_filterModel, &AssetBrowserFilterModel::filterChanged, this, &AssetBrowserTableModel::beginResetModel);
connect(m_filterModel, &AssetBrowserFilterModel::filterChanged, this, &AssetBrowserTableModel::UpdateTableModelMaps);
connect(m_filterModel, &QAbstractItemModel::dataChanged, this, &AssetBrowserTableModel::SourceDataChanged);
}

@ -23,6 +23,7 @@
#include <AzToolsFramework/Entity/EditorEntityModelComponent.h>
#include <AzToolsFramework/Entity/EditorEntitySearchComponent.h>
#include <AzToolsFramework/Entity/EditorEntitySortComponent.h>
#include <AzToolsFramework/Entity/ReadOnly/ReadOnlyEntitySystemComponent.h>
#include <AzToolsFramework/FocusMode/FocusModeSystemComponent.h>
#include <AzToolsFramework/PropertyTreeEditor/PropertyTreeEditorComponent.h>
#include <AzToolsFramework/Render/EditorIntersectorComponent.h>
@ -75,6 +76,7 @@ namespace AzToolsFramework
EditorEntityFixupComponent::CreateDescriptor(),
EntityUtilityComponent::CreateDescriptor(),
ContainerEntitySystemComponent::CreateDescriptor(),
ReadOnlyEntitySystemComponent::CreateDescriptor(),
FocusModeSystemComponent::CreateDescriptor(),
SliceMetadataEntityContextComponent::CreateDescriptor(),
SliceRequestComponent::CreateDescriptor(),

@ -0,0 +1,63 @@
/*
* 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 <AzCore/Component/EntityId.h>
#include <AzCore/EBus/EBus.h>
#include <AzFramework/Entity/EntityContext.h>
namespace AzToolsFramework
{
//! Used to notify changes of state for read-only entities.
class ReadOnlyEntityPublicNotifications
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
using BusIdType = AzFramework::EntityContextId;
//////////////////////////////////////////////////////////////////////////
//! Triggered when an entity's read-only status changes.
//! @param entityId The entity whose status has changed.
//! @param readOnly The read-only state the container was changed to.
virtual void OnReadOnlyEntityStatusChanged([[maybe_unused]] const AZ::EntityId& entityId, [[maybe_unused]] bool readOnly) {}
protected:
~ReadOnlyEntityPublicNotifications() = default;
};
using ReadOnlyEntityPublicNotificationBus = AZ::EBus<ReadOnlyEntityPublicNotifications>;
//! Used by the ReadOnlyEntitySystemComponent to query the read-only state of entities as set by systems using the API.
class ReadOnlyEntityQueryRequests
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
using BusIdType = AzFramework::EntityContextId;
//////////////////////////////////////////////////////////////////////////
//! Triggered when an entity's read-only status is queried.
//! Allows multiple systems to weigh in on the read-only status of an entity.
//! @param entityId The entity whose status has changed.
//! @param[out] isReadOnly The output of the query. Should only be changed to true, and left untouched if false.
virtual void IsReadOnly(const AZ::EntityId& entityId, bool& isReadOnly) = 0;
protected:
~ReadOnlyEntityQueryRequests() = default;
};
using ReadOnlyEntityQueryRequestBus = AZ::EBus<ReadOnlyEntityQueryRequests>;
} // namespace AzToolsFramework

@ -0,0 +1,43 @@
/*
* 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 <AzCore/Interface/Interface.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Entity/EntityContextBus.h>
namespace AzToolsFramework
{
//! An entity registered as read-only cannot be altered in the editor.
class ReadOnlyEntityPublicInterface
{
public:
AZ_RTTI(ReadOnlyEntityPublicInterface, "{921FE15B-6EBD-47F0-8238-BC63318DEDEA}");
//! Returns whether the entity id provided is registered as read-only.
virtual bool IsReadOnly(const AZ::EntityId& entityId) = 0;
};
//! An entity registered as read-only cannot be altered in the editor.
class ReadOnlyEntityQueryInterface
{
public:
AZ_RTTI(ReadOnlyEntityQueryInterface, "{2ACD63C5-1F3E-4DE8-880E-8115F857D329}");
//! Refreshes the cached read-only status for the entities provided.
//! @param entityIds The entityIds whose read-only state will be queried again.
virtual void RefreshReadOnlyState(const EntityIdList& entityIds) = 0;
//! Refreshes the cached read-only status for all entities.
//! Useful when disconnecting a handler at runtime.
virtual void RefreshReadOnlyStateForAllEntities() = 0;
};
} // namespace AzToolsFramework

@ -0,0 +1,99 @@
/*
* 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 <AzToolsFramework/Entity/ReadOnly/ReadOnlyEntitySystemComponent.h>
#include <AzToolsFramework/Entity/ReadOnly/ReadOnlyEntityBus.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
namespace AzToolsFramework
{
void ReadOnlyEntitySystemComponent::Activate()
{
AZ::Interface<ReadOnlyEntityQueryInterface>::Register(this);
AZ::Interface<ReadOnlyEntityPublicInterface>::Register(this);
EditorEntityContextNotificationBus::Handler::BusConnect();
}
void ReadOnlyEntitySystemComponent::Deactivate()
{
EditorEntityContextNotificationBus::Handler::BusDisconnect();
AZ::Interface<ReadOnlyEntityPublicInterface>::Unregister(this);
AZ::Interface<ReadOnlyEntityQueryInterface>::Unregister(this);
}
void ReadOnlyEntitySystemComponent::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<ReadOnlyEntitySystemComponent, AZ::Component>()->Version(1);
}
}
void ReadOnlyEntitySystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC_CE("ReadOnlyEntityService"));
}
bool ReadOnlyEntitySystemComponent::IsReadOnly(const AZ::EntityId& entityId)
{
if (!m_readOnlystates.contains(entityId))
{
QueryReadOnlyStateForEntity(entityId);
}
return m_readOnlystates[entityId];
}
void ReadOnlyEntitySystemComponent::RefreshReadOnlyState(const EntityIdList& entityIds)
{
for (const AZ::EntityId entityId : entityIds)
{
bool wasReadOnly = m_readOnlystates[entityId];
QueryReadOnlyStateForEntity(entityId);
if (bool isReadOnly = m_readOnlystates[entityId]; wasReadOnly != isReadOnly)
{
ReadOnlyEntityPublicNotificationBus::Broadcast(
&ReadOnlyEntityPublicNotificationBus::Events::OnReadOnlyEntityStatusChanged, entityId, isReadOnly);
}
}
}
void ReadOnlyEntitySystemComponent::RefreshReadOnlyStateForAllEntities()
{
for (auto elem : m_readOnlystates)
{
AZ::EntityId entityId = elem.first;
bool wasReadOnly = m_readOnlystates[entityId];
QueryReadOnlyStateForEntity(entityId);
if (bool isReadOnly = m_readOnlystates[entityId]; wasReadOnly != isReadOnly)
{
ReadOnlyEntityPublicNotificationBus::Broadcast(
&ReadOnlyEntityPublicNotificationBus::Events::OnReadOnlyEntityStatusChanged, entityId, isReadOnly);
}
}
}
void ReadOnlyEntitySystemComponent::OnContextReset()
{
m_readOnlystates.clear();
}
void ReadOnlyEntitySystemComponent::QueryReadOnlyStateForEntity(const AZ::EntityId& entityId)
{
bool isReadOnly = false;
ReadOnlyEntityQueryRequestBus::Broadcast(
&ReadOnlyEntityQueryRequestBus::Events::IsReadOnly, entityId, isReadOnly);
m_readOnlystates[entityId] = isReadOnly;
}
} // namespace AzToolsFramework

@ -0,0 +1,56 @@
/*
* 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 <AzCore/Component/Component.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Entity/ReadOnly/ReadOnlyEntityInterface.h>
namespace AzToolsFramework
{
//! System Component to track read-only entity registration.
//! An entity registered as ReadOnly cannot be altered in the Editor.
class ReadOnlyEntitySystemComponent final
: public AZ::Component
, private ReadOnlyEntityPublicInterface
, private ReadOnlyEntityQueryInterface
, private EditorEntityContextNotificationBus::Handler
{
public:
AZ_COMPONENT(ReadOnlyEntitySystemComponent, "{B32EB03F-D88F-4B3A-9C16-071AF04DA646}");
ReadOnlyEntitySystemComponent() = default;
virtual ~ReadOnlyEntitySystemComponent() = default;
// AZ::Component overrides ...
void Activate() override;
void Deactivate() override;
static void Reflect(AZ::ReflectContext* context);
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
// ReadOnlyEntityPublicNotifications overrides ...
bool IsReadOnly(const AZ::EntityId& entityId) override;
// ReadOnlyEntityQueryInterface overrides ...
void RefreshReadOnlyState(const EntityIdList& entityIds) override;
void RefreshReadOnlyStateForAllEntities() override;
// EditorEntityContextNotificationBus overrides ...
void OnContextReset() override;
private:
void QueryReadOnlyStateForEntity(const AZ::EntityId& entityId);
AZStd::unordered_map<AZ::EntityId, bool> m_readOnlystates;
};
} // namespace AzToolsFramework

@ -84,12 +84,12 @@ namespace AzToolsFramework
break;
case State::Translating:
{
if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() &&
if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Middle() &&
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down &&
mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Shift() &&
mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Ctrl())
{
SnapVerticesToTerrain(mouseInteraction);
SnapVerticesToSurface(mouseInteraction);
return true;
}
@ -109,29 +109,23 @@ namespace AzToolsFramework
}
template<typename Vertex>
void EditorVertexSelectionBase<Vertex>::SnapVerticesToTerrain(const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
void EditorVertexSelectionBase<Vertex>::SnapVerticesToSurface(const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
{
ScopedUndoBatch surfaceSnapUndo("Snap to Surface");
ScopedUndoBatch::MarkEntityDirty(GetEntityId());
const int viewportId = mouseInteraction.m_mouseInteraction.m_interactionId.m_viewportId;
// get unsnapped terrain position (world space)
AZ::Vector3 worldSurfacePosition = AZ::Vector3::CreateZero();
;
ViewportInteraction::MainEditorViewportInteractionRequestBus::EventResult(
worldSurfacePosition, viewportId, &ViewportInteraction::MainEditorViewportInteractionRequestBus::Events::PickTerrain,
mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates);
// get unsnapped surface position (world space)
const AZ::Vector3 worldSurfacePosition = FindClosestPickIntersection(
viewportId, mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates, EditorPickRayLength,
GetDefaultEntityPlacementDistance());
AZ::Transform worldFromLocal;
AZ::TransformBus::EventResult(worldFromLocal, GetEntityId(), &AZ::TransformBus::Events::GetWorldTM);
const AZ::Transform localFromWorld = worldFromLocal.GetInverse();
// convert to local space - snap if enabled
const GridSnapParameters gridSnapParams = GridSnapSettings(viewportId);
const AZ::Vector3 localFinalSurfacePosition = gridSnapParams.m_gridSnap
? CalculateSnappedTerrainPosition(worldSurfacePosition, worldFromLocal, viewportId, gridSnapParams.m_gridSize)
: localFromWorld.TransformPoint(worldSurfacePosition);
// convert to local space
const AZ::Vector3 localFinalSurfacePosition = localFromWorld.TransformPoint(worldSurfacePosition);
SetSelectedPosition(localFinalSurfacePosition);
OnEntityComponentPropertyChanged(GetEntityComponentIdPair());

@ -171,7 +171,7 @@ namespace AzToolsFramework
//! Snap the selected vertices to the terrain.
//! Note: With a multi-selection the manipulator will be translated to the picked
//! terrain position with all vertices moved relative to it.
void SnapVerticesToTerrain(const ViewportInteraction::MouseInteractionEvent& mouseInteraction);
void SnapVerticesToSurface(const ViewportInteraction::MouseInteractionEvent& mouseInteraction);
//! The Actions provided by the EditorVertexSelection while it is active.
//! e.g. Vertex deletion, duplication etc.

@ -110,30 +110,6 @@ namespace AzToolsFramework
return unsnappedPosition + CalculateSnappedOffset(unsnappedPosition, snapAxes, snapAxesCount, size);
}
AZ::Vector3 CalculateSnappedTerrainPosition(
const AZ::Vector3& worldSurfacePosition, const AZ::Transform& worldFromLocal, const int viewportId, const float size)
{
const AZ::Transform localFromWorld = worldFromLocal.GetInverse();
const AZ::Vector3 localSurfacePosition = localFromWorld.TransformPoint(worldSurfacePosition);
// snap in xy plane
AZ::Vector3 localSnappedSurfacePosition = localSurfacePosition +
CalculateSnappedOffset(localSurfacePosition, AZ::Vector3::CreateAxisX(), size) +
CalculateSnappedOffset(localSurfacePosition, AZ::Vector3::CreateAxisY(), size);
// find terrain height at xy snapped location
float terrainHeight = 0.0f;
ViewportInteraction::MainEditorViewportInteractionRequestBus::EventResult(
terrainHeight, viewportId, &ViewportInteraction::MainEditorViewportInteractionRequestBus::Events::TerrainHeight,
Vector3ToVector2(worldFromLocal.TransformPoint(localSnappedSurfacePosition)));
// set snapped z value to terrain height
AZ::Vector3 localTerrainHeight = localFromWorld.TransformPoint(AZ::Vector3(0.0f, 0.0f, terrainHeight));
localSnappedSurfacePosition.SetZ(localTerrainHeight.GetZ());
return localSnappedSurfacePosition;
}
bool GridSnapping(const int viewportId)
{
bool snapping = false;

@ -63,11 +63,6 @@ namespace AzToolsFramework
AZ::Vector3 CalculateSnappedPosition(
const AZ::Vector3& unsnappedPosition, const AZ::Vector3* snapAxes, size_t snapAxesCount, float size);
//! For a given point on the terrain, calculate the closest xy position snapped to the grid
//! (z position is aligned to terrain height, not snapped to z grid)
AZ::Vector3 CalculateSnappedTerrainPosition(
const AZ::Vector3& worldSurfacePosition, const AZ::Transform& worldFromLocal, int viewportId, float size);
//! Wrapper for grid snapping and grid size bus calls.
GridSnapParameters GridSnapSettings(int viewportId);

@ -10,6 +10,7 @@
#include <AzToolsFramework/Manipulators/ManipulatorSnapping.h>
#include <AzToolsFramework/Manipulators/ManipulatorView.h>
#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
namespace AzToolsFramework
{
@ -17,28 +18,16 @@ namespace AzToolsFramework
const AZ::Transform& worldFromLocal,
const AZ::Vector3& worldSurfacePosition,
const AZ::Vector3& localStartPosition,
const bool snapping,
const float gridSize,
const int viewportId)
[[maybe_unused]] const bool snapping,
[[maybe_unused]] const float gridSize,
[[maybe_unused]] const int viewportId)
{
const AZ::Transform worldFromLocalUniform = AzToolsFramework::TransformUniformScale(worldFromLocal);
const AZ::Transform localFromWorldUniform = worldFromLocalUniform.GetInverse();
const AZ::Vector3 localFinalSurfacePosition = snapping
// note: gridSize is not scaled by scaleRecip here as localStartPosition is
// unscaled itself so the position returned by CalculateSnappedTerrainPosition
// must be in the same space (if localStartPosition were also scaled, gridSize
// would need to be multiplied by scaleRecip)
? CalculateSnappedTerrainPosition(worldSurfacePosition, worldFromLocalUniform, viewportId, gridSize)
: localFromWorldUniform.TransformPoint(worldSurfacePosition);
// delta/offset between initial vertex position and terrain pick position
const AZ::Vector3 localSurfaceOffset = localFinalSurfacePosition - localStartPosition;
const AZ::Transform localFromWorld = worldFromLocal.GetInverse();
StartInternal startInternal;
startInternal.m_snapOffset = localSurfaceOffset;
startInternal.m_localPosition = localStartPosition + localSurfaceOffset;
startInternal.m_localHitPosition = localFromWorldUniform.TransformVector(worldSurfacePosition);
startInternal.m_snapOffset = AZ::Vector3::CreateZero();
startInternal.m_localPosition = localStartPosition;
startInternal.m_localHitPosition = localFromWorld.TransformPoint(worldSurfacePosition);
return startInternal;
}
@ -46,26 +35,19 @@ namespace AzToolsFramework
const StartInternal& startInternal,
const AZ::Transform& worldFromLocal,
const AZ::Vector3& worldSurfacePosition,
const bool snapping,
const float gridSize,
[[maybe_unused]] const bool snapping,
[[maybe_unused]] const float gridSize,
const ViewportInteraction::KeyboardModifiers keyboardModifiers,
const int viewportId)
[[maybe_unused]] const int viewportId)
{
const AZ::Transform worldFromLocalUniform = AzToolsFramework::TransformUniformScale(worldFromLocal);
const AZ::Transform localFromWorldUniform = worldFromLocalUniform.GetInverse();
const float scaleRecip = ScaleReciprocal(worldFromLocalUniform);
const AZ::Vector3 localFinalSurfacePosition = snapping
? CalculateSnappedTerrainPosition(worldSurfacePosition, worldFromLocalUniform, viewportId, gridSize * scaleRecip)
: localFromWorldUniform.TransformPoint(worldSurfacePosition);
const AZ::Transform localFromWorld = worldFromLocal.GetInverse();
const AZ::Vector3 localFinalSurfacePosition = localFromWorld.TransformPoint(worldSurfacePosition);
Action action;
action.m_start.m_localPosition = startInternal.m_localPosition;
action.m_start.m_snapOffset = startInternal.m_snapOffset;
action.m_start.m_snapOffset = AZ::Vector3::CreateZero();
action.m_current.m_localOffset = localFinalSurfacePosition - startInternal.m_localPosition;
// record what modifier keys are held during this action
action.m_modifiers = keyboardModifiers;
action.m_modifiers = keyboardModifiers; // record what modifier keys are held during this action
return action;
}
@ -78,12 +60,16 @@ namespace AzToolsFramework
{
SetSpace(worldFromLocal);
AttachLeftMouseDownImpl();
// only cast rays against objects (entities/meshes etc.) we can actually see
m_rayRequest.m_onlyVisible = true;
}
void SurfaceManipulator::InstallLeftMouseDownCallback(const MouseActionCallback& onMouseDownCallback)
{
m_onLeftMouseDownCallback = onMouseDownCallback;
}
void SurfaceManipulator::InstallLeftMouseUpCallback(const MouseActionCallback& onMouseUpCallback)
{
m_onLeftMouseUpCallback = onMouseUpCallback;
@ -95,17 +81,30 @@ namespace AzToolsFramework
}
void SurfaceManipulator::OnLeftMouseDownImpl(
const ViewportInteraction::MouseInteraction& interaction, float /*rayIntersectionDistance*/)
const ViewportInteraction::MouseInteraction& interaction, [[maybe_unused]] float rayIntersectionDistance)
{
const AZ::Transform worldFromLocalUniformScale = TransformUniformScale(GetSpace());
const GridSnapParameters gridSnapParams = GridSnapSettings(interaction.m_interactionId.m_viewportId);
const AzFramework::ViewportId viewportId = interaction.m_interactionId.m_viewportId;
const auto& entityComponentIdPairs = EntityComponentIdPairs();
m_rayRequest.m_entityFilter.m_ignoreEntities.clear();
m_rayRequest.m_entityFilter.m_ignoreEntities.reserve(entityComponentIdPairs.size());
AZStd::transform(
entityComponentIdPairs.begin(), entityComponentIdPairs.end(),
AZStd::inserter(m_rayRequest.m_entityFilter.m_ignoreEntities, m_rayRequest.m_entityFilter.m_ignoreEntities.begin()),
[](const AZ::EntityComponentIdPair& entityComponentIdPair)
{
return entityComponentIdPair.GetEntityId();
});
// calculate the start and end of the ray
RefreshRayRequest(
m_rayRequest, ViewportInteraction::ViewportScreenToWorldRay(viewportId, interaction.m_mousePick.m_screenCoordinates),
EditorPickRayLength);
AZ::Vector3 worldSurfacePosition;
ViewportInteraction::MainEditorViewportInteractionRequestBus::EventResult(
worldSurfacePosition, interaction.m_interactionId.m_viewportId,
&ViewportInteraction::MainEditorViewportInteractionRequestBus::Events::PickTerrain,
interaction.m_mousePick.m_screenCoordinates);
const GridSnapParameters gridSnapParams = GridSnapSettings(viewportId);
const AZ::Vector3 worldSurfacePosition = FindClosestPickIntersection(m_rayRequest, GetDefaultEntityPlacementDistance());
m_startInternal = CalculateManipulationDataStart(
worldFromLocalUniformScale, worldSurfacePosition, GetLocalPosition(), gridSnapParams.m_gridSnap, gridSnapParams.m_gridSize,
@ -123,17 +122,14 @@ namespace AzToolsFramework
{
if (m_onLeftMouseUpCallback)
{
AZ::Vector3 worldSurfacePosition = AZ::Vector3::CreateZero();
ViewportInteraction::MainEditorViewportInteractionRequestBus::EventResult(
worldSurfacePosition, interaction.m_interactionId.m_viewportId,
&ViewportInteraction::MainEditorViewportInteractionRequestBus::Events::PickTerrain,
interaction.m_mousePick.m_screenCoordinates);
const AzFramework::ViewportId viewportId = interaction.m_interactionId.m_viewportId;
const GridSnapParameters gridSnapParams = GridSnapSettings(interaction.m_interactionId.m_viewportId);
const GridSnapParameters gridSnapParams = GridSnapSettings(viewportId);
const AZ::Vector3 worldSurfacePosition = FindClosestPickIntersection(m_rayRequest, GetDefaultEntityPlacementDistance());
m_onLeftMouseUpCallback(CalculateManipulationDataAction(
m_startInternal, TransformUniformScale(GetSpace()), worldSurfacePosition, gridSnapParams.m_gridSnap,
gridSnapParams.m_gridSize, interaction.m_keyboardModifiers, interaction.m_interactionId.m_viewportId));
gridSnapParams.m_gridSize, interaction.m_keyboardModifiers, viewportId));
}
}
@ -141,13 +137,15 @@ namespace AzToolsFramework
{
if (m_onMouseMoveCallback)
{
AZ::Vector3 worldSurfacePosition = AZ::Vector3::CreateZero();
ViewportInteraction::MainEditorViewportInteractionRequestBus::EventResult(
worldSurfacePosition, interaction.m_interactionId.m_viewportId,
&ViewportInteraction::MainEditorViewportInteractionRequestBus::Events::PickTerrain,
interaction.m_mousePick.m_screenCoordinates);
const AzFramework::ViewportId viewportId = interaction.m_interactionId.m_viewportId;
// update the start and end of the ray
RefreshRayRequest(
m_rayRequest, ViewportInteraction::ViewportScreenToWorldRay(viewportId, interaction.m_mousePick.m_screenCoordinates),
EditorPickRayLength);
const GridSnapParameters gridSnapParams = GridSnapSettings(interaction.m_interactionId.m_viewportId);
const GridSnapParameters gridSnapParams = GridSnapSettings(viewportId);
const AZ::Vector3 worldSurfacePosition = FindClosestPickIntersection(m_rayRequest, GetDefaultEntityPlacementDistance());
m_onMouseMoveCallback(CalculateManipulationDataAction(
m_startInternal, TransformUniformScale(GetSpace()), worldSurfacePosition, gridSnapParams.m_gridSnap,

@ -11,6 +11,7 @@
#include "BaseManipulator.h"
#include <AzCore/Memory/SystemAllocator.h>
#include <AzFramework/Render/GeometryIntersectionStructures.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
namespace AzToolsFramework
@ -58,10 +59,12 @@ namespace AzToolsFramework
Start m_start;
Current m_current;
ViewportInteraction::KeyboardModifiers m_modifiers;
AZ::Vector3 LocalPosition() const
{
return m_start.m_localPosition + m_current.m_localOffset;
}
AZ::Vector3 LocalPositionOffset() const
{
return m_current.m_localOffset;
@ -106,6 +109,9 @@ namespace AzToolsFramework
MouseActionCallback m_onLeftMouseUpCallback = nullptr;
MouseActionCallback m_onMouseMoveCallback = nullptr;
//! Cached ray request initialized at mouse down and updated during mouse move.
AzFramework::RenderGeometry::RayRequest m_rayRequest;
static StartInternal CalculateManipulationDataStart(
const AZ::Transform& worldFromLocal,
const AZ::Vector3& worldSurfacePosition,

@ -34,10 +34,12 @@ namespace AzToolsFramework::Prefab
PrefabPublicNotificationBus::Handler::BusConnect();
AZ::Interface<PrefabFocusInterface>::Register(this);
AZ::Interface<PrefabFocusPublicInterface>::Register(this);
PrefabFocusPublicRequestBus::Handler::BusConnect();
}
PrefabFocusHandler::~PrefabFocusHandler()
{
PrefabFocusPublicRequestBus::Handler::BusDisconnect();
AZ::Interface<PrefabFocusPublicInterface>::Unregister(this);
AZ::Interface<PrefabFocusInterface>::Unregister(this);
PrefabPublicNotificationBus::Handler::BusDisconnect();
@ -45,6 +47,18 @@ namespace AzToolsFramework::Prefab
EditorEntityInfoNotificationBus::Handler::BusDisconnect();
}
void PrefabFocusHandler::Reflect(AZ::ReflectContext* context)
{
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context); behaviorContext)
{
behaviorContext->EBus<PrefabFocusPublicRequestBus>("PrefabFocusPublicRequestBus")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Category, "Prefab")
->Attribute(AZ::Script::Attributes::Module, "prefab")
->Event("FocusOnOwningPrefab", &PrefabFocusPublicInterface::FocusOnOwningPrefab);
}
}
void PrefabFocusHandler::InitializeEditorInterfaces()
{
m_containerEntityInterface = AZ::Interface<ContainerEntityInterface>::Get();

@ -30,8 +30,8 @@ namespace AzToolsFramework::Prefab
//! Handles Prefab Focus mode, determining which prefab file entity changes will target.
class PrefabFocusHandler final
: private PrefabFocusInterface
, private PrefabFocusPublicInterface
: public PrefabFocusPublicRequestBus::Handler
, private PrefabFocusInterface
, private PrefabPublicNotificationBus::Handler
, private EditorEntityContextNotificationBus::Handler
, private EditorEntityInfoNotificationBus::Handler
@ -42,13 +42,15 @@ namespace AzToolsFramework::Prefab
PrefabFocusHandler();
~PrefabFocusHandler();
static void Reflect(AZ::ReflectContext* context);
// PrefabFocusInterface overrides ...
void InitializeEditorInterfaces() override;
PrefabFocusOperationResult FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) override;
TemplateId GetFocusedPrefabTemplateId(AzFramework::EntityContextId entityContextId) const override;
InstanceOptionalReference GetFocusedPrefabInstance(AzFramework::EntityContextId entityContextId) const override;
// PrefabFocusPublicInterface overrides ...
// PrefabFocusPublicInterface and PrefabFocusPublicRequestBus overrides ...
PrefabFocusOperationResult FocusOnOwningPrefab(AZ::EntityId entityId) override;
PrefabFocusOperationResult FocusOnParentOfFocusedPrefab(AzFramework::EntityContextId entityContextId) override;
PrefabFocusOperationResult FocusOnPathIndex(AzFramework::EntityContextId entityContextId, int index) override;

@ -58,4 +58,18 @@ namespace AzToolsFramework::Prefab
virtual const int GetPrefabFocusPathLength(AzFramework::EntityContextId entityContextId) const = 0;
};
/**
* The primary purpose of this bus is to facilitate writing automated tests for prefab focus mode.
* If you would like to integrate prefabs focus mode into your system, please call PrefabFocusPublicInterface
* for better performance.
*/
class PrefabFocusPublicRequests
: public AZ::EBusTraits
{
public:
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
};
using PrefabFocusPublicRequestBus = AZ::EBus<PrefabFocusPublicInterface, PrefabFocusPublicRequests>;
} // namespace AzToolsFramework::Prefab

@ -60,6 +60,7 @@ namespace AzToolsFramework
AzToolsFramework::Prefab::PrefabConversionUtils::PrefabCatchmentProcessor::Reflect(context);
AzToolsFramework::Prefab::PrefabConversionUtils::EditorInfoRemover::Reflect(context);
PrefabPublicRequestHandler::Reflect(context);
PrefabFocusHandler::Reflect(context);
PrefabLoader::Reflect(context);
PrefabSystemScriptingHandler::Reflect(context);

@ -169,6 +169,10 @@ namespace AzToolsFramework
{
AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast(&PropertyEditorGUIMessages::Bus::Events::RequestWrite, newCtrl);
});
this->connect(newCtrl, &PropertyControl::editingFinished, this, [newCtrl]()
{
AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast(&PropertyEditorGUIMessages::Bus::Handler::OnEditingFinished, newCtrl);
});
// note: Qt automatically disconnects objects from each other when either end is destroyed, no need to worry about delete.
// Set the value range to that of ValueType as clamped to the range of QtWidgetValueType

@ -23,11 +23,11 @@ namespace AzToolsFramework
/// @name Reverse URLs.
/// Used to identify common actions and override them when necessary.
//@{
static const AZ::Crc32 s_backAction = AZ_CRC("com.o3de.action.common.back", 0xd772a2af);
static const AZ::Crc32 s_deleteAction = AZ_CRC("com.o3de.action.common.delete", 0x5731f6cb);
static const AZ::Crc32 s_duplicateAction = AZ_CRC("com.o3de.action.common.duplicate", 0x08ccf461);
static const AZ::Crc32 s_nextComponentMode = AZ_CRC("com.o3de.action.common.nextComponentMode", 0xcc26094f);
static const AZ::Crc32 s_previousComponentMode = AZ_CRC("com.o3de.action.common.previousComponentMode", 0x0d18ff39);
static const AZ::Crc32 s_backAction = AZ_CRC("com.o3de.action.common.back", 0x80c3030f);
static const AZ::Crc32 s_deleteAction = AZ_CRC("com.o3de.action.common.delete", 0x58e78eed);
static const AZ::Crc32 s_duplicateAction = AZ_CRC("com.o3de.action.common.duplicate", 0xbc5a4a23);
static const AZ::Crc32 s_nextComponentMode = AZ_CRC("com.o3de.action.common.nextComponentMode", 0xf9aca3a8);
static const AZ::Crc32 s_previousComponentMode = AZ_CRC("com.o3de.action.common.previousComponentMode", 0x0580eaec);
//@}
/// Specific Action properties to be sent to a type implementing

@ -64,23 +64,12 @@ namespace AzToolsFramework
return circleBoundWidth;
}
AZ::Vector3 FindClosestPickIntersection(
AzFramework::ViewportId viewportId, const AzFramework::ScreenPoint& screenPoint, const float rayLength, const float defaultDistance)
AZ::Vector3 FindClosestPickIntersection(const AzFramework::RenderGeometry::RayRequest& rayRequest, const float defaultDistance)
{
using AzToolsFramework::ViewportInteraction::ViewportInteractionRequestBus;
AzToolsFramework::ViewportInteraction::ProjectedViewportRay viewportRay{};
ViewportInteractionRequestBus::EventResult(
viewportRay, viewportId, &ViewportInteractionRequestBus::Events::ViewportScreenToWorldRay, screenPoint);
AzFramework::RenderGeometry::RayRequest ray;
ray.m_startWorldPosition = viewportRay.origin;
ray.m_endWorldPosition = viewportRay.origin + viewportRay.direction * rayLength;
ray.m_onlyVisible = true;
AzFramework::RenderGeometry::RayResult renderGeometryIntersectionResult;
AzFramework::RenderGeometry::IntersectorBus::EventResult(
renderGeometryIntersectionResult, AzToolsFramework::GetEntityContextId(),
&AzFramework::RenderGeometry::IntersectorBus::Events::RayIntersect, ray);
&AzFramework::RenderGeometry::IntersectorBus::Events::RayIntersect, rayRequest);
// attempt a ray intersection with any visible mesh and return the intersection position if successful
if (renderGeometryIntersectionResult)
@ -89,7 +78,32 @@ namespace AzToolsFramework
}
else
{
return viewportRay.origin + viewportRay.direction * defaultDistance;
const AZ::Vector3 rayDirection = (rayRequest.m_endWorldPosition - rayRequest.m_startWorldPosition).GetNormalized();
return rayRequest.m_startWorldPosition + rayDirection * defaultDistance;
}
}
void RefreshRayRequest(
AzFramework::RenderGeometry::RayRequest& rayRequest,
const ViewportInteraction::ProjectedViewportRay& viewportRay,
const float rayLength)
{
AZ_Assert(rayLength > 0.0f, "Invalid ray length passed to RefreshRayRequest");
rayRequest.m_startWorldPosition = viewportRay.origin;
rayRequest.m_endWorldPosition = viewportRay.origin + viewportRay.direction * rayLength;
}
AZ::Vector3 FindClosestPickIntersection(
const AzFramework::ViewportId viewportId,
const AzFramework::ScreenPoint& screenPoint,
const float rayLength,
const float defaultDistance)
{
AzFramework::RenderGeometry::RayRequest ray;
ray.m_onlyVisible = true; // only consider visible objects
RefreshRayRequest(ray, ViewportInteraction::ViewportScreenToWorldRay(viewportId, screenPoint), rayLength);
return FindClosestPickIntersection(ray, defaultDistance);
}
} // namespace AzToolsFramework

@ -15,13 +15,19 @@
#include <AzFramework/Viewport/CameraState.h>
#include <AzFramework/Viewport/ClickDetector.h>
#include <AzFramework/Viewport/ViewportId.h>
#include <AzFramework/Viewport/ViewportScreen.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Viewport/ViewportTypes.h>
namespace AzFramework
{
struct ScreenPoint;
}
namespace RenderGeometry
{
struct RayRequest;
}
} // namespace AzFramework
namespace AzToolsFramework
{
@ -177,6 +183,26 @@ namespace AzToolsFramework
//! Type to inherit to implement ViewportInteractionRequests.
using ViewportInteractionRequestBus = AZ::EBus<ViewportInteractionRequests, ViewportEBusTraits>;
//! Utility function to return a viewport ray.
inline ProjectedViewportRay ViewportScreenToWorldRay(
const AzFramework::CameraState& cameraState, const AzFramework::ScreenPoint& screenPoint)
{
const AZ::Vector3 rayOrigin = AzFramework::ScreenToWorld(screenPoint, cameraState);
const AZ::Vector3 rayDirection = (rayOrigin - cameraState.m_position).GetNormalized();
return AzToolsFramework::ViewportInteraction::ProjectedViewportRay{ rayOrigin, rayDirection };
}
//! Utility function to return a viewport ray using the ViewportInteractionRequestBus.
inline ProjectedViewportRay ViewportScreenToWorldRay(
const AzFramework::ViewportId viewportId, const AzFramework::ScreenPoint& screenPoint)
{
ProjectedViewportRay viewportRay{};
ViewportInteractionRequestBus::EventResult(
viewportRay, viewportId, &ViewportInteractionRequestBus::Events::ViewportScreenToWorldRay, screenPoint);
return viewportRay;
}
//! Interface to return only viewport specific settings (e.g. snapping).
class ViewportSettingsRequests
{
@ -228,10 +254,6 @@ namespace AzToolsFramework
class MainEditorViewportInteractionRequests
{
public:
//! Given a point in screen space, return the terrain position in world space.
virtual AZ::Vector3 PickTerrain(const AzFramework::ScreenPoint& point) = 0;
//! Return the terrain height given a world position in 2d (xy plane).
virtual float TerrainHeight(const AZ::Vector2& position) = 0;
//! Is the user holding a modifier key to move the manipulator space from local to world.
virtual bool ShowingWorldSpace() = 0;
//! Return the widget to use as the parent for the viewport context menu.
@ -337,9 +359,18 @@ namespace AzToolsFramework
//! Performs an intersection test against meshes in the scene, if there is a hit (the ray intersects
//! a mesh), that position is returned, otherwise a point projected defaultDistance from the
//! origin of the ray will be returned.
//! @note The intersection will only consider visible objects.
AZ::Vector3 FindClosestPickIntersection(
AzFramework::ViewportId viewportId, const AzFramework::ScreenPoint& screenPoint, float rayLength, float defaultDistance);
//! Overload of FindClosestPickIntersection taking a RenderGeometry::RayRequest directly.
//! @note rayRequest must contain a valid ray/line segment (start/endWorldPosition must not be at the same position).
AZ::Vector3 FindClosestPickIntersection(const AzFramework::RenderGeometry::RayRequest& rayRequest, float defaultDistance);
//! Update the in/out parameter rayRequest based on the latest viewport ray.
void RefreshRayRequest(
AzFramework::RenderGeometry::RayRequest& rayRequest, const ViewportInteraction::ProjectedViewportRay& viewportRay, float rayLength);
//! Maps a mouse interaction event to a ClickDetector event.
//! @note Function only cares about up or down events, all other events are mapped to Nil (ignored).
AzFramework::ClickDetector::ClickEvent ClickDetectorEventFromViewportInteraction(

@ -9,6 +9,7 @@
#include "EditorSelectionUtil.h"
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/Math/Aabb.h>
#include <AzCore/Math/IntersectSegment.h>
@ -16,8 +17,21 @@
#include <AzToolsFramework/API/ComponentEntitySelectionBus.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
AZ_CVAR(
float,
ed_defaultEntityPlacementDistance,
10.0f,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"The default distance to place an entity from the camera if no intersection is found");
namespace AzToolsFramework
{
float GetDefaultEntityPlacementDistance()
{
return ed_defaultEntityPlacementDistance;
}
AZ::Vector3 CalculateCenterOffset(const AZ::EntityId entityId, const EditorTransformComponentSelectionRequests::Pivot pivot)
{
if (Centered(pivot))

@ -60,6 +60,9 @@ namespace AzToolsFramework
//! Wrapper for EBus call to return the DPI scaling for a given viewport.
float GetScreenDisplayScaling(int viewportId);
//! The default distance an entity is placed from the camera if there is no intersection.
float GetDefaultEntityPlacementDistance();
//! A utility to return the center of several points.
//! Take several positions and store the min and max of each in
//! turn - when all points have been added return the center/midpoint.

@ -231,8 +231,8 @@ namespace AzToolsFramework
{
return mouseInteraction.m_mouseInteraction.m_mouseButtons.Middle() &&
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down &&
mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Ctrl() &&
!mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Alt();
!mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Alt() &&
mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Ctrl();
}
static bool IndividualDitto(const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
@ -243,12 +243,12 @@ namespace AzToolsFramework
mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Ctrl();
}
static bool SnapTerrain(const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
static bool SnapSurface(const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
{
return mouseInteraction.m_mouseInteraction.m_mouseButtons.Middle() &&
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down &&
(mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Alt() ||
mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Ctrl());
mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Shift() &&
mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Ctrl();
}
static bool ManipulatorDitto(
@ -923,26 +923,6 @@ namespace AzToolsFramework
}
}
static AZ::Vector3 PickTerrainPosition(const ViewportInteraction::MouseInteraction& mouseInteraction)
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
const int viewportId = mouseInteraction.m_interactionId.m_viewportId;
// get unsnapped terrain position (world space)
AZ::Vector3 worldSurfacePosition;
ViewportInteraction::MainEditorViewportInteractionRequestBus::EventResult(
worldSurfacePosition, viewportId, &ViewportInteraction::MainEditorViewportInteractionRequestBus::Events::PickTerrain,
mouseInteraction.m_mousePick.m_screenCoordinates);
// convert to local space - snap if enabled
const GridSnapParameters gridSnapParams = GridSnapSettings(viewportId);
const AZ::Vector3 finalSurfacePosition = gridSnapParams.m_gridSnap
? CalculateSnappedTerrainPosition(worldSurfacePosition, AZ::Transform::CreateIdentity(), viewportId, gridSnapParams.m_gridSize)
: worldSurfacePosition;
return finalSurfacePosition;
}
// is the passed entity id contained with in the entity id list
template<typename EntityIdContainer>
static bool IsEntitySelectedInternal(AZ::EntityId entityId, const EntityIdContainer& selectedEntityIds)
@ -1286,7 +1266,7 @@ namespace AzToolsFramework
m_axisPreview.m_translation = m_entityIdManipulators.m_manipulators->GetLocalTransform().GetTranslation();
m_axisPreview.m_orientation = QuaternionFromTransformNoScaling(m_entityIdManipulators.m_manipulators->GetLocalTransform());
// [ref 1.]
// see comment [ref 1.] above
BeginRecordManipulatorCommand();
});
@ -1321,7 +1301,7 @@ namespace AzToolsFramework
m_axisPreview.m_translation = m_entityIdManipulators.m_manipulators->GetLocalTransform().GetTranslation();
m_axisPreview.m_orientation = QuaternionFromTransformNoScaling(m_entityIdManipulators.m_manipulators->GetLocalTransform());
// [ref 1.]
// see comment [ref 1.] above
BeginRecordManipulatorCommand();
});
@ -1343,6 +1323,39 @@ namespace AzToolsFramework
EndRecordManipulatorCommand();
});
// surface
translationManipulators->InstallSurfaceManipulatorMouseDownCallback(
[this, manipulatorEntityIds]([[maybe_unused]] const SurfaceManipulator::Action& action)
{
BuildSortedEntityIdVectorFromEntityIdMap(m_entityIdManipulators.m_lookups, manipulatorEntityIds->m_entityIds);
InitializeTranslationLookup(m_entityIdManipulators);
m_axisPreview.m_translation = m_entityIdManipulators.m_manipulators->GetLocalTransform().GetTranslation();
m_axisPreview.m_orientation = QuaternionFromTransformNoScaling(m_entityIdManipulators.m_manipulators->GetLocalTransform());
// see comment [ref 1.] above
BeginRecordManipulatorCommand();
});
translationManipulators->InstallSurfaceManipulatorMouseMoveCallback(
[this, prevModifiers, manipulatorEntityIds](const SurfaceManipulator::Action& action) mutable
{
UpdateTranslationManipulator(
action, manipulatorEntityIds->m_entityIds, m_entityIdManipulators, m_pivotOverrideFrame, prevModifiers,
m_transformChangedInternally, m_spaceCluster.m_spaceLock);
});
translationManipulators->InstallSurfaceManipulatorMouseUpCallback(
[this, manipulatorEntityIds]([[maybe_unused]] const SurfaceManipulator::Action& action)
{
AzToolsFramework::EditorTransformChangeNotificationBus::Broadcast(
&AzToolsFramework::EditorTransformChangeNotificationBus::Events::OnEntityTransformChanged,
manipulatorEntityIds->m_entityIds);
EndRecordManipulatorCommand();
});
// transfer ownership
m_entityIdManipulators.m_manipulators = AZStd::move(translationManipulators);
}
@ -1395,7 +1408,7 @@ namespace AzToolsFramework
m_axisPreview.m_translation = m_entityIdManipulators.m_manipulators->GetLocalTransform().GetTranslation();
m_axisPreview.m_orientation = QuaternionFromTransformNoScaling(m_entityIdManipulators.m_manipulators->GetLocalTransform());
// [ref 1.]
// see comment [ref 1.] above
BeginRecordManipulatorCommand();
});
@ -1867,22 +1880,22 @@ namespace AzToolsFramework
if (!m_selectedEntityIds.empty())
{
// group copying/alignment to specific entity - 'ditto' position/orientation for group
if (Input::GroupDitto(mouseInteraction) && PerformGroupDitto(entityIdUnderCursor))
// try snapping to a surface (mesh) if in Translation mode
if (Input::SnapSurface(mouseInteraction))
{
PerformSnapToSurface(mouseInteraction);
return false;
}
// individual copying/alignment to specific entity - 'ditto' position/orientation for individual
if (Input::IndividualDitto(mouseInteraction) && PerformIndividualDitto(entityIdUnderCursor))
// group copying/alignment to specific entity - 'ditto' position/orientation for group
if (Input::GroupDitto(mouseInteraction) && PerformGroupDitto(entityIdUnderCursor))
{
return false;
}
// try snapping to the terrain (if in Translation mode) and entity wasn't picked
if (Input::SnapTerrain(mouseInteraction))
// individual copying/alignment to specific entity - 'ditto' position/orientation for individual
if (Input::IndividualDitto(mouseInteraction) && PerformIndividualDitto(entityIdUnderCursor))
{
PerformSnapToTerrain(mouseInteraction);
return false;
}
@ -1973,25 +1986,28 @@ namespace AzToolsFramework
return false;
}
void EditorTransformComponentSelection::PerformSnapToTerrain(const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
void EditorTransformComponentSelection::PerformSnapToSurface(const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
{
for (AZ::EntityId entityId : m_selectedEntityIds)
for (const AZ::EntityId& entityId : m_selectedEntityIds)
{
ScopedUndoBatch::MarkEntityDirty(entityId);
}
if (m_mode == Mode::Translation)
{
const AZ::Vector3 finalSurfacePosition = PickTerrainPosition(mouseInteraction.m_mouseInteraction);
const AZ::Vector3 worldPosition = FindClosestPickIntersection(
mouseInteraction.m_mouseInteraction.m_interactionId.m_viewportId,
mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates, AzToolsFramework::EditorPickRayLength,
GetDefaultEntityPlacementDistance());
// handle modifier alternatives
if (Input::IndividualDitto(mouseInteraction))
{
CopyTranslationToSelectedEntitiesIndividual(finalSurfacePosition);
CopyTranslationToSelectedEntitiesIndividual(worldPosition);
}
else if (Input::GroupDitto(mouseInteraction))
{
CopyTranslationToSelectedEntitiesGroup(finalSurfacePosition);
CopyTranslationToSelectedEntitiesGroup(worldPosition);
}
}
else if (m_mode == Mode::Rotation)

@ -308,7 +308,7 @@ namespace AzToolsFramework
bool PerformGroupDitto(AZ::EntityId entityId);
bool PerformIndividualDitto(AZ::EntityId entityId);
void PerformManipulatorDitto(AZ::EntityId entityId);
void PerformSnapToTerrain(const ViewportInteraction::MouseInteractionEvent& mouseInteraction);
void PerformSnapToSurface(const ViewportInteraction::MouseInteractionEvent& mouseInteraction);
//! Responsible for keeping the space cluster in sync with the current reference frame.
void UpdateSpaceCluster(ReferenceFrame referenceFrame);

@ -159,6 +159,10 @@ set(FILES
Entity/SliceEditorEntityOwnershipServiceBus.h
Entity/EntityUtilityComponent.h
Entity/EntityUtilityComponent.cpp
Entity/ReadOnly/ReadOnlyEntityInterface.h
Entity/ReadOnly/ReadOnlyEntityBus.h
Entity/ReadOnly/ReadOnlyEntitySystemComponent.cpp
Entity/ReadOnly/ReadOnlyEntitySystemComponent.h
Fingerprinting/TypeFingerprinter.h
Fingerprinting/TypeFingerprinter.cpp
FocusMode/FocusModeInterface.h

@ -8,6 +8,8 @@
#include <Tests/BoundsTestComponent.h>
#include <AzCore/Math/Obb.h>
#include <AzCore/Math/IntersectSegment.h>
#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
namespace UnitTest
@ -62,4 +64,51 @@ namespace UnitTest
{
return m_localBounds;
}
void RenderGeometryIntersectionTestComponent::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<RenderGeometryIntersectionTestComponent, BoundsTestComponent>()->Version(1);
}
}
void RenderGeometryIntersectionTestComponent::Activate()
{
BoundsTestComponent::Activate();
const AZ::EntityId entityId = GetEntityId();
AzFramework::EntityContextId contextId = AzFramework::EntityContextId::CreateNull();
AzFramework::EntityIdContextQueryBus::EventResult(contextId, entityId, &AzFramework::EntityIdContextQueries::GetOwningContextId);
AzFramework::RenderGeometry::IntersectionRequestBus::Handler::BusConnect({entityId, contextId});
}
void RenderGeometryIntersectionTestComponent::Deactivate()
{
AzFramework::RenderGeometry::IntersectionRequestBus::Handler::BusDisconnect();
BoundsTestComponent::Deactivate();
}
AzFramework::RenderGeometry::RayResult RenderGeometryIntersectionTestComponent::RenderGeometryIntersect(
const AzFramework::RenderGeometry::RayRequest& ray)
{
AZ::Transform worldFromLocal = AZ::Transform::CreateIdentity();
AZ::TransformBus::EventResult(worldFromLocal, GetEntityId(), &AZ::TransformBus::Events::GetWorldTM);
AzFramework::RenderGeometry::RayResult rayResult;
float t = 0.0f;
const AZ::Obb obb = GetLocalBounds().GetTransformedObb(worldFromLocal);
const AZ::Vector3 rayDirection = ray.m_endWorldPosition - ray.m_startWorldPosition;
if (AZ::Intersect::IntersectRayObb(ray.m_startWorldPosition, rayDirection, obb, t))
{
rayResult.m_worldPosition = ray.m_startWorldPosition + rayDirection * t;
rayResult.m_entityAndComponent = AZ::EntityComponentIdPair(GetEntityId(), GetId());
rayResult.m_distance = t;
rayResult.m_uv = AZ::Vector2::CreateZero();
rayResult.m_worldNormal = AZ::Vector3::CreateZero();
}
return rayResult;
}
} // namespace UnitTest

@ -8,6 +8,7 @@
#pragma once
#include <AzFramework/Render/GeometryIntersectionBus.h>
#include <AzFramework/Visibility/BoundsBus.h>
#include <AzToolsFramework/API/ComponentEntitySelectionBus.h>
#include <AzToolsFramework/ToolsComponents/EditorComponentBase.h>
@ -44,4 +45,21 @@ namespace UnitTest
AZ::Aabb m_localBounds; //!< Local bounds that can be modified for certain tests (defaults to unit cube).
};
class RenderGeometryIntersectionTestComponent
: public BoundsTestComponent
, public AzFramework::RenderGeometry::IntersectionRequestBus::Handler
{
public:
AZ_EDITOR_COMPONENT(RenderGeometryIntersectionTestComponent, "{6F46B5BF-60DF-4BDD-9BA7-9658E85B99C2}", BoundsTestComponent);
static void Reflect(AZ::ReflectContext* context);
// AZ::Component overrides ...
void Activate() override;
void Deactivate() override;
// IntersectionRequestBus overrides ...
AzFramework::RenderGeometry::RayResult RenderGeometryIntersect(const AzFramework::RenderGeometry::RayRequest& ray) override;
};
} // namespace UnitTest

@ -1663,7 +1663,7 @@ namespace UnitTest
const AZ::Transform finalEntityTransform = AzToolsFramework::GetWorldTransform(m_entityId1);
// ensure final world positions match
EXPECT_TRUE(finalEntityTransform.IsClose(finalTransformWorld, 0.01f));
EXPECT_THAT(finalEntityTransform, IsCloseTolerance(finalTransformWorld, 0.01f));
}
TEST_F(EditorTransformComponentSelectionManipulatorTestFixture, TranslatingEntityWithLinearManipulatorNotifiesOnEntityTransformChanged)
@ -2973,4 +2973,176 @@ namespace UnitTest
EXPECT_THAT(hoveredEntityIdAccentRemoved, IsTrue());
EXPECT_THAT(hoveredEntityEntityId, Eq(AZ::EntityId(12345)));
}
class EditorTransformComponentSelectionRenderGeometryIntersectionFixture : public ToolsApplicationFixture
{
public:
void SetUpEditorFixtureImpl() override
{
auto* app = GetApplication();
// register a simple component implementing BoundsRequestBus and EditorComponentSelectionRequestsBus
app->RegisterComponentDescriptor(BoundsTestComponent::CreateDescriptor());
// register a component implementing RenderGeometry::IntersectionRequestBus
app->RegisterComponentDescriptor(RenderGeometryIntersectionTestComponent::CreateDescriptor());
auto createEntityWithGeometryIntersectionFn = [](const char* entityName)
{
AZ::Entity* entity = nullptr;
AZ::EntityId entityId = CreateDefaultEditorEntity(entityName, &entity);
entity->Deactivate();
entity->CreateComponent<RenderGeometryIntersectionTestComponent>();
entity->Activate();
return entityId;
};
m_entityIdGround = createEntityWithGeometryIntersectionFn("Entity1");
m_entityIdBox = createEntityWithGeometryIntersectionFn("Entity2");
if (auto* ground = AzToolsFramework::GetEntityById(m_entityIdGround)->FindComponent<RenderGeometryIntersectionTestComponent>())
{
ground->m_localBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-10.0f, -10.0f, -0.5f), AZ::Vector3(10.0f, 10.0f, 0.5f));
}
AzToolsFramework::SetWorldTransform(m_entityIdGround, AZ::Transform::CreateTranslation(AZ::Vector3(0.0f, 10.0f, 5.0f)));
if (auto* box = AzToolsFramework::GetEntityById(m_entityIdBox)->FindComponent<RenderGeometryIntersectionTestComponent>())
{
box->m_localBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-0.5f), AZ::Vector3(0.5f));
}
AzToolsFramework::SetWorldTransform(
m_entityIdBox,
AZ::Transform::CreateFromMatrix3x3AndTranslation(
AZ::Matrix3x3::CreateRotationZ(AZ::DegToRad(45.0f)), AZ::Vector3(0.0f, 10.0f, 7.0f)));
}
AZ::EntityId m_entityIdGround;
AZ::EntityId m_entityIdBox;
};
using EditorTransformComponentSelectionRenderGeometryIntersectionManipulatorFixture =
IndirectCallManipulatorViewportInteractionFixtureMixin<EditorTransformComponentSelectionRenderGeometryIntersectionFixture>;
TEST_F(
EditorTransformComponentSelectionRenderGeometryIntersectionManipulatorFixture, BoxCanBePlacedOnMeshSurfaceUsingSurfaceManipulator)
{
// camera (go to position format) - 0.00, 20.00, 12.00, -35.00, -180.00
m_cameraState.m_viewportSize = AZ::Vector2(1280.0f, 720.0f);
AzFramework::SetCameraTransform(
m_cameraState,
AZ::Transform::CreateFromMatrix3x3AndTranslation(
AZ::Matrix3x3::CreateRotationZ(AZ::DegToRad(-180.0f)) * AZ::Matrix3x3::CreateRotationX(AZ::DegToRad(-35.0f)),
AZ::Vector3(0.0f, 20.0f, 12.0f)));
// the initial starting position of the entity
const auto initialTransformWorld = AzToolsFramework::GetWorldTransform(m_entityIdBox);
// where the entity should end up (snapped to the larger ground surface)
const auto finalTransformWorld =
AZ::Transform::CreateFromQuaternionAndTranslation(initialTransformWorld.GetRotation(), AZ::Vector3(2.5f, 12.5f, 5.5f));
// calculate the position in screen space of the initial position of the entity
const auto initialPositionScreen = AzFramework::WorldToScreen(initialTransformWorld.GetTranslation(), m_cameraState);
// calculate the position in screen space of the final position of the entity
const auto finalPositionScreen = AzFramework::WorldToScreen(finalTransformWorld.GetTranslation(), m_cameraState);
// select the entity (this will cause the manipulators to appear in EditorTransformComponentSelection)
AzToolsFramework::SelectEntity(m_entityIdBox);
// press and drag the mouse (starting where the surface manipulator is)
m_actionDispatcher->CameraState(m_cameraState)
->MousePosition(initialPositionScreen)
->MouseLButtonDown()
->MousePosition(finalPositionScreen)
->MouseLButtonUp();
// read back the position of the entity now
const AZ::Transform finalEntityTransform = AzToolsFramework::GetWorldTransform(m_entityIdBox);
// ensure final world positions match
EXPECT_THAT(finalEntityTransform, IsCloseTolerance(finalTransformWorld, 0.01f));
}
TEST_F(
EditorTransformComponentSelectionRenderGeometryIntersectionManipulatorFixture,
SurfaceManipulatorFollowsMouseAtDefaultEditorDistanceFromCameraWhenNoMeshIntersection)
{
// camera (go to position format) - 0.00, 25.00, 12.00, 0.00, -180.00
m_cameraState.m_viewportSize = AZ::Vector2(1280.0f, 720.0f);
AzFramework::SetCameraTransform(
m_cameraState,
AZ::Transform::CreateFromMatrix3x3AndTranslation(
AZ::Matrix3x3::CreateRotationZ(AZ::DegToRad(-180.0f)), AZ::Vector3(0.0f, 25.0f, 12.0f)));
// the initial starting position of the entity
const auto initialTransformWorld = AzToolsFramework::GetWorldTransform(m_entityIdBox);
// where the entity should end up (default distance away from the camera/near clip under where the mouse is)
const auto finalTransformWorld =
AZ::Transform::CreateFromQuaternionAndTranslation(initialTransformWorld.GetRotation(), AZ::Vector3(0.0f, 14.9f, 12.0f));
// calculate the position in screen space of the initial position of the entity
const auto initialPositionScreen = AzFramework::WorldToScreen(initialTransformWorld.GetTranslation(), m_cameraState);
// calculate the position in screen space of the final position of the entity
const auto finalPositionScreen = AzFramework::WorldToScreen(finalTransformWorld.GetTranslation(), m_cameraState);
// select the entity (this will cause the manipulators to appear in EditorTransformComponentSelection)
AzToolsFramework::SelectEntity(m_entityIdBox);
// press and drag the mouse (starting where the surface manipulator is)
m_actionDispatcher->CameraState(m_cameraState)
->MousePosition(initialPositionScreen)
->MouseLButtonDown()
->MousePosition(finalPositionScreen)
->MouseLButtonUp();
// read back the position of the entity now
const AZ::Transform finalEntityTransform = AzToolsFramework::GetWorldTransform(m_entityIdBox);
const auto viewportRay = AzToolsFramework::ViewportInteraction::ViewportScreenToWorldRay(m_cameraState, initialPositionScreen);
const auto distanceAway = (finalEntityTransform.GetTranslation() - viewportRay.origin).GetLength();
// ensure final world positions match
EXPECT_THAT(finalEntityTransform, IsCloseTolerance(finalTransformWorld, 0.01f));
// ensure distance away is what we expect
EXPECT_NEAR(distanceAway, AzToolsFramework::GetDefaultEntityPlacementDistance(), 0.001f);
}
TEST_F(
EditorTransformComponentSelectionRenderGeometryIntersectionManipulatorFixture,
MiddleMouseButtonWithShiftAndCtrlHeldOnMeshSurfaceWillSnapSelectedEntityToIntersectionPoint)
{
// camera (go to position format) - 21.00, 8.00, 11.00, -22.00, 150.00
m_cameraState.m_viewportSize = AZ::Vector2(1280.0f, 720.0f);
AzFramework::SetCameraTransform(
m_cameraState,
AZ::Transform::CreateFromMatrix3x3AndTranslation(
AZ::Matrix3x3::CreateRotationZ(AZ::DegToRad(150.0f)) * AZ::Matrix3x3::CreateRotationX(AZ::DegToRad(-22.0f)),
AZ::Vector3(21.0f, 8.0f, 11.0f)));
// position the ground entity
AzToolsFramework::SetWorldTransform(
m_entityIdGround,
AZ::Transform::CreateFromMatrix3x3AndTranslation(
AZ::Matrix3x3::CreateRotationY(AZ::DegToRad(40.0f)) * AZ::Matrix3x3::CreateRotationZ(AZ::DegToRad(60.0f)),
AZ::Vector3(14.0f, -6.0f, 5.0f)));
// select the other entity (a 1x1x1 box)
AzToolsFramework::SelectEntity(m_entityIdBox);
// expected world position (value taken from editor scenario)
const auto expectedWorldPosition = AZ::Vector3(13.606657f, -2.6753534f, 5.9827675f);
const auto screenPosition = AzFramework::WorldToScreen(expectedWorldPosition, m_cameraState);
// perform snap action
m_actionDispatcher->CameraState(m_cameraState)
->MousePosition(screenPosition)
->KeyboardModifierDown(AzToolsFramework::ViewportInteraction::KeyboardModifier::Control)
->KeyboardModifierDown(AzToolsFramework::ViewportInteraction::KeyboardModifier::Shift)
->MouseMButtonDown();
// read back the current entity transform after placement
const AZ::Transform finalEntityTransform = AzToolsFramework::GetWorldTransform(m_entityIdBox);
EXPECT_THAT(finalEntityTransform.GetTranslation(), IsCloseTolerance(expectedWorldPosition, 0.01f));
}
} // namespace UnitTest

@ -10,22 +10,23 @@
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzFramework/Viewport/ViewportScreen.h>
#include <AzManipulatorTestFramework/AzManipulatorTestFramework.h>
#include <AzManipulatorTestFramework/AzManipulatorTestFrameworkTestHelpers.h>
#include <AzManipulatorTestFramework/AzManipulatorTestFrameworkUtils.h>
#include <AzManipulatorTestFramework/ImmediateModeActionDispatcher.h>
#include <AzManipulatorTestFramework/IndirectManipulatorViewportInteraction.h>
#include <AzTest/AzTest.h>
#include <AzToolsFramework/Application/ToolsApplication.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/Manipulators/EditorVertexSelection.h>
#include <AzToolsFramework/Manipulators/HoverSelection.h>
#include <AzToolsFramework/Manipulators/ManipulatorManager.h>
#include <AzToolsFramework/Manipulators/TranslationManipulators.h>
#include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
#include <AzToolsFramework/ViewportSelection/EditorInteractionSystemViewportSelectionRequestBus.h>
#include <AzManipulatorTestFramework/AzManipulatorTestFramework.h>
#include <AzManipulatorTestFramework/AzManipulatorTestFrameworkUtils.h>
#include <AzManipulatorTestFramework/AzManipulatorTestFrameworkTestHelpers.h>
#include <AzManipulatorTestFramework/IndirectManipulatorViewportInteraction.h>
#include <AzManipulatorTestFramework/ImmediateModeActionDispatcher.h>
#include <Tests/Utils/Printers.h>
using namespace AzToolsFramework;
#include <Tests/BoundsTestComponent.h>
namespace UnitTest
{
@ -42,20 +43,65 @@ namespace UnitTest
void Disconnect();
// FixedVerticesRequestBus/VariableVerticesRequestBus ...
bool GetVertex(size_t index, AZ::Vector3& vertex) const override { return m_vertexContainer.GetVertex(index, vertex); }
bool UpdateVertex(size_t index, const AZ::Vector3& vertex) override { return m_vertexContainer.UpdateVertex(index, vertex); };
void AddVertex(const AZ::Vector3& vertex) override { m_vertexContainer.AddVertex(vertex); }
bool InsertVertex(size_t index, const AZ::Vector3& vertex) override { return m_vertexContainer.InsertVertex(index, vertex); }
bool RemoveVertex(size_t index) override { return m_vertexContainer.RemoveVertex(index); }
void SetVertices(const AZStd::vector<AZ::Vector3>& vertices) override { m_vertexContainer.SetVertices(vertices); };
void ClearVertices() override { m_vertexContainer.Clear(); }
size_t Size() const override { return m_vertexContainer.Size(); }
bool Empty() const override { return m_vertexContainer.Empty(); }
bool GetVertex(size_t index, AZ::Vector3& vertex) const override;
bool UpdateVertex(size_t index, const AZ::Vector3& vertex) override;
void AddVertex(const AZ::Vector3& vertex) override;
bool InsertVertex(size_t index, const AZ::Vector3& vertex) override;
bool RemoveVertex(size_t index) override;
void SetVertices(const AZStd::vector<AZ::Vector3>& vertices) override;
void ClearVertices() override;
size_t Size() const override;
bool Empty() const override;
private:
AZ::VertexContainer<AZ::Vector3> m_vertexContainer;
};
bool TestVariableVerticesVertexContainer::GetVertex(size_t index, AZ::Vector3& vertex) const
{
return m_vertexContainer.GetVertex(index, vertex);
}
bool TestVariableVerticesVertexContainer::UpdateVertex(size_t index, const AZ::Vector3& vertex)
{
return m_vertexContainer.UpdateVertex(index, vertex);
}
void TestVariableVerticesVertexContainer::AddVertex(const AZ::Vector3& vertex)
{
m_vertexContainer.AddVertex(vertex);
}
bool TestVariableVerticesVertexContainer::InsertVertex(size_t index, const AZ::Vector3& vertex)
{
return m_vertexContainer.InsertVertex(index, vertex);
}
bool TestVariableVerticesVertexContainer::RemoveVertex(size_t index)
{
return m_vertexContainer.RemoveVertex(index);
}
void TestVariableVerticesVertexContainer::SetVertices(const AZStd::vector<AZ::Vector3>& vertices)
{
m_vertexContainer.SetVertices(vertices);
}
void TestVariableVerticesVertexContainer::ClearVertices()
{
m_vertexContainer.Clear();
}
size_t TestVariableVerticesVertexContainer::Size() const
{
return m_vertexContainer.Size();
}
bool TestVariableVerticesVertexContainer::Empty() const
{
return m_vertexContainer.Empty();
}
void TestVariableVerticesVertexContainer::Connect(const AZ::EntityId entityId)
{
AZ::VariableVerticesRequestBus<AZ::Vector3>::Handler::BusConnect(entityId);
@ -68,17 +114,18 @@ namespace UnitTest
AZ::VariableVerticesRequestBus<AZ::Vector3>::Handler::BusDisconnect();
}
class TestEditorVertexSelectionVariable
: public EditorVertexSelectionVariable<AZ::Vector3>
class TestEditorVertexSelectionVariable : public AzToolsFramework::EditorVertexSelectionVariable<AZ::Vector3>
{
public:
AZ_CLASS_ALLOCATOR(TestEditorVertexSelectionVariable, AZ::SystemAllocator, 0)
void ShowVertexDeletionWarning() override { /*noop*/ }
void ShowVertexDeletionWarning() override
{
// noop
}
};
class EditorVertexSelectionFixture
: public ToolsApplicationFixture
class EditorVertexSelectionFixture : public ToolsApplicationFixture
{
public:
void SetUpEditorFixtureImpl() override
@ -112,26 +159,25 @@ namespace UnitTest
void EditorVertexSelectionFixture::RecreateVertexSelection()
{
namespace aztf = AzToolsFramework;
m_vertexSelection.Create(
AZ::EntityComponentIdPair(m_entityId, TestComponentId),
g_mainManipulatorManagerId, AZStd::make_unique<NullHoverSelection>(),
TranslationManipulators::Dimensions::Three, ConfigureTranslationManipulatorAppearance3d);
AZ::EntityComponentIdPair(m_entityId, TestComponentId), aztf::g_mainManipulatorManagerId,
AZStd::make_unique<aztf::NullHoverSelection>(), aztf::TranslationManipulators::Dimensions::Three,
aztf::ConfigureTranslationManipulatorAppearance3d);
}
void EditorVertexSelectionFixture::PopulateVertices()
{
for (size_t vertIndex = 0; vertIndex < EditorVertexSelectionFixture::VertexCount; ++vertIndex)
{
InsertVertexAfter(
AZ::EntityComponentIdPair(m_entityId, TestComponentId), 0, AZ::Vector3::CreateZero());
AzToolsFramework::InsertVertexAfter(AZ::EntityComponentIdPair(m_entityId, TestComponentId), 0, AZ::Vector3::CreateZero());
}
}
void EditorVertexSelectionFixture::ClearVertices()
{
for (size_t vertIndex = 0; vertIndex < EditorVertexSelectionFixture::VertexCount; ++vertIndex)
{
SafeRemoveVertex<AZ::Vector3>(
AZ::EntityComponentIdPair(m_entityId, TestComponentId), 0);
AzToolsFramework::SafeRemoveVertex<AZ::Vector3>(AZ::EntityComponentIdPair(m_entityId, TestComponentId), 0);
}
}
@ -188,7 +234,7 @@ namespace UnitTest
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// When
// just provide a placeholder mouse interaction event in this case
m_vertexSelection.SnapVerticesToTerrain(ViewportInteraction::MouseInteractionEvent{});
m_vertexSelection.SnapVerticesToSurface(AzToolsFramework::ViewportInteraction::MouseInteractionEvent{});
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -197,8 +243,7 @@ namespace UnitTest
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
}
using EditorVertexSelectionManipulatorFixture =
IndirectCallManipulatorViewportInteractionFixtureMixin<EditorVertexSelectionFixture>;
using EditorVertexSelectionManipulatorFixture = IndirectCallManipulatorViewportInteractionFixtureMixin<EditorVertexSelectionFixture>;
TEST_F(EditorVertexSelectionManipulatorFixture, CannotDeleteAllVertices)
{
@ -206,25 +251,23 @@ namespace UnitTest
const auto entityComponentIdPair = AZ::EntityComponentIdPair(m_entityId, TestComponentId);
const float horizontalPositions[] = {-1.5f, -0.5f, 0.5f, 1.5f};
for (size_t vertIndex = 0; vertIndex < std::size(horizontalPositions); ++vertIndex)
const float horizontalPositions[] = { -1.5f, -0.5f, 0.5f, 1.5f };
for (size_t vertIndex = 0; vertIndex < AZStd::size(horizontalPositions); ++vertIndex)
{
InsertVertexAfter(
entityComponentIdPair, vertIndex, AZ::Vector3(horizontalPositions[vertIndex], 5.0f, 0.0f));
AzToolsFramework::InsertVertexAfter(entityComponentIdPair, vertIndex, AZ::Vector3(horizontalPositions[vertIndex], 5.0f, 0.0f));
}
// rebuild the vertex selection after adding the new verts
// rebuild the vertex selection after adding the new vertices
RecreateVertexSelection();
// build a vector of the vertex positions in screen space
AZStd::vector<AzFramework::ScreenPoint> vertexScreenPositions;
for (size_t vertIndex = 0; vertIndex < std::size(horizontalPositions); ++vertIndex)
for (size_t vertIndex = 0; vertIndex < AZStd::size(horizontalPositions); ++vertIndex)
{
AZ::Vector3 localVertex;
AZ::Vector3 localVertex = AZ::Vector3::CreateZero();
bool found = false;
AZ::FixedVerticesRequestBus<AZ::Vector3>::EventResult(
found, m_entityId, &AZ::FixedVerticesRequestBus<AZ::Vector3>::Handler::GetVertex,
vertIndex, localVertex);
found, m_entityId, &AZ::FixedVerticesRequestBus<AZ::Vector3>::Handler::GetVertex, vertIndex, localVertex);
if (found)
{
@ -267,9 +310,9 @@ namespace UnitTest
const auto entityComponentIdPair = AZ::EntityComponentIdPair(m_entityId, TestComponentId);
// add a single vertex (in front of the camera)
InsertVertexAfter(entityComponentIdPair, 0, AZ::Vector3::CreateAxisY(5.0f));
AzToolsFramework::InsertVertexAfter(entityComponentIdPair, 0, AZ::Vector3::CreateAxisY(5.0f));
// rebuild the vertex selection after adding the new verts
// rebuild the vertex selection after adding the new vertices
RecreateVertexSelection();
AzFramework::ScreenPoint vertexScreenPosition;
@ -300,4 +343,132 @@ namespace UnitTest
// deleting the last vertex through a manipulator is disallowed - size should remain the same
EXPECT_THAT(vertexCountAfter, Eq(1));
}
static AZ::EntityId CreateEntityForVertexIntersectionPlacement(EditorVertexSelectionManipulatorFixture& fixture)
{
auto* app = fixture.GetApplication();
app->RegisterComponentDescriptor(BoundsTestComponent::CreateDescriptor());
app->RegisterComponentDescriptor(RenderGeometryIntersectionTestComponent::CreateDescriptor());
AZ::Entity* entityGround = nullptr;
AZ::EntityId entityIdGround = CreateDefaultEditorEntity("EntityGround", &entityGround);
entityGround->Deactivate();
auto ground = entityGround->CreateComponent<RenderGeometryIntersectionTestComponent>();
entityGround->Activate();
ground->m_localBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-10.0f, -10.0f, -0.5f), AZ::Vector3(10.0f, 10.0f, 0.5f));
return entityIdGround;
}
static AZStd::vector<AzFramework::ScreenPoint> SetupVertices(
const AZ::EntityId entityId, EditorVertexSelectionManipulatorFixture& fixture)
{
const auto entityComponentIdPair = AZ::EntityComponentIdPair(entityId, TestComponentId);
const float horizontalPositions[] = { -3.0f, -1.0f, 1.0f, 3.0f };
for (size_t vertIndex = 0; vertIndex < AZStd::size(horizontalPositions); ++vertIndex)
{
AzToolsFramework::InsertVertexAfter(entityComponentIdPair, vertIndex, AZ::Vector3(horizontalPositions[vertIndex], 0.0f, 0.0f));
}
// rebuild the vertex selection after adding the new vertices
fixture.RecreateVertexSelection();
// build a vector of the vertex positions in screen space
AZStd::vector<AzFramework::ScreenPoint> vertexScreenPositions;
for (size_t vertIndex = 0; vertIndex < AZStd::size(horizontalPositions); ++vertIndex)
{
AZ::Vector3 localVertex;
bool found = false;
AZ::FixedVerticesRequestBus<AZ::Vector3>::EventResult(
found, entityId, &AZ::FixedVerticesRequestBus<AZ::Vector3>::Handler::GetVertex, vertIndex, localVertex);
if (found)
{
const AZ::Vector3 worldVertex = AzToolsFramework::GetWorldTransform(entityId).TransformPoint(localVertex);
vertexScreenPositions.push_back(AzFramework::WorldToScreen(worldVertex, fixture.m_cameraState));
}
}
return vertexScreenPositions;
}
AzToolsFramework::ViewportInteraction::MouseInteractionEvent BuildMiddleMouseDownEvent(
const AzFramework::ScreenPoint& screenPosition, const AzFramework::ViewportId viewportId)
{
AzToolsFramework::ViewportInteraction::MousePick mousePick;
mousePick.m_screenCoordinates = screenPosition;
AzToolsFramework::ViewportInteraction::MouseInteraction mouseInteraction;
mouseInteraction.m_interactionId.m_cameraId = AZ::EntityId();
mouseInteraction.m_interactionId.m_viewportId = viewportId;
mouseInteraction.m_mouseButtons =
AzToolsFramework::ViewportInteraction::MouseButtonsFromButton(AzToolsFramework::ViewportInteraction::MouseButton::Middle);
mouseInteraction.m_mousePick = mousePick;
mouseInteraction.m_keyboardModifiers = AzToolsFramework::ViewportInteraction::KeyboardModifiers(
static_cast<AZ::u32>(AzToolsFramework::ViewportInteraction::KeyboardModifier::Shift) |
static_cast<AZ::u32>(AzToolsFramework::ViewportInteraction::KeyboardModifier::Ctrl));
return AzToolsFramework::ViewportInteraction::MouseInteractionEvent(
mouseInteraction, AzToolsFramework::ViewportInteraction::MouseEvent::Down, /*captured=*/false);
}
TEST_F(EditorVertexSelectionManipulatorFixture, VertexPlacedWhereIntersectionPointIsFoundWithCustomReferenceSpace)
{
const AZ::EntityId entityIdGround = CreateEntityForVertexIntersectionPlacement(*this);
// position ground
AzToolsFramework::SetWorldTransform(
entityIdGround,
AZ::Transform::CreateFromMatrix3x3AndTranslation(
AZ::Matrix3x3::CreateRotationX(AZ::DegToRad(-20.0f)) * AZ::Matrix3x3::CreateRotationY(AZ::DegToRad(-40.0f)) *
AZ::Matrix3x3::CreateRotationZ(AZ::DegToRad(60.0f)),
AZ::Vector3(14.0f, -6.0f, 5.0f)));
// camera (go to position format) - 12.00, 18.00, 16.00, -38.00, -175.00
m_cameraState.m_viewportSize = AZ::Vector2(1280.0f, 720.0f);
AzFramework::SetCameraTransform(
m_cameraState,
AZ::Transform::CreateFromMatrix3x3AndTranslation(
AZ::Matrix3x3::CreateRotationZ(AZ::DegToRad(-175.0f)) * AZ::Matrix3x3::CreateRotationX(AZ::DegToRad(-38.0f)),
AZ::Vector3(12.0f, 18.0f, 16.0f)));
// create orientated and scaled transform for vertex selection entity transform
auto vertexSelectionTransform = AZ::Transform::CreateFromMatrix3x3AndTranslation(
AZ::Matrix3x3::CreateRotationZ(AZ::DegToRad(45.0f)), AZ::Vector3(14.0f, 7.0f, 5.0f));
vertexSelectionTransform.MultiplyByUniformScale(3.0f);
// set the initial starting position of the vertex selection
AzToolsFramework::SetWorldTransform(m_entityId, vertexSelectionTransform);
auto vertexScreenPositions = SetupVertices(m_entityId, *this);
// press and drag the mouse (starting where the surface manipulator is)
// select each vertex (by holding ctrl)
m_actionDispatcher->CameraState(m_cameraState)->MousePosition(vertexScreenPositions[0])->MouseLButtonDown()->MouseLButtonUp();
const auto finalPositionWorld = AZ::Vector3(14.3573294f, -8.94695091f, 7.08627319f);
// calculate the position in screen space of the final position of the entity
const auto finalPositionScreen = AzFramework::WorldToScreen(finalPositionWorld, m_cameraState);
auto middleMouseDownEvent =
BuildMiddleMouseDownEvent(finalPositionScreen, m_viewportManipulatorInteraction->GetViewportInteraction().GetViewportId());
// explicitly handle mouse event in vertex selection instance
m_vertexSelection.HandleMouse(middleMouseDownEvent);
// read back the position of the vertex now
AZ::Vector3 localVertex = AZ::Vector3::CreateZero();
bool found = false;
AZ::FixedVerticesRequestBus<AZ::Vector3>::EventResult(
found, m_entityId, &AZ::FixedVerticesRequestBus<AZ::Vector3>::Handler::GetVertex, 0, localVertex);
// transform to world space
const AZ::Vector3 worldVertex = vertexSelectionTransform.TransformPoint(localVertex);
EXPECT_THAT(found, ::testing::IsTrue());
// ensure final world positions match
EXPECT_THAT(worldVertex, IsCloseTolerance(finalPositionWorld, 0.01f));
}
} // namespace UnitTest

@ -0,0 +1,125 @@
/*
* 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 <Tests/Entity/ReadOnly/ReadOnlyEntityFixture.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
namespace AzToolsFramework
{
void ReadOnlyEntityFixture::SetUpEditorFixtureImpl()
{
// Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is
// shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash
// in the unit tests.
AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize);
m_readOnlyEntityPublicInterface = AZ::Interface<ReadOnlyEntityPublicInterface>::Get();
ASSERT_TRUE(m_readOnlyEntityPublicInterface != nullptr);
GenerateTestHierarchy();
}
void ReadOnlyEntityFixture::TearDownEditorFixtureImpl()
{
}
void ReadOnlyEntityFixture::GenerateTestHierarchy()
{
/*
* Root
* |_ Child
* |_ GrandChild1
* |_ GrandChild2
*/
m_entityMap[RootEntityName] = CreateEditorEntity(RootEntityName, AZ::EntityId());
m_entityMap[ChildEntityName] = CreateEditorEntity(ChildEntityName, m_entityMap[RootEntityName]);
m_entityMap[GrandChild1EntityName] = CreateEditorEntity(GrandChild1EntityName, m_entityMap[ChildEntityName]);
m_entityMap[GrandChild2EntityName] = CreateEditorEntity(GrandChild2EntityName, m_entityMap[ChildEntityName]);
}
AZ::EntityId ReadOnlyEntityFixture::CreateEditorEntity(const char* name, AZ::EntityId parentId)
{
AZ::Entity* entity = nullptr;
UnitTest::CreateDefaultEditorEntity(name, &entity);
// Parent
AZ::TransformBus::Event(entity->GetId(), &AZ::TransformInterface::SetParent, parentId);
return entity->GetId();
}
ReadOnlyHandlerAlwaysTrue::ReadOnlyHandlerAlwaysTrue()
{
auto editorEntityContextId = AzFramework::EntityContextId::CreateNull();
EditorEntityContextRequestBus::BroadcastResult(editorEntityContextId, &EditorEntityContextRequests::GetEditorEntityContextId);
ReadOnlyEntityQueryRequestBus::Handler::BusConnect(editorEntityContextId);
}
ReadOnlyHandlerAlwaysTrue::~ReadOnlyHandlerAlwaysTrue()
{
ReadOnlyEntityQueryRequestBus::Handler::BusDisconnect();
if (auto readOnlyEntityQueryInterface = AZ::Interface<ReadOnlyEntityQueryInterface>::Get())
{
readOnlyEntityQueryInterface->RefreshReadOnlyStateForAllEntities();
}
}
void ReadOnlyHandlerAlwaysTrue::IsReadOnly([[maybe_unused]] const AZ::EntityId& entityId, bool& isReadOnly)
{
isReadOnly = true;
}
ReadOnlyHandlerAlwaysFalse::ReadOnlyHandlerAlwaysFalse()
{
auto editorEntityContextId = AzFramework::EntityContextId::CreateNull();
EditorEntityContextRequestBus::BroadcastResult(editorEntityContextId, &EditorEntityContextRequests::GetEditorEntityContextId);
ReadOnlyEntityQueryRequestBus::Handler::BusConnect(editorEntityContextId);
}
ReadOnlyHandlerAlwaysFalse::~ReadOnlyHandlerAlwaysFalse()
{
ReadOnlyEntityQueryRequestBus::Handler::BusDisconnect();
if (auto readOnlyEntityQueryInterface = AZ::Interface<ReadOnlyEntityQueryInterface>::Get())
{
readOnlyEntityQueryInterface->RefreshReadOnlyStateForAllEntities();
}
}
ReadOnlyHandlerEntityId::ReadOnlyHandlerEntityId(AZ::EntityId entityId)
: m_entityId(entityId)
{
auto editorEntityContextId = AzFramework::EntityContextId::CreateNull();
EditorEntityContextRequestBus::BroadcastResult(editorEntityContextId, &EditorEntityContextRequests::GetEditorEntityContextId);
ReadOnlyEntityQueryRequestBus::Handler::BusConnect(editorEntityContextId);
}
ReadOnlyHandlerEntityId::~ReadOnlyHandlerEntityId()
{
ReadOnlyEntityQueryRequestBus::Handler::BusDisconnect();
if (auto readOnlyEntityQueryInterface = AZ::Interface<ReadOnlyEntityQueryInterface>::Get())
{
readOnlyEntityQueryInterface->RefreshReadOnlyStateForAllEntities();
}
}
void ReadOnlyHandlerEntityId::IsReadOnly(const AZ::EntityId& entityId, bool& isReadOnly)
{
if (entityId == m_entityId)
{
isReadOnly = true;
}
}
}

@ -0,0 +1,78 @@
/*
* 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 <AzCore/Component/TransformBus.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzTest/AzTest.h>
#include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
#include <AzToolsFramework/Entity/ReadOnly/ReadOnlyEntityBus.h>
#include <AzToolsFramework/Entity/ReadOnly/ReadOnlyEntityInterface.h>
namespace AzToolsFramework
{
class ReadOnlyEntityFixture
: public UnitTest::ToolsApplicationFixture
{
protected:
void SetUpEditorFixtureImpl() override;
void TearDownEditorFixtureImpl() override;
void GenerateTestHierarchy();
AZ::EntityId CreateEditorEntity(const char* name, AZ::EntityId parentId);
AZStd::unordered_map<AZStd::string, AZ::EntityId> m_entityMap;
ReadOnlyEntityPublicInterface* m_readOnlyEntityPublicInterface = nullptr;
public:
inline static const char* RootEntityName = "Root";
inline static const char* ChildEntityName = "Child";
inline static const char* GrandChild1EntityName = "GrandChild1";
inline static const char* GrandChild2EntityName = "GrandChild2";
};
class ReadOnlyHandlerAlwaysTrue
: public ReadOnlyEntityQueryRequestBus::Handler
{
public:
ReadOnlyHandlerAlwaysTrue();
~ReadOnlyHandlerAlwaysTrue();
// ReadOnlyEntityQueryNotificationBus overrides ...
void IsReadOnly(const AZ::EntityId& entityId, bool& isReadOnly) override;
};
class ReadOnlyHandlerAlwaysFalse
: public ReadOnlyEntityQueryRequestBus::Handler
{
public:
ReadOnlyHandlerAlwaysFalse();
~ReadOnlyHandlerAlwaysFalse();
// ReadOnlyEntityQueryNotificationBus overrides ...
void IsReadOnly([[maybe_unused]] const AZ::EntityId& entityId, [[maybe_unused]] bool& isReadOnly) override {}
};
class ReadOnlyHandlerEntityId
: public ReadOnlyEntityQueryRequestBus::Handler
{
public:
ReadOnlyHandlerEntityId(AZ::EntityId entityId);
~ReadOnlyHandlerEntityId();
// ReadOnlyEntityQueryNotificationBus overrides ...
void IsReadOnly(const AZ::EntityId& entityId, bool& isReadOnly) override;
private:
AZ::EntityId m_entityId;
};
}

@ -0,0 +1,99 @@
/*
* 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 <Tests/Entity/ReadOnly/ReadOnlyEntityFixture.h>
namespace AzToolsFramework
{
TEST_F(ReadOnlyEntityFixture, NoHandlerEntityIsNotReadOnlyByDefault)
{
EXPECT_FALSE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[ChildEntityName]));
}
TEST_F(ReadOnlyEntityFixture, SingleHandlerEntityIsReadOnly)
{
// Create a handler that sets all entities to read-only.
ReadOnlyHandlerAlwaysTrue alwaysTrueHandler;
// All entities should be marked read-only now.
EXPECT_TRUE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[RootEntityName]));
EXPECT_TRUE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[ChildEntityName]));
EXPECT_TRUE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[GrandChild1EntityName]));
EXPECT_TRUE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[GrandChild2EntityName]));
}
TEST_F(ReadOnlyEntityFixture, SingleHandlerEntityIsNotReadOnly)
{
// Create a handler that sets all entities to read-only.
ReadOnlyHandlerAlwaysFalse alwaysFalseHandler;
// All entities should not be marked read-only now.
EXPECT_FALSE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[RootEntityName]));
EXPECT_FALSE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[ChildEntityName]));
EXPECT_FALSE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[GrandChild1EntityName]));
EXPECT_FALSE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[GrandChild2EntityName]));
}
TEST_F(ReadOnlyEntityFixture, SingleHandlerWithLogic)
{
// Create a handler that sets just the child entity to read-only.
ReadOnlyHandlerEntityId entityIdHandler(m_entityMap[ChildEntityName]);
EXPECT_FALSE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[RootEntityName]));
EXPECT_TRUE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[ChildEntityName]));
EXPECT_FALSE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[GrandChild1EntityName]));
EXPECT_FALSE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[GrandChild2EntityName]));
}
TEST_F(ReadOnlyEntityFixture, TwoHandlersCanOverlap)
{
// Create two handlers that set different entities to read-only.
ReadOnlyHandlerEntityId entityIdHandler1(m_entityMap[ChildEntityName]);
ReadOnlyHandlerEntityId entityIdHandler2(m_entityMap[GrandChild2EntityName]);
// Both entities should be marked as read-only, while others aren't.
EXPECT_FALSE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[RootEntityName]));
EXPECT_TRUE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[ChildEntityName]));
EXPECT_FALSE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[GrandChild1EntityName]));
EXPECT_TRUE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[GrandChild2EntityName]));
}
TEST_F(ReadOnlyEntityFixture, EnsureCacheIsRefreshedCorrectly)
{
// Verify the child entity is not marked as read-only
EXPECT_FALSE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[ChildEntityName]));
// Create a handler that sets the child entity to read-only.
ReadOnlyHandlerEntityId entityIdHandler(m_entityMap[ChildEntityName]);
// Communicate to the ReadOnlyEntitySystemComponent that the read-only state for the child entity may have changed.
// Note that this operation would usually be executed by the handler, hence the Query interface call.
if (auto readOnlyEntityQueryInterface = AZ::Interface<ReadOnlyEntityQueryInterface>::Get())
{
readOnlyEntityQueryInterface->RefreshReadOnlyState({ m_entityMap[ChildEntityName] });
}
// Verify the child entity is marked as read-only
EXPECT_TRUE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[ChildEntityName]));
}
TEST_F(ReadOnlyEntityFixture, EnsureCacheIsClearedCorrectly)
{
{
// Create a handler that sets the child entity to read-only.
ReadOnlyHandlerEntityId entityIdHandler(m_entityMap[ChildEntityName]);
// Verify the child entity is marked as read-only
EXPECT_TRUE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[ChildEntityName]));
}
// When the handler goes out of scope, it calls RefreshReadOnlyStateForAllEntities and refreshes the cache.
// Verify the child entity is no longer marked as read-only
EXPECT_FALSE(m_readOnlyEntityPublicInterface->IsReadOnly(m_entityMap[ChildEntityName]));
}
}

@ -6,7 +6,9 @@
*
*/
#include "IntegerPrimtitiveTestConfig.h"
#include <AzToolsFramework/UI/PropertyEditor/PropertyIntCtrlCommon.h>
#include <AzToolsFramework/UI/PropertyEditor/QtWidgetLimits.h>
#include <Tests/IntegerPrimtitiveTestConfig.h>
namespace UnitTest
{

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save