Merge branch 'stabilization/2110' into Atom/santorac/MaterialEditorHandlesMissingTextures

monroegm-disable-blank-issue-2
santorac 4 years ago
commit be9db80f99

@ -12,6 +12,7 @@ import pytest
import ly_test_tools.log.log_monitor import ly_test_tools.log.log_monitor
from AWS.common import constants from AWS.common import constants
from AWS.common.resource_mappings import AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY
# fixture imports # fixture imports
from assetpipeline.ap_fixtures.asset_processor_fixture import asset_processor from assetpipeline.ap_fixtures.asset_processor_fixture import asset_processor
@ -70,6 +71,41 @@ class TestAWSClientAuthWindows(object):
halt_on_unexpected=True, halt_on_unexpected=True,
) )
assert result, 'Anonymous credentials fetched successfully.' assert result, 'Anonymous credentials fetched successfully.'
@pytest.mark.parametrize('level', ['AWS/ClientAuth'])
def test_anonymous_credentials_no_global_accountid(self,
level: str,
launcher: pytest.fixture,
resource_mappings: pytest.fixture,
workspace: pytest.fixture,
asset_processor: pytest.fixture
):
"""
Test to verify AWS Cognito Identity pool anonymous authorization.
Setup: Updates resource mapping file using existing CloudFormation stacks.
Tests: Getting credentials when no credentials are configured
Verification: Log monitor looks for success credentials log.
"""
# Remove top-level account ID from resource mappings
resource_mappings.clear_select_keys([AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY])
asset_processor.start()
asset_processor.wait_for_idle()
file_to_monitor = os.path.join(launcher.workspace.paths.project_log(), constants.GAME_LOG_NAME)
log_monitor = ly_test_tools.log.log_monitor.LogMonitor(launcher=launcher, log_file_path=file_to_monitor)
launcher.args = ['+LoadLevel', level]
launcher.args.extend(['-rhi=null'])
with launcher.start(launch_ap=False):
result = log_monitor.monitor_log_for_lines(
expected_lines=['(Script) - Success anonymous credentials'],
unexpected_lines=['(Script) - Fail anonymous credentials'],
halt_on_unexpected=True,
)
assert result, 'Anonymous credentials fetched successfully.'
def test_password_signin_credentials(self, def test_password_signin_credentials(self,
launcher: pytest.fixture, launcher: pytest.fixture,

@ -150,8 +150,7 @@ def create_basic_atom_level(level_name):
entity_position=default_position, entity_position=default_position,
components=["HDRi Skybox", "Global Skylight (IBL)"], components=["HDRi Skybox", "Global Skylight (IBL)"],
parent_id=default_level.id) parent_id=default_level.id)
global_skylight_asset_path = os.path.join( global_skylight_asset_path = os.path.join("LightingPresets", "default_iblskyboxcm.exr.streamingimage")
"LightingPresets", "greenwich_park_02_4k_iblskyboxcm_iblspecular.exr.streamingimage")
global_skylight_asset_value = asset.AssetCatalogRequestBus( global_skylight_asset_value = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", global_skylight_asset_path, math.Uuid(), False) bus.Broadcast, "GetAssetIdByPath", global_skylight_asset_path, math.Uuid(), False)
global_skylight.get_set_test(0, "Controller|Configuration|Cubemap Texture", global_skylight_asset_value) global_skylight.get_set_test(0, "Controller|Configuration|Cubemap Texture", global_skylight_asset_value)

@ -55,11 +55,13 @@ class AtomComponentProperties:
def camera(property: str = 'name') -> str: def camera(property: str = 'name') -> str:
""" """
Camera component properties. Camera component properties.
- 'Field of view': Sets the value for the camera's FOV (Field of View) in degrees, i.e. 60.0
:param property: From the last element of the property tree path. Default 'name' for component name string. :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. :return: Full property path OR component name if no property specified.
""" """
properties = { properties = {
'name': 'Camera', 'name': 'Camera',
'Field of view': 'Controller|Configuration|Field of view'
} }
return properties[property] return properties[property]
@ -198,11 +200,13 @@ class AtomComponentProperties:
def grid(property: str = 'name') -> str: def grid(property: str = 'name') -> str:
""" """
Grid component properties. Grid component properties.
- 'Secondary Grid Spacing': The spacing value for the secondary grid, i.e. 1.0
:param property: From the last element of the property tree path. Default 'name' for component name string. :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. :return: Full property path OR component name if no property specified.
""" """
properties = { properties = {
'name': 'Grid', 'name': 'Grid',
'Secondary Grid Spacing': 'Controller|Configuration|Secondary Grid Spacing',
} }
return properties[property] return properties[property]
@ -225,11 +229,13 @@ class AtomComponentProperties:
def hdri_skybox(property: str = 'name') -> str: def hdri_skybox(property: str = 'name') -> str:
""" """
HDRi Skybox component properties. HDRi Skybox component properties.
- 'Cubemap Texture': Asset.id for the cubemap texture to set.
:param property: From the last element of the property tree path. Default 'name' for component name string. :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. :return: Full property path OR component name if no property specified.
""" """
properties = { properties = {
'name': 'HDRi Skybox', 'name': 'HDRi Skybox',
'Cubemap Texture': 'Controller|Configuration|Cubemap Texture',
} }
return properties[property] return properties[property]
@ -268,12 +274,14 @@ class AtomComponentProperties:
Material component properties. Requires one of Actor OR Mesh component. Material component properties. Requires one of Actor OR Mesh component.
- 'requires' a list of component names as strings required by this component. - 'requires' a list of component names as strings required by this component.
Only one of these is required at a time for this component.\n Only one of these is required at a time for this component.\n
- 'Material Asset': the material Asset.id of the material.
:param property: From the last element of the property tree path. Default 'name' for component name string. :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. :return: Full property path OR component name if no property specified.
""" """
properties = { properties = {
'name': 'Material', 'name': 'Material',
'requires': [AtomComponentProperties.actor(), AtomComponentProperties.mesh()], 'requires': [AtomComponentProperties.actor(), AtomComponentProperties.mesh()],
'Material Asset': 'Default Material|Material Asset',
} }
return properties[property] return properties[property]

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:954d7d0df47c840a24e313893800eb3126d0c0d47c3380926776b51833778db7 oid sha256:aee1fd4d5264e5ef1676b507409ce70af6358cf1ff368d9aeb17f7b2597dfbca
size 6220817 size 6220817

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e81c19128f42ba362a2d5f3ccf159dfbc942d67ceeb1ac8c21f295a6fd9d2ce5 oid sha256:d4787cdafbcc2fe71c1cb3f1da53a249db839a9df539a9e88be43ccd6d8e4d6a
size 6220817 size 6220817

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5e20801213e065b6ea8c95ede81c23faa9b6dc70a2002dc5bced293e1bed989f oid sha256:5fac5bf41c9b16b6fbd762868e5cf514376af92d6ef7ebb9e819f024f1a3e1a7
size 6220817 size 6220817

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e250f812e594e5152bf2d6f23caa8b53b78276bfdf344d7a8d355dd96cb995c0 oid sha256:7a23969670499524725535e8be7428b55b6f3e887cc24e2e903f7ea821a6d1a5
size 6220817 size 6220817

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:95be359041f8291c74b335297a4dfe9902a180510f24a181b15e1a5ba4d3b024 oid sha256:2f1f4d8865c56ed7f96f339c39e5feb4e0dbc6c6a8b4a7843b4166381b06b00d
size 6220817 size 6220817

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:07e09d3eb5bf0cee3d9b3752aaad40f3ead1dcc5ddd837a6226fadde55d57274 oid sha256:5d4ee5641e19eef08dd6b93d2f4054a1aae2165325416ed2cbf0b8243f2c0b06
size 6220817 size 6220817

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:118e43e4b915e262726183467cc4b82f244565213fea5b6bfe02be07f0851ab1 oid sha256:55c8f0d1790bb12660b7557630efca297b2a1b59e6c93167a2563da79e0a8255
size 6220817 size 6220817

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:dc2ce3256a6552975962c9e113c52c1a22bf3817d417151f6f60640dd568e0fa oid sha256:082ff368b621e12b083d96562a0889b11a1d683767a74296cbe6d8732830e9e8
size 6220817 size 6220817

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:287d98890b35427688999760f9d066bcbff1a3bc9001534241dc212b32edabd8 oid sha256:78cc62d89782899747875b41abee57c2efdfacf4c8af6511c88f82d76eaae4ca
size 6220817 size 6220817

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:66e91c92c868167c850078cd91714db47e10a96e23cc30191994486bd79c353f oid sha256:3d6719326f4dacae278d1723090ce1182193b793f250963af8be4b2c298e8841
size 6220817 size 6220817

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d950d173f5101820c5e18205401ca08ce5feeff2302ac2920b292750d86a8fa4 oid sha256:9e492bb394fb18fb117f8a5b61cd2789922f9d6e88fc83189b5b6d59ffb1c3ef
size 6220817 size 6220817

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:72eddb7126eae0c839b933886e0fb69d78229f72d49ef13199de28df2b7879db oid sha256:caca85f7728f660daae36afc81d681ba2de2377c516eb3c637599de5c94012aa
size 6220817 size 6220817

@ -123,8 +123,7 @@ def AtomEditorComponents_GlobalSkylightIBL_AddedToEntity():
Report.result(Tests.is_visible, global_skylight_entity.is_visible() is True) Report.result(Tests.is_visible, global_skylight_entity.is_visible() is True)
# 8. Set the Diffuse Image asset on the Global Skylight (IBL) entity. # 8. Set the Diffuse Image asset on the Global Skylight (IBL) entity.
global_skylight_diffuse_image_property = "Controller|Configuration|Diffuse Image" diffuse_image_path = os.path.join("LightingPresets", "default_iblskyboxcm.exr.streamingimage")
diffuse_image_path = os.path.join("LightingPresets", "greenwich_park_02_4k_iblskyboxcm.exr.streamingimage")
diffuse_image_asset = Asset.find_asset_by_path(diffuse_image_path, False) diffuse_image_asset = Asset.find_asset_by_path(diffuse_image_path, False)
global_skylight_component.set_component_property_value( global_skylight_component.set_component_property_value(
global_skylight_diffuse_image_property, diffuse_image_asset.id) global_skylight_diffuse_image_property, diffuse_image_asset.id)
@ -133,8 +132,7 @@ def AtomEditorComponents_GlobalSkylightIBL_AddedToEntity():
Report.result(Tests.diffuse_image_set, diffuse_image_set == diffuse_image_asset.id) Report.result(Tests.diffuse_image_set, diffuse_image_set == diffuse_image_asset.id)
# 9. Set the Specular Image asset on the Global Light (IBL) entity. # 9. Set the Specular Image asset on the Global Light (IBL) entity.
global_skylight_specular_image_property = "Controller|Configuration|Specular Image" specular_image_path = os.path.join("LightingPresets", "default_iblskyboxcm.exr.streamingimage")
specular_image_path = os.path.join("LightingPresets", "greenwich_park_02_4k_iblskyboxcm.exr.streamingimage")
specular_image_asset = Asset.find_asset_by_path(specular_image_path, False) specular_image_asset = Asset.find_asset_by_path(specular_image_path, False)
global_skylight_component.set_component_property_value( global_skylight_component.set_component_property_value(
global_skylight_specular_image_property, specular_image_asset.id) global_skylight_specular_image_property, specular_image_asset.id)

@ -6,30 +6,70 @@ SPDX-License-Identifier: Apache-2.0 OR MIT
""" """
# fmt: off class Tests:
class Tests : camera_component_added = (
camera_component_added = ("Camera component was added", "Camera component wasn't added") "Camera component was added",
camera_fov_set = ("Camera component FOV property set", "Camera component FOV property wasn't set") "Camera component wasn't added")
directional_light_component_added = ("Directional Light component added", "Directional Light component wasn't added") camera_fov_set = (
enter_game_mode = ("Entered game mode", "Failed to enter game mode") "Camera component FOV property set",
exit_game_mode = ("Exited game mode", "Couldn't exit game mode") "Camera component FOV property wasn't set")
global_skylight_component_added = ("Global Skylight (IBL) component added", "Global Skylight (IBL) component wasn't added") directional_light_component_added = (
global_skylight_diffuse_image_set = ("Global Skylight Diffuse Image property set", "Global Skylight Diffuse Image property wasn't set") "Directional Light component added",
global_skylight_specular_image_set = ("Global Skylight Specular Image property set", "Global Skylight Specular Image property wasn't set") "Directional Light component wasn't added")
ground_plane_material_asset_set = ("Ground Plane Material Asset was set", "Ground Plane Material Asset wasn't set") enter_game_mode = (
ground_plane_material_component_added = ("Ground Plane Material component added", "Ground Plane Material component wasn't added") "Entered game mode",
ground_plane_mesh_asset_set = ("Ground Plane Mesh Asset property was set", "Ground Plane Mesh Asset property wasn't set") "Failed to enter game mode")
hdri_skybox_component_added = ("HDRi Skybox component added", "HDRi Skybox component wasn't added") exit_game_mode = (
hdri_skybox_cubemap_texture_set = ("HDRi Skybox Cubemap Texture property set", "HDRi Skybox Cubemap Texture property wasn't set") "Exited game mode",
mesh_component_added = ("Mesh component added", "Mesh component wasn't added") "Couldn't exit game mode")
no_assert_occurred = ("No asserts detected", "Asserts were detected") global_skylight_component_added = (
no_error_occurred = ("No errors detected", "Errors were detected") "Global Skylight (IBL) component added",
secondary_grid_spacing = ("Secondary Grid Spacing set", "Secondary Grid Spacing not set") "Global Skylight (IBL) component wasn't added")
sphere_material_component_added = ("Sphere Material component added", "Sphere Material component wasn't added") global_skylight_diffuse_image_set = (
sphere_material_set = ("Sphere Material Asset was set", "Sphere Material Asset wasn't set") "Global Skylight Diffuse Image property set",
sphere_mesh_asset_set = ("Sphere Mesh Asset was set", "Sphere Mesh Asset wasn't set") "Global Skylight Diffuse Image property wasn't set")
viewport_set = ("Viewport set to correct size", "Viewport not set to correct size") global_skylight_specular_image_set = (
# fmt: on "Global Skylight Specular Image property set",
"Global Skylight Specular Image property wasn't set")
ground_plane_material_asset_set = (
"Ground Plane Material Asset was set",
"Ground Plane Material Asset wasn't set")
ground_plane_material_component_added = (
"Ground Plane Material component added",
"Ground Plane Material component wasn't added")
ground_plane_mesh_asset_set = (
"Ground Plane Mesh Asset property was set",
"Ground Plane Mesh Asset property wasn't set")
hdri_skybox_component_added = (
"HDRi Skybox component added",
"HDRi Skybox component wasn't added")
hdri_skybox_cubemap_texture_set = (
"HDRi Skybox Cubemap Texture property set",
"HDRi Skybox Cubemap Texture property wasn't set")
mesh_component_added = (
"Mesh component added",
"Mesh component wasn't added")
no_assert_occurred = (
"No asserts detected",
"Asserts were detected")
no_error_occurred = (
"No errors detected",
"Errors were detected")
secondary_grid_spacing = (
"Secondary Grid Spacing set",
"Secondary Grid Spacing not set")
sphere_material_component_added = (
"Sphere Material component added",
"Sphere Material component wasn't added")
sphere_material_set = (
"Sphere Material Asset was set",
"Sphere Material Asset wasn't set")
sphere_mesh_asset_set = (
"Sphere Mesh Asset was set",
"Sphere Mesh Asset wasn't set")
viewport_set = (
"Viewport set to correct size",
"Viewport not set to correct size")
def AtomGPU_BasicLevelSetup_SetsUpLevel(): def AtomGPU_BasicLevelSetup_SetsUpLevel():
@ -77,19 +117,17 @@ def AtomGPU_BasicLevelSetup_SetsUpLevel():
import os import os
from math import isclose from math import isclose
import azlmbr.asset as asset
import azlmbr.bus as bus
import azlmbr.legacy.general as general import azlmbr.legacy.general as general
import azlmbr.math as math import azlmbr.math as math
import azlmbr.paths import azlmbr.paths
from editor_python_test_tools.asset_utils import Asset
from editor_python_test_tools.editor_entity_utils import EditorEntity from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper as helper from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties
from Atom.atom_utils.screenshot_utils import ScreenshotHelper from Atom.atom_utils.screenshot_utils import ScreenshotHelper
MATERIAL_COMPONENT_NAME = "Material"
MESH_COMPONENT_NAME = "Mesh"
SCREENSHOT_NAME = "AtomBasicLevelSetup" SCREENSHOT_NAME = "AtomBasicLevelSetup"
SCREEN_WIDTH = 1280 SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720 SCREEN_HEIGHT = 720
@ -98,24 +136,24 @@ def AtomGPU_BasicLevelSetup_SetsUpLevel():
def initial_viewport_setup(screen_width, screen_height): def initial_viewport_setup(screen_width, screen_height):
general.set_viewport_size(screen_width, screen_height) general.set_viewport_size(screen_width, screen_height)
general.update_viewport() general.update_viewport()
result = isclose( TestHelper.wait_for_condition(
a=general.get_viewport_size().x, b=SCREEN_WIDTH, rel_tol=0.1) and isclose( function=lambda: isclose(a=general.get_viewport_size().x, b=SCREEN_WIDTH, rel_tol=0.1)
a=general.get_viewport_size().y, b=SCREEN_HEIGHT, rel_tol=0.1) and isclose(a=general.get_viewport_size().y, b=SCREEN_HEIGHT, rel_tol=0.1),
timeout_in_seconds=4.0
return result )
with Tracer() as error_tracer: with Tracer() as error_tracer:
# Test setup begins. # Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level. # Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
helper.init_idle() TestHelper.init_idle()
helper.open_level("", "Base") TestHelper.open_level("", "Base")
# Test steps begin. # Test steps begin.
# 1. Close error windows and display helpers then update the viewport size. # 1. Close error windows and display helpers then update the viewport size.
helper.close_error_windows() TestHelper.close_error_windows()
helper.close_display_helpers() TestHelper.close_display_helpers()
initial_viewport_setup(SCREEN_WIDTH, SCREEN_HEIGHT)
general.update_viewport() general.update_viewport()
Report.critical_result(Tests.viewport_set, initial_viewport_setup(SCREEN_WIDTH, SCREEN_HEIGHT))
# 2. Create Default Level Entity. # 2. Create Default Level Entity.
default_level_entity_name = "Default Level" default_level_entity_name = "Default Level"
@ -123,168 +161,167 @@ def AtomGPU_BasicLevelSetup_SetsUpLevel():
math.Vector3(0.0, 0.0, 0.0), default_level_entity_name) math.Vector3(0.0, 0.0, 0.0), default_level_entity_name)
# 3. Create Grid Entity as a child entity of the Default Level Entity. # 3. Create Grid Entity as a child entity of the Default Level Entity.
grid_name = "Grid" grid_entity = EditorEntity.create_editor_entity(AtomComponentProperties.grid(), default_level_entity.id)
grid_entity = EditorEntity.create_editor_entity(grid_name, default_level_entity.id)
# 4. Add Grid component to Grid Entity and set Secondary Grid Spacing. # 4. Add Grid component to Grid Entity and set Secondary Grid Spacing.
grid_component = grid_entity.add_component(grid_name) grid_component = grid_entity.add_component(AtomComponentProperties.grid())
secondary_grid_spacing_property = "Controller|Configuration|Secondary Grid Spacing"
secondary_grid_spacing_value = 1.0 secondary_grid_spacing_value = 1.0
grid_component.set_component_property_value(secondary_grid_spacing_property, secondary_grid_spacing_value) grid_component.set_component_property_value(
AtomComponentProperties.grid('Secondary Grid Spacing'), secondary_grid_spacing_value)
secondary_grid_spacing_set = grid_component.get_component_property_value( secondary_grid_spacing_set = grid_component.get_component_property_value(
secondary_grid_spacing_property) == secondary_grid_spacing_value AtomComponentProperties.grid('Secondary Grid Spacing')) == secondary_grid_spacing_value
Report.result(Tests.secondary_grid_spacing, secondary_grid_spacing_set) Report.result(Tests.secondary_grid_spacing, secondary_grid_spacing_set)
# 5. Create Global Skylight (IBL) Entity as a child entity of the Default Level Entity. # 5. Create Global Skylight (IBL) Entity as a child entity of the Default Level Entity.
global_skylight_name = "Global Skylight (IBL)" global_skylight_entity = EditorEntity.create_editor_entity(
global_skylight_entity = EditorEntity.create_editor_entity(global_skylight_name, default_level_entity.id) AtomComponentProperties.global_skylight(), default_level_entity.id)
# 6. Add HDRi Skybox component to the Global Skylight (IBL) Entity. # 6. Add HDRi Skybox component to the Global Skylight (IBL) Entity.
hdri_skybox_name = "HDRi Skybox" hdri_skybox_component = global_skylight_entity.add_component(AtomComponentProperties.hdri_skybox())
hdri_skybox_component = global_skylight_entity.add_component(hdri_skybox_name) Report.result(Tests.hdri_skybox_component_added, global_skylight_entity.has_component(
Report.result(Tests.hdri_skybox_component_added, global_skylight_entity.has_component(hdri_skybox_name)) AtomComponentProperties.hdri_skybox()))
# 7. Add Global Skylight (IBL) component to the Global Skylight (IBL) Entity. # 7. Add Global Skylight (IBL) component to the Global Skylight (IBL) Entity.
global_skylight_component = global_skylight_entity.add_component(global_skylight_name) global_skylight_component = global_skylight_entity.add_component(AtomComponentProperties.global_skylight())
Report.result(Tests.global_skylight_component_added, global_skylight_entity.has_component(global_skylight_name)) Report.result(Tests.global_skylight_component_added, global_skylight_entity.has_component(
AtomComponentProperties.global_skylight()))
# 8. Set the Cubemap Texture property of the HDRi Skybox component. # 8. Set the Cubemap Texture property of the HDRi Skybox component.
global_skylight_image_asset_path = os.path.join( global_skylight_image_asset_path = os.path.join("LightingPresets", "default_iblskyboxcm.exr.streamingimage")
"LightingPresets", "greenwich_park_02_4k_iblskyboxcm_iblspecular.exr.streamingimage") global_skylight_image_asset = Asset.find_asset_by_path(global_skylight_image_asset_path, False)
global_skylight_image_asset = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", global_skylight_image_asset_path, math.Uuid(), False)
hdri_skybox_cubemap_texture_property = "Controller|Configuration|Cubemap Texture"
hdri_skybox_component.set_component_property_value( hdri_skybox_component.set_component_property_value(
hdri_skybox_cubemap_texture_property, global_skylight_image_asset) AtomComponentProperties.hdri_skybox('Cubemap Texture'), global_skylight_image_asset.id)
Report.result( Report.result(
Tests.hdri_skybox_cubemap_texture_set, Tests.hdri_skybox_cubemap_texture_set,
hdri_skybox_component.get_component_property_value( hdri_skybox_component.get_component_property_value(
hdri_skybox_cubemap_texture_property) == global_skylight_image_asset) AtomComponentProperties.hdri_skybox('Cubemap Texture')) == global_skylight_image_asset.id)
# 9. Set the Diffuse Image property of the Global Skylight (IBL) component. # 9. Set the Diffuse Image property of the Global Skylight (IBL) component.
# Re-use the same image that was used in the previous test step. # Re-use the same image that was used in the previous test step.
global_skylight_diffuse_image_property = "Controller|Configuration|Diffuse Image" global_skylight_diffuse_image_asset_path = os.path.join(
"LightingPresets", "default_iblskyboxcm_ibldiffuse.exr.streamingimage")
global_skylight_diffuse_image_asset = Asset.find_asset_by_path(global_skylight_diffuse_image_asset_path, False)
global_skylight_component.set_component_property_value( global_skylight_component.set_component_property_value(
global_skylight_diffuse_image_property, global_skylight_image_asset) AtomComponentProperties.global_skylight('Diffuse Image'), global_skylight_diffuse_image_asset.id)
Report.result( Report.result(
Tests.global_skylight_diffuse_image_set, Tests.global_skylight_diffuse_image_set,
global_skylight_component.get_component_property_value( global_skylight_component.get_component_property_value(
global_skylight_diffuse_image_property) == global_skylight_image_asset) AtomComponentProperties.global_skylight('Diffuse Image')) == global_skylight_diffuse_image_asset.id)
# 10. Set the Specular Image property of the Global Skylight (IBL) component. # 10. Set the Specular Image property of the Global Skylight (IBL) component.
# Re-use the same image that was used in the previous test step. # Re-use the same image that was used in the previous test step.
global_skylight_specular_image_property = "Controller|Configuration|Specular Image" global_skylight_specular_image_asset_path = os.path.join(
"LightingPresets", "default_iblskyboxcm_iblspecular.exr.streamingimage")
global_skylight_specular_image_asset = Asset.find_asset_by_path(
global_skylight_specular_image_asset_path, False)
global_skylight_component.set_component_property_value( global_skylight_component.set_component_property_value(
global_skylight_specular_image_property, global_skylight_image_asset) AtomComponentProperties.global_skylight('Specular Image'), global_skylight_specular_image_asset.id)
global_skylight_specular_image_set = global_skylight_component.get_component_property_value( global_skylight_specular_image_set = global_skylight_component.get_component_property_value(
global_skylight_specular_image_property) AtomComponentProperties.global_skylight('Specular Image'))
Report.result( Report.result(
Tests.global_skylight_specular_image_set, global_skylight_specular_image_set == global_skylight_image_asset) Tests.global_skylight_specular_image_set,
global_skylight_specular_image_set == global_skylight_specular_image_asset.id)
# 11. Create a Ground Plane Entity with a Material component that is a child entity of the Default Level Entity. # 11. Create a Ground Plane Entity with a Material component that is a child entity of the Default Level Entity.
ground_plane_name = "Ground Plane" ground_plane_name = "Ground Plane"
ground_plane_entity = EditorEntity.create_editor_entity(ground_plane_name, default_level_entity.id) ground_plane_entity = EditorEntity.create_editor_entity(ground_plane_name, default_level_entity.id)
ground_plane_material_component = ground_plane_entity.add_component(MATERIAL_COMPONENT_NAME) ground_plane_material_component = ground_plane_entity.add_component(AtomComponentProperties.material())
Report.result( Report.result(
Tests.ground_plane_material_component_added, ground_plane_entity.has_component(MATERIAL_COMPONENT_NAME)) Tests.ground_plane_material_component_added,
ground_plane_entity.has_component(AtomComponentProperties.material()))
# 12. Set the Material Asset property of the Material component for the Ground Plane Entity. # 12. Set the Material Asset property of the Material component for the Ground Plane Entity.
ground_plane_entity.set_local_uniform_scale(32.0) ground_plane_entity.set_local_uniform_scale(32.0)
ground_plane_material_asset_path = os.path.join("Materials", "Presets", "PBR", "metal_chrome.azmaterial") ground_plane_material_asset_path = os.path.join("Materials", "Presets", "PBR", "metal_chrome.azmaterial")
ground_plane_material_asset = asset.AssetCatalogRequestBus( ground_plane_material_asset = Asset.find_asset_by_path(ground_plane_material_asset_path, False)
bus.Broadcast, "GetAssetIdByPath", ground_plane_material_asset_path, math.Uuid(), False)
ground_plane_material_asset_property = "Default Material|Material Asset"
ground_plane_material_component.set_component_property_value( ground_plane_material_component.set_component_property_value(
ground_plane_material_asset_property, ground_plane_material_asset) AtomComponentProperties.material('Material Asset'), ground_plane_material_asset.id)
Report.result( Report.result(
Tests.ground_plane_material_asset_set, Tests.ground_plane_material_asset_set,
ground_plane_material_component.get_component_property_value( ground_plane_material_component.get_component_property_value(
ground_plane_material_asset_property) == ground_plane_material_asset) AtomComponentProperties.material('Material Asset')) == ground_plane_material_asset.id)
# 13. Add the Mesh component to the Ground Plane Entity and set the Mesh component Mesh Asset property. # 13. Add the Mesh component to the Ground Plane Entity and set the Mesh component Mesh Asset property.
ground_plane_mesh_component = ground_plane_entity.add_component(MESH_COMPONENT_NAME) ground_plane_mesh_component = ground_plane_entity.add_component(AtomComponentProperties.mesh())
Report.result(Tests.mesh_component_added, ground_plane_entity.has_component(MESH_COMPONENT_NAME)) Report.result(Tests.mesh_component_added, ground_plane_entity.has_component(AtomComponentProperties.mesh()))
ground_plane_mesh_asset_path = os.path.join("Objects", "plane.azmodel") ground_plane_mesh_asset_path = os.path.join("TestData", "Objects", "plane.azmodel")
ground_plane_mesh_asset = asset.AssetCatalogRequestBus( ground_plane_mesh_asset = Asset.find_asset_by_path(ground_plane_mesh_asset_path, False)
bus.Broadcast, "GetAssetIdByPath", ground_plane_mesh_asset_path, math.Uuid(), False)
ground_plane_mesh_asset_property = "Controller|Configuration|Mesh Asset"
ground_plane_mesh_component.set_component_property_value( ground_plane_mesh_component.set_component_property_value(
ground_plane_mesh_asset_property, ground_plane_mesh_asset) AtomComponentProperties.mesh('Mesh Asset'), ground_plane_mesh_asset.id)
Report.result( Report.result(
Tests.ground_plane_mesh_asset_set, Tests.ground_plane_mesh_asset_set,
ground_plane_mesh_component.get_component_property_value( ground_plane_mesh_component.get_component_property_value(
ground_plane_mesh_asset_property) == ground_plane_mesh_asset) AtomComponentProperties.mesh('Mesh Asset')) == ground_plane_mesh_asset.id)
# 14. Create a Directional Light Entity as a child entity of the Default Level Entity. # 14. Create a Directional Light Entity as a child entity of the Default Level Entity.
directional_light_name = "Directional Light"
directional_light_entity = EditorEntity.create_editor_entity_at( directional_light_entity = EditorEntity.create_editor_entity_at(
math.Vector3(0.0, 0.0, 10.0), directional_light_name, default_level_entity.id) math.Vector3(0.0, 0.0, 10.0), AtomComponentProperties.directional_light(), default_level_entity.id)
# 15. Add Directional Light component to Directional Light Entity and set entity rotation. # 15. Add Directional Light component to Directional Light Entity and set entity rotation.
directional_light_entity.add_component(directional_light_name) directional_light_entity.add_component(AtomComponentProperties.directional_light())
directional_light_entity_rotation = math.Vector3(DEGREE_RADIAN_FACTOR * -90.0, 0.0, 0.0) directional_light_entity_rotation = math.Vector3(DEGREE_RADIAN_FACTOR * -90.0, 0.0, 0.0)
directional_light_entity.set_local_rotation(directional_light_entity_rotation) directional_light_entity.set_local_rotation(directional_light_entity_rotation)
Report.result( Report.result(
Tests.directional_light_component_added, directional_light_entity.has_component(directional_light_name)) Tests.directional_light_component_added, directional_light_entity.has_component(
AtomComponentProperties.directional_light()))
# 16. Create a Sphere Entity as a child entity of the Default Level Entity then add a Material component. # 16. Create a Sphere Entity as a child entity of the Default Level Entity then add a Material component.
sphere_entity = EditorEntity.create_editor_entity_at( sphere_entity = EditorEntity.create_editor_entity_at(
math.Vector3(0.0, 0.0, 1.0), "Sphere", default_level_entity.id) math.Vector3(0.0, 0.0, 1.0), "Sphere", default_level_entity.id)
sphere_material_component = sphere_entity.add_component(MATERIAL_COMPONENT_NAME) sphere_material_component = sphere_entity.add_component(AtomComponentProperties.material())
Report.result(Tests.sphere_material_component_added, sphere_entity.has_component(MATERIAL_COMPONENT_NAME)) Report.result(Tests.sphere_material_component_added, sphere_entity.has_component(
AtomComponentProperties.material()))
# 17. Set the Material Asset property of the Material component for the Sphere Entity. # 17. Set the Material Asset property of the Material component for the Sphere Entity.
sphere_material_asset_path = os.path.join("Materials", "Presets", "PBR", "metal_brass_polished.azmaterial") sphere_material_asset_path = os.path.join("Materials", "Presets", "PBR", "metal_brass_polished.azmaterial")
sphere_material_asset = asset.AssetCatalogRequestBus( sphere_material_asset = Asset.find_asset_by_path(sphere_material_asset_path, False)
bus.Broadcast, "GetAssetIdByPath", sphere_material_asset_path, math.Uuid(), False) sphere_material_component.set_component_property_value(
sphere_material_asset_property = "Default Material|Material Asset" AtomComponentProperties.material('Material Asset'), sphere_material_asset.id)
sphere_material_component.set_component_property_value(sphere_material_asset_property, sphere_material_asset)
Report.result(Tests.sphere_material_set, sphere_material_component.get_component_property_value( Report.result(Tests.sphere_material_set, sphere_material_component.get_component_property_value(
sphere_material_asset_property) == sphere_material_asset) AtomComponentProperties.material('Material Asset')) == sphere_material_asset.id)
# 18. Add Mesh component to Sphere Entity and set the Mesh Asset property for the Mesh component. # 18. Add Mesh component to Sphere Entity and set the Mesh Asset property for the Mesh component.
sphere_mesh_component = sphere_entity.add_component(MESH_COMPONENT_NAME) sphere_mesh_component = sphere_entity.add_component(AtomComponentProperties.mesh())
sphere_mesh_asset_path = os.path.join("Models", "sphere.azmodel") sphere_mesh_asset_path = os.path.join("Models", "sphere.azmodel")
sphere_mesh_asset = asset.AssetCatalogRequestBus( sphere_mesh_asset = Asset.find_asset_by_path(sphere_mesh_asset_path, False)
bus.Broadcast, "GetAssetIdByPath", sphere_mesh_asset_path, math.Uuid(), False) sphere_mesh_component.set_component_property_value(
sphere_mesh_asset_property = "Controller|Configuration|Mesh Asset" AtomComponentProperties.mesh('Mesh Asset'), sphere_mesh_asset.id)
sphere_mesh_component.set_component_property_value(sphere_mesh_asset_property, sphere_mesh_asset)
Report.result(Tests.sphere_mesh_asset_set, sphere_mesh_component.get_component_property_value( Report.result(Tests.sphere_mesh_asset_set, sphere_mesh_component.get_component_property_value(
sphere_mesh_asset_property) == sphere_mesh_asset) AtomComponentProperties.mesh('Mesh Asset')) == sphere_mesh_asset.id)
# 19. Create a Camera Entity as a child entity of the Default Level Entity then add a Camera component. # 19. Create a Camera Entity as a child entity of the Default Level Entity then add a Camera component.
camera_name = "Camera"
camera_entity = EditorEntity.create_editor_entity_at( camera_entity = EditorEntity.create_editor_entity_at(
math.Vector3(5.5, -12.0, 9.0), camera_name, default_level_entity.id) math.Vector3(5.5, -12.0, 9.0), AtomComponentProperties.camera(), default_level_entity.id)
camera_component = camera_entity.add_component(camera_name) camera_component = camera_entity.add_component(AtomComponentProperties.camera())
Report.result(Tests.camera_component_added, camera_entity.has_component(camera_name)) Report.result(Tests.camera_component_added, camera_entity.has_component(AtomComponentProperties.camera()))
# 20. Set the Camera Entity rotation value and set the Camera component Field of View value. # 20. Set the Camera Entity rotation value and set the Camera component Field of View value.
camera_entity_rotation = math.Vector3( camera_entity_rotation = math.Vector3(
DEGREE_RADIAN_FACTOR * -27.0, DEGREE_RADIAN_FACTOR * -12.0, DEGREE_RADIAN_FACTOR * 25.0) DEGREE_RADIAN_FACTOR * -27.0, DEGREE_RADIAN_FACTOR * -12.0, DEGREE_RADIAN_FACTOR * 25.0)
camera_entity.set_local_rotation(camera_entity_rotation) camera_entity.set_local_rotation(camera_entity_rotation)
camera_fov_property = "Controller|Configuration|Field of view"
camera_fov_value = 60.0 camera_fov_value = 60.0
camera_component.set_component_property_value(camera_fov_property, camera_fov_value) camera_component.set_component_property_value(AtomComponentProperties.camera('Field of view'), camera_fov_value)
azlmbr.camera.EditorCameraViewRequestBus(azlmbr.bus.Event, "ToggleCameraAsActiveView", camera_entity.id) azlmbr.camera.EditorCameraViewRequestBus(azlmbr.bus.Event, "ToggleCameraAsActiveView", camera_entity.id)
Report.result(Tests.camera_fov_set, camera_component.get_component_property_value( Report.result(Tests.camera_fov_set, camera_component.get_component_property_value(
camera_fov_property) == camera_fov_value) AtomComponentProperties.camera('Field of view')) == camera_fov_value)
# 21. Enter game mode. # 21. Enter game mode.
helper.enter_game_mode(Tests.enter_game_mode) TestHelper.enter_game_mode(Tests.enter_game_mode)
helper.wait_for_condition(function=lambda: general.is_in_game_mode(), timeout_in_seconds=4.0) TestHelper.wait_for_condition(function=lambda: general.is_in_game_mode(), timeout_in_seconds=4.0)
# 22. Take screenshot. # 22. Take screenshot.
ScreenshotHelper(general.idle_wait_frames).capture_screenshot_blocking(f"{SCREENSHOT_NAME}.ppm") ScreenshotHelper(general.idle_wait_frames).capture_screenshot_blocking(f"{SCREENSHOT_NAME}.ppm")
# 23. Exit game mode. # 23. Exit game mode.
helper.exit_game_mode(Tests.exit_game_mode) TestHelper.exit_game_mode(Tests.exit_game_mode)
helper.wait_for_condition(function=lambda: not general.is_in_game_mode(), timeout_in_seconds=4.0) TestHelper.wait_for_condition(function=lambda: not general.is_in_game_mode(), timeout_in_seconds=4.0)
# 24. Look for errors. # 24. Look for errors.
helper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0) TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
Report.result(Tests.no_assert_occurred, not error_tracer.has_asserts) for error_info in error_tracer.errors:
Report.result(Tests.no_error_occurred, not error_tracer.has_errors) Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__": if __name__ == "__main__":

@ -131,8 +131,7 @@ def run():
components=["HDRi Skybox", "Global Skylight (IBL)"], components=["HDRi Skybox", "Global Skylight (IBL)"],
parent_id=default_level.id parent_id=default_level.id
) )
global_skylight_image_asset_path = os.path.join( global_skylight_image_asset_path = os.path.join("LightingPresets", "default_iblskyboxcm.exr.streamingimage")
"LightingPresets", "greenwich_park_02_4k_iblskyboxcm_iblspecular.exr.streamingimage")
global_skylight_image_asset = asset.AssetCatalogRequestBus( global_skylight_image_asset = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", global_skylight_image_asset_path, math.Uuid(), False) bus.Broadcast, "GetAssetIdByPath", global_skylight_image_asset_path, math.Uuid(), False)
global_skylight.get_set_test(0, "Controller|Configuration|Cubemap Texture", global_skylight_image_asset) global_skylight.get_set_test(0, "Controller|Configuration|Cubemap Texture", global_skylight_image_asset)

@ -57,6 +57,10 @@ def add_level_component(component_name):
level_component_list, entity.EntityType().Level) level_component_list, entity.EntityType().Level)
level_component_outcome = editor.EditorLevelComponentAPIBus(bus.Broadcast, 'AddComponentsOfType', level_component_outcome = editor.EditorLevelComponentAPIBus(bus.Broadcast, 'AddComponentsOfType',
[level_component_type_ids_list[0]]) [level_component_type_ids_list[0]])
if not level_component_outcome.IsSuccess():
print('Failed to add {} level component'.format(component_name))
return None
level_component = level_component_outcome.GetValue()[0] level_component = level_component_outcome.GetValue()[0]
return level_component return level_component

@ -8,42 +8,54 @@ import azlmbr.bus
import azlmbr.asset import azlmbr.asset
import azlmbr.editor import azlmbr.editor
import azlmbr.math import azlmbr.math
import azlmbr.legacy.general
def raise_and_stop(msg): print('Starting mock asset tests')
print (msg) handler = azlmbr.editor.EditorEventBusHandler()
azlmbr.editor.EditorToolsApplicationRequestBus(azlmbr.bus.Broadcast, 'ExitNoPrompt')
def on_notify_editor_initialized(args):
# These tests are meant to check that the test_asset.mock source asset turned into # These tests are meant to check that the test_asset.mock source asset turned into
# a test_asset.mock_asset product asset via the Python asset builder system # a test_asset.mock_asset product asset via the Python asset builder system
mockAssetType = azlmbr.math.Uuid_CreateString('{9274AD17-3212-4651-9F3B-7DCCB080E467}', 0) mockAssetType = azlmbr.math.Uuid_CreateString('{9274AD17-3212-4651-9F3B-7DCCB080E467}', 0)
mockAssetPath = 'gem/pythontests/pythonassetbuilder/test_asset.mock_asset' mockAssetPath = 'gem/pythontests/pythonassetbuilder/test_asset.mock_asset'
assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', mockAssetPath, mockAssetType, False) assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', mockAssetPath, mockAssetType, False)
if (assetId.is_valid() is False): if (assetId.is_valid() is False):
raise_and_stop(f'Mock AssetId is not valid! Got {assetId.to_string()} instead') print(f'Mock AssetId is not valid! Got {assetId.to_string()} instead')
else:
assetIdString = assetId.to_string() print(f'Mock AssetId is valid!')
if (assetIdString.endswith(':528cca58') is False):
raise_and_stop(f'Mock AssetId {assetIdString} has unexpected sub-id for {mockAssetPath}!')
print ('Mock asset exists')
# These tests detect if the geom_group.fbx file turns into a number of azmodel product assets
def test_azmodel_product(generatedModelAssetPath):
azModelAssetType = azlmbr.math.Uuid_CreateString('{2C7477B6-69C5-45BE-8163-BCD6A275B6D8}', 0)
assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', generatedModelAssetPath, azModelAssetType, False)
assetIdString = assetId.to_string() assetIdString = assetId.to_string()
if (assetId.is_valid()): if (assetIdString.endswith(':528cca58') is False):
print(f'AssetId found for asset ({generatedModelAssetPath}) found') print(f'Mock AssetId {assetIdString} has unexpected sub-id for {mockAssetPath}!')
else: else:
raise_and_stop(f'Asset at path {generatedModelAssetPath} has unexpected asset ID ({assetIdString})!') print(f'Mock AssetId has expected sub-id for {mockAssetPath}!')
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_z_positive_1.azmodel') print ('Mock asset exists')
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_z_negative_1.azmodel')
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_y_positive_1.azmodel') # These tests detect if the geom_group.fbx file turns into a number of azmodel product assets
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_y_negative_1.azmodel') def test_azmodel_product(generatedModelAssetPath):
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_x_positive_1.azmodel') azModelAssetType = azlmbr.math.Uuid_CreateString('{2C7477B6-69C5-45BE-8163-BCD6A275B6D8}', 0)
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_x_negative_1.azmodel') assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', generatedModelAssetPath, azModelAssetType, False)
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center_1.azmodel') assetIdString = assetId.to_string()
if (assetId.is_valid()):
print(f'AssetId found for asset ({generatedModelAssetPath}) found')
else:
print(f'Asset at path {generatedModelAssetPath} has unexpected asset ID ({assetIdString})!')
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_z_positive_1.azmodel')
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_z_negative_1.azmodel')
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_y_positive_1.azmodel')
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_y_negative_1.azmodel')
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_x_positive_1.azmodel')
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_x_negative_1.azmodel')
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center_1.azmodel')
# clear up notification handler
global handler
handler.disconnect()
handler = None
print('Finished mock asset tests')
azlmbr.editor.EditorToolsApplicationRequestBus(azlmbr.bus.Broadcast, 'ExitNoPrompt')
azlmbr.editor.EditorToolsApplicationRequestBus(azlmbr.bus.Broadcast, 'ExitNoPrompt') handler.connect()
handler.add_callback('NotifyEditorInitialized', on_notify_editor_initialized)

@ -12,13 +12,12 @@ class Tests():
add_terrain_collider = ("Terrain Physics Heightfield Collider component added", "Failed to add a Terrain Physics Heightfield Collider component") add_terrain_collider = ("Terrain Physics Heightfield Collider component added", "Failed to add a Terrain Physics Heightfield Collider component")
box_dimensions_changed = ("Aabb dimensions changed successfully", "Failed change Aabb dimensions") box_dimensions_changed = ("Aabb dimensions changed successfully", "Failed change Aabb dimensions")
configuration_changed = ("Terrain size changed successfully", "Failed terrain size change") configuration_changed = ("Terrain size changed successfully", "Failed terrain size change")
no_errors_and_warnings_found = ("No errors and warnings found", "Found errors and warnings")
#fmt: on #fmt: on
def TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges(): def TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges():
""" """
Summary: Summary:
Test aspects of the TerrainHeightGradientList through the BehaviorContext and the Property Tree. Test aspects of the Terrain Physics Heightfield Collider through the BehaviorContext and the Property Tree.
Test Steps: Test Steps:
Expected Behavior: Expected Behavior:

@ -0,0 +1,164 @@
"""
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
"""
#fmt: off
class Tests():
create_terrain_spawner_entity = ("Terrain_spawner_entity created successfully", "Failed to create terrain_spawner_entity")
create_height_provider_entity = ("Height_provider_entity created successfully", "Failed to create height_provider_entity")
create_test_ball = ("Ball created successfully", "Failed to create Ball")
box_dimensions_changed = ("Aabb dimensions changed successfully", "Failed change Aabb dimensions")
shape_changed = ("Shape changed successfully", "Failed Shape change")
entity_added = ("Entity added successfully", "Failed Entity add")
frequency_changed = ("Frequency changed successfully", "Failed Frequency change")
shape_set = ("Shape set to Sphere successfully", "Failed to set Sphere shape")
test_collision = ("Ball collided with terrain", "Ball failed to collide with terrain")
no_errors_and_warnings_found = ("No errors and warnings found", "Found errors and warnings")
#fmt: on
def Terrain_SupportsPhysics():
"""
Summary:
Test aspects of the TerrainHeightGradientList through the BehaviorContext and the Property Tree.
Test Steps:
Expected Behavior:
The Editor is stable there are no warnings or errors.
Test Steps:
1) Load the base level
2) Create 2 test entities, one parent at 512.0, 512.0, 50.0 and one child at the default position and add the required components
2a) Create a ball at 600.0, 600.0, 46.0 - This position is bot too high over the heighfield so will collide in a reasonable time
3) Start the Tracer to catch any errors and warnings
4) Change the Axis Aligned Box Shape dimensions
5) Set the Vegetation Shape reference to TestEntity1
6) Set the FastNoise gradient frequency to 0.01
7) Set the Gradient List to TestEntity2
8) Set the PhysX Collider to Sphere mode
9) Disable and Enable the Terrain Gradient List so that it is recognised
10) Enter game mode and test if the ball hits the heightfield within 3 seconds
11) Verify there are no errors and warnings in the logs
:return: None
"""
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import TestHelper as helper, Report
from editor_python_test_tools.utils import Report, Tracer
import editor_python_test_tools.hydra_editor_utils as hydra
import azlmbr.math as azmath
import azlmbr.legacy.general as general
import azlmbr.bus as bus
import azlmbr.editor as editor
import math
SET_BOX_X_SIZE = 1024.0
SET_BOX_Y_SIZE = 1024.0
SET_BOX_Z_SIZE = 100.0
helper.init_idle()
# 1) Load the level
helper.open_level("", "Base")
helper.wait_for_condition(lambda: general.get_current_level_name() == "Base", 2.0)
#1a) Load the level components
hydra.add_level_component("Terrain World")
hydra.add_level_component("Terrain World Renderer")
# 2) Create 2 test entities, one parent at 512.0, 512.0, 50.0 and one child at the default position and add the required components
entity1_components_to_add = ["Axis Aligned Box Shape", "Terrain Layer Spawner", "Terrain Height Gradient List", "Terrain Physics Heightfield Collider", "PhysX Heightfield Collider"]
entity2_components_to_add = ["Vegetation Reference Shape", "Gradient Transform Modifier", "FastNoise Gradient"]
ball_components_to_add = ["Sphere Shape", "PhysX Collider", "PhysX Rigid Body"]
terrain_spawner_entity = hydra.Entity("TestEntity1")
terrain_spawner_entity.create_entity(azmath.Vector3(512.0, 512.0, 50.0), entity1_components_to_add)
Report.result(Tests.create_terrain_spawner_entity, terrain_spawner_entity.id.IsValid())
height_provider_entity = hydra.Entity("TestEntity2")
height_provider_entity.create_entity(azmath.Vector3(0.0, 0.0, 0.0), entity2_components_to_add,terrain_spawner_entity.id)
Report.result(Tests.create_height_provider_entity, height_provider_entity.id.IsValid())
# 2a) Create a ball at 600.0, 600.0, 46.0 - This position is bot too high over the heighfield so will collide in a reasonable time
ball = hydra.Entity("Ball")
ball.create_entity(azmath.Vector3(600.0, 600.0, 46.0), ball_components_to_add)
Report.result(Tests.create_test_ball, ball.id.IsValid())
# Give everything a chance to finish initializing.
general.idle_wait_frames(1)
# 3) Start the Tracer to catch any errors and warnings
with Tracer() as section_tracer:
# 4) Change the Axis Aligned Box Shape dimensions
box_dimensions = azmath.Vector3(SET_BOX_X_SIZE, SET_BOX_Y_SIZE, SET_BOX_Z_SIZE)
terrain_spawner_entity.get_set_test(0, "Axis Aligned Box Shape|Box Configuration|Dimensions", box_dimensions)
box_shape_dimensions = hydra.get_component_property_value(terrain_spawner_entity.components[0], "Axis Aligned Box Shape|Box Configuration|Dimensions")
Report.result(Tests.box_dimensions_changed, box_dimensions == box_shape_dimensions)
# 5) Set the Vegetaion Shape reference to TestEntity1
height_provider_entity.get_set_test(0, "Configuration|Shape Entity Id", terrain_spawner_entity.id)
entityId = hydra.get_component_property_value(height_provider_entity.components[0], "Configuration|Shape Entity Id")
Report.result(Tests.shape_changed, entityId == terrain_spawner_entity.id)
# 6) Set the FastNoise Gradient frequency to 0.01
Frequency = 0.01
height_provider_entity.get_set_test(2, "Configuration|Frequency", Frequency)
FrequencyVal = hydra.get_component_property_value(height_provider_entity.components[2], "Configuration|Frequency")
Report.result(Tests.frequency_changed, math.isclose(Frequency, FrequencyVal, abs_tol = 0.00001))
# 7) Set the Gradient List to TestEntity2
pte = hydra.get_property_tree(terrain_spawner_entity.components[2])
pte.add_container_item("Configuration|Gradient Entities", 0, height_provider_entity.id)
checkID = pte.get_container_item("Configuration|Gradient Entities", 0)
Report.result(Tests.entity_added, checkID.GetValue() == height_provider_entity.id)
# 8) Set the PhysX Collider to Sphere mode
shape = 0
hydra.get_set_test(ball, 1, "Shape Configuration|Shape", shape)
setShape = hydra.get_component_property_value(ball.components[1], "Shape Configuration|Shape")
Report.result(Tests.shape_set, shape == setShape)
# 9) Disable and Enable the Terrain Gradient List so that it is recognised
editor.EditorComponentAPIBus(bus.Broadcast, 'EnableComponents', [terrain_spawner_entity.components[2]])
general.enter_game_mode()
general.idle_wait_frames(1)
# 10) Enter game mode and test if the ball hits the heightfield within 3 seconds
TIMEOUT = 3.0
class Collider:
id = general.find_game_entity("Ball")
touched_ground = False
terrain_id = general.find_game_entity("TestEntity1")
def on_collision_begin(args):
other_id = args[0]
if other_id.Equal(terrain_id):
Report.info("Touched ground")
Collider.touched_ground = True
handler = azlmbr.physics.CollisionNotificationBusHandler()
handler.connect(Collider.id)
handler.add_callback("OnCollisionBegin", on_collision_begin)
helper.wait_for_condition(lambda: Collider.touched_ground, TIMEOUT)
Report.result(Tests.test_collision, Collider.touched_ground)
general.exit_game_mode()
# 11) Verify there are no errors and warnings in the logs
helper.wait_for_condition(lambda: section_tracer.has_errors or section_tracer.has_asserts, 1.0)
for error_info in section_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in section_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(Terrain_SupportsPhysics)

@ -19,7 +19,9 @@ from ly_test_tools.o3de.editor_test import EditorTestSuite, EditorSingleTest
@pytest.mark.parametrize("launcher_platform", ['windows_editor']) @pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("project", ["AutomatedTesting"]) @pytest.mark.parametrize("project", ["AutomatedTesting"])
class TestAutomation(EditorTestSuite): class TestAutomation(EditorTestSuite):
#global_extra_cmdline_args=["--regset=/Amazon/Preferences/EnablePrefabSystem=true"]
class test_AxisAlignedBoxShape_ConfigurationWorks(EditorSingleTest): class test_AxisAlignedBoxShape_ConfigurationWorks(EditorSingleTest):
from .EditorScripts import TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges as test_module from .EditorScripts import TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges as test_module
class test_Terrain_SupportsPhysics(EditorSingleTest):
from .EditorScripts import Terrain_SupportsPhysics as test_module

@ -17,7 +17,16 @@
}, },
"Component_[14126657869720434043]": { "Component_[14126657869720434043]": {
"$type": "EditorEntitySortComponent", "$type": "EditorEntitySortComponent",
"Id": 14126657869720434043 "Id": 14126657869720434043,
"ChildEntityOrderEntryArray": [
{
"EntityId": ""
},
{
"EntityId": "",
"SortIndex": 1
}
]
}, },
"Component_[15230859088967841193]": { "Component_[15230859088967841193]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent", "$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",

@ -547,13 +547,6 @@ QPoint Q2DViewport::WorldToView(const Vec3& wp) const
QPoint p = QPoint(static_cast<int>(sp.x), static_cast<int>(sp.y)); QPoint p = QPoint(static_cast<int>(sp.x), static_cast<int>(sp.y));
return p; return p;
} }
//////////////////////////////////////////////////////////////////////////
QPoint Q2DViewport::WorldToViewParticleEditor(const Vec3& wp, [[maybe_unused]] int width, [[maybe_unused]] int height) const //Eric@conffx implement for the children class of IDisplayViewport
{
Vec3 sp = m_screenTM.TransformPoint(wp);
QPoint p = QPoint(static_cast<int>(sp.x), static_cast<int>(sp.y));
return p;
}
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
Vec3 Q2DViewport::ViewToWorld(const QPoint& vp, [[maybe_unused]] bool* collideWithTerrain, [[maybe_unused]] bool onlyTerrain, [[maybe_unused]] bool bSkipVegetation, [[maybe_unused]] bool bTestRenderMesh, [[maybe_unused]] bool* collideWithObject) const Vec3 Q2DViewport::ViewToWorld(const QPoint& vp, [[maybe_unused]] bool* collideWithTerrain, [[maybe_unused]] bool onlyTerrain, [[maybe_unused]] bool bSkipVegetation, [[maybe_unused]] bool bTestRenderMesh, [[maybe_unused]] bool* collideWithObject) const

@ -50,8 +50,6 @@ public:
//! Map world space position to viewport position. //! Map world space position to viewport position.
QPoint WorldToView(const Vec3& wp) const override; QPoint WorldToView(const Vec3& wp) const override;
QPoint WorldToViewParticleEditor(const Vec3& wp, int width, int height) const override; //Eric@conffx
//! Map viewport position to world space position. //! Map viewport position to world space position.
Vec3 ViewToWorld(const QPoint& vp, bool* collideWithTerrain = nullptr, bool onlyTerrain = false, bool bSkipVegetation = false, bool bTestRenderMesh = false, bool* collideWithObject = nullptr) const override; Vec3 ViewToWorld(const QPoint& vp, bool* collideWithTerrain = nullptr, bool onlyTerrain = false, bool bSkipVegetation = false, bool bTestRenderMesh = false, bool* collideWithObject = nullptr) const override;
//! Map viewport position to world space ray from camera. //! Map viewport position to world space ray from camera.

@ -145,6 +145,15 @@ namespace SandboxEditor
} }
}; };
const auto trackingTransform = [viewportId = m_viewportId]
{
bool tracking = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
tracking, viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsTrackingTransform);
return tracking;
};
m_firstPersonRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(SandboxEditor::CameraFreeLookChannelId()); m_firstPersonRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(SandboxEditor::CameraFreeLookChannelId());
m_firstPersonRotateCamera->m_rotateSpeedFn = [] m_firstPersonRotateCamera->m_rotateSpeedFn = []
@ -152,6 +161,11 @@ namespace SandboxEditor
return SandboxEditor::CameraRotateSpeed(); return SandboxEditor::CameraRotateSpeed();
}; };
m_firstPersonRotateCamera->m_constrainPitch = [trackingTransform]
{
return !trackingTransform();
};
// default behavior is to hide the cursor but this can be disabled (useful for remote desktop) // default behavior is to hide the cursor but this can be disabled (useful for remote desktop)
// note: See CaptureCursorLook in the Settings Registry // note: See CaptureCursorLook in the Settings Registry
m_firstPersonRotateCamera->SetActivationBeganFn(hideCursor); m_firstPersonRotateCamera->SetActivationBeganFn(hideCursor);
@ -255,6 +269,11 @@ namespace SandboxEditor
return SandboxEditor::CameraOrbitYawRotationInverted(); return SandboxEditor::CameraOrbitYawRotationInverted();
}; };
m_orbitRotateCamera->m_constrainPitch = [trackingTransform]
{
return !trackingTransform();
};
m_orbitTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>( m_orbitTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>(
translateCameraInputChannelIds, AzFramework::LookTranslation, AzFramework::TranslateOffsetOrbit); translateCameraInputChannelIds, AzFramework::LookTranslation, AzFramework::TranslateOffsetOrbit);
@ -337,12 +356,12 @@ namespace SandboxEditor
AZ::TransformBus::EventResult(worldFromLocal, viewEntityId, &AZ::TransformBus::Events::GetWorldTM); AZ::TransformBus::EventResult(worldFromLocal, viewEntityId, &AZ::TransformBus::Events::GetWorldTM);
AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event( AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::SetReferenceFrame, worldFromLocal); m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, worldFromLocal);
} }
else else
{ {
AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event( AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::ClearReferenceFrame); m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StopTrackingTransform);
} }
} }

@ -299,13 +299,9 @@ AzToolsFramework::ViewportInteraction::MousePick EditorViewportWidget::BuildMous
{ {
AzToolsFramework::ViewportInteraction::MousePick mousePick; AzToolsFramework::ViewportInteraction::MousePick mousePick;
mousePick.m_screenCoordinates = AzToolsFramework::ViewportInteraction::ScreenPointFromQPoint(point); mousePick.m_screenCoordinates = AzToolsFramework::ViewportInteraction::ScreenPointFromQPoint(point);
if (const auto& ray = m_renderViewport->ViewportScreenToWorldRay(mousePick.m_screenCoordinates); const auto[origin, direction] = m_renderViewport->ViewportScreenToWorldRay(mousePick.m_screenCoordinates);
ray.has_value()) mousePick.m_rayOrigin = origin;
{ mousePick.m_rayDirection = direction;
mousePick.m_rayOrigin = ray.value().origin;
mousePick.m_rayDirection = ray.value().direction;
}
return mousePick; return mousePick;
} }
@ -912,23 +908,6 @@ AZ::Vector3 EditorViewportWidget::PickTerrain(const AzFramework::ScreenPoint& po
return LYVec3ToAZVec3(ViewToWorld(AzToolsFramework::ViewportInteraction::QPointFromScreenPoint(point), nullptr, true)); return LYVec3ToAZVec3(ViewToWorld(AzToolsFramework::ViewportInteraction::QPointFromScreenPoint(point), nullptr, true));
} }
AZ::EntityId EditorViewportWidget::PickEntity(const AzFramework::ScreenPoint& point)
{
AZ::EntityId entityId;
HitContext hitInfo;
hitInfo.view = this;
if (HitTest(AzToolsFramework::ViewportInteraction::QPointFromScreenPoint(point), hitInfo))
{
if (hitInfo.object && (hitInfo.object->GetType() == OBJTYPE_AZENTITY))
{
auto entityObject = static_cast<CComponentEntityObject*>(hitInfo.object);
entityId = entityObject->GetAssociatedEntityId();
}
}
return entityId;
}
float EditorViewportWidget::TerrainHeight(const AZ::Vector2& position) float EditorViewportWidget::TerrainHeight(const AZ::Vector2& position)
{ {
return GetIEditor()->GetTerrainElevation(position.GetX(), position.GetY()); return GetIEditor()->GetTerrainElevation(position.GetX(), position.GetY());
@ -1653,16 +1632,15 @@ void EditorViewportWidget::RenderSelectedRegion()
Vec3 EditorViewportWidget::WorldToView3D(const Vec3& wp, [[maybe_unused]] int nFlags) const Vec3 EditorViewportWidget::WorldToView3D(const Vec3& wp, [[maybe_unused]] int nFlags) const
{ {
Vec3 out(0, 0, 0); Vec3 out(0, 0, 0);
float x, y, z; float x, y;
ProjectToScreen(wp.x, wp.y, wp.z, &x, &y, &z); ProjectToScreen(wp.x, wp.y, wp.z, &x, &y);
if (_finite(x) && _finite(y) && _finite(z)) if (_finite(x) && _finite(y))
{ {
out.x = (x / 100) * m_rcClient.width(); out.x = (x / 100) * m_rcClient.width();
out.y = (y / 100) * m_rcClient.height(); out.y = (y / 100) * m_rcClient.height();
out.x /= static_cast<float>(QHighDpiScaling::factor(windowHandle()->screen())); out.x /= static_cast<float>(QHighDpiScaling::factor(windowHandle()->screen()));
out.y /= static_cast<float>(QHighDpiScaling::factor(windowHandle()->screen())); out.y /= static_cast<float>(QHighDpiScaling::factor(windowHandle()->screen()));
out.z = z;
} }
return out; return out;
} }
@ -1672,24 +1650,6 @@ QPoint EditorViewportWidget::WorldToView(const Vec3& wp) const
{ {
return AzToolsFramework::ViewportInteraction::QPointFromScreenPoint(m_renderViewport->ViewportWorldToScreen(LYVec3ToAZVec3(wp))); return AzToolsFramework::ViewportInteraction::QPointFromScreenPoint(m_renderViewport->ViewportWorldToScreen(LYVec3ToAZVec3(wp)));
} }
//////////////////////////////////////////////////////////////////////////
QPoint EditorViewportWidget::WorldToViewParticleEditor(const Vec3& wp, int width, int height) const
{
QPoint p;
float x, y, z;
ProjectToScreen(wp.x, wp.y, wp.z, &x, &y, &z);
if (_finite(x) || _finite(y))
{
p.rx() = static_cast<int>((x / 100) * width);
p.ry() = static_cast<int>((y / 100) * height);
}
else
{
QPoint(0, 0);
}
return p;
}
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
Vec3 EditorViewportWidget::ViewToWorld( Vec3 EditorViewportWidget::ViewToWorld(
@ -1705,20 +1665,16 @@ Vec3 EditorViewportWidget::ViewToWorld(
AZ_UNUSED(collideWithObject); AZ_UNUSED(collideWithObject);
auto ray = m_renderViewport->ViewportScreenToWorldRay(AzToolsFramework::ViewportInteraction::ScreenPointFromQPoint(vp)); auto ray = m_renderViewport->ViewportScreenToWorldRay(AzToolsFramework::ViewportInteraction::ScreenPointFromQPoint(vp));
if (!ray.has_value())
{
return Vec3(0, 0, 0);
}
const float maxDistance = 10000.f; const float maxDistance = 10000.f;
Vec3 v = AZVec3ToLYVec3(ray.value().direction) * maxDistance; Vec3 v = AZVec3ToLYVec3(ray.direction) * maxDistance;
if (!_finite(v.x) || !_finite(v.y) || !_finite(v.z)) if (!_finite(v.x) || !_finite(v.y) || !_finite(v.z))
{ {
return Vec3(0, 0, 0); return Vec3(0, 0, 0);
} }
Vec3 colp = AZVec3ToLYVec3(ray.value().origin) + 0.002f * v; Vec3 colp = AZVec3ToLYVec3(ray.origin) + 0.002f * v;
return colp; return colp;
} }
@ -1757,21 +1713,19 @@ bool EditorViewportWidget::RayRenderMeshIntersection(IRenderMesh* pRenderMesh, c
return bRes;*/ return bRes;*/
} }
void EditorViewportWidget::UnProjectFromScreen(float sx, float sy, float sz, float* px, float* py, float* pz) const void EditorViewportWidget::UnProjectFromScreen(float sx, float sy, float* px, float* py, float* pz) const
{ {
AZ::Vector3 wp; const AZ::Vector3 wp = m_renderViewport->ViewportScreenToWorld(AzFramework::ScreenPoint{(int)sx, m_rcClient.bottom() - ((int)sy)});
wp = m_renderViewport->ViewportScreenToWorld(AzFramework::ScreenPoint{(int)sx, m_rcClient.bottom() - ((int)sy)}, sz).value_or(wp);
*px = wp.GetX(); *px = wp.GetX();
*py = wp.GetY(); *py = wp.GetY();
*pz = wp.GetZ(); *pz = wp.GetZ();
} }
void EditorViewportWidget::ProjectToScreen(float ptx, float pty, float ptz, float* sx, float* sy, float* sz) const void EditorViewportWidget::ProjectToScreen(float ptx, float pty, float ptz, float* sx, float* sy) const
{ {
AzFramework::ScreenPoint screenPosition = m_renderViewport->ViewportWorldToScreen(AZ::Vector3{ptx, pty, ptz}); AzFramework::ScreenPoint screenPosition = m_renderViewport->ViewportWorldToScreen(AZ::Vector3{ptx, pty, ptz});
*sx = static_cast<float>(screenPosition.m_x); *sx = static_cast<float>(screenPosition.m_x);
*sy = static_cast<float>(screenPosition.m_y); *sy = static_cast<float>(screenPosition.m_y);
*sz = 0.f;
} }
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
@ -1781,32 +1735,22 @@ void EditorViewportWidget::ViewToWorldRay(const QPoint& vp, Vec3& raySrc, Vec3&
Vec3 pos0, pos1; Vec3 pos0, pos1;
float wx, wy, wz; float wx, wy, wz;
UnProjectFromScreen(static_cast<float>(vp.x()), static_cast<float>(rc.bottom() - vp.y()), 0.0f, &wx, &wy, &wz); UnProjectFromScreen(static_cast<float>(vp.x()), static_cast<float>(rc.bottom() - vp.y()), &wx, &wy, &wz);
if (!_finite(wx) || !_finite(wy) || !_finite(wz))
{
return;
}
if (fabs(wx) > 1000000 || fabs(wy) > 1000000 || fabs(wz) > 1000000)
{
return;
}
pos0(wx, wy, wz);
UnProjectFromScreen(static_cast<float>(vp.x()), static_cast<float>(rc.bottom() - vp.y()), 1.0f, &wx, &wy, &wz);
if (!_finite(wx) || !_finite(wy) || !_finite(wz)) if (!_finite(wx) || !_finite(wy) || !_finite(wz))
{ {
return; return;
} }
if (fabs(wx) > 1000000 || fabs(wy) > 1000000 || fabs(wz) > 1000000) if (fabs(wx) > 1000000 || fabs(wy) > 1000000 || fabs(wz) > 1000000)
{ {
return; return;
} }
pos1(wx, wy, wz);
Vec3 v = (pos1 - pos0); pos0(wx, wy, wz);
v = v.GetNormalized();
raySrc = pos0; raySrc = pos0;
rayDir = v; rayDir = (pos0 - AZVec3ToLYVec3(m_renderViewport->GetCameraState().m_position)).GetNormalized();
} }
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////

@ -166,7 +166,6 @@ private:
Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers, const QPoint& point) override; Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers, const QPoint& point) override;
void SetViewportId(int id) override; void SetViewportId(int id) override;
QPoint WorldToView(const Vec3& wp) const override; QPoint WorldToView(const Vec3& wp) const override;
QPoint WorldToViewParticleEditor(const Vec3& wp, int width, int height) const override;
Vec3 WorldToView3D(const Vec3& wp, int nFlags = 0) const override; Vec3 WorldToView3D(const Vec3& wp, int nFlags = 0) const override;
Vec3 ViewToWorld(const QPoint& vp, bool* collideWithTerrain = nullptr, bool onlyTerrain = false, bool bSkipVegetation = false, bool bTestRenderMesh = false, bool* collideWithObject = nullptr) const override; Vec3 ViewToWorld(const QPoint& vp, bool* collideWithTerrain = nullptr, bool onlyTerrain = false, bool bSkipVegetation = false, bool bTestRenderMesh = false, bool* collideWithObject = nullptr) const override;
void ViewToWorldRay(const QPoint& vp, Vec3& raySrc, Vec3& rayDir) const override; void ViewToWorldRay(const QPoint& vp, Vec3& raySrc, Vec3& rayDir) const override;
@ -208,7 +207,6 @@ private:
void* GetSystemCursorConstraintWindow() const override; void* GetSystemCursorConstraintWindow() const override;
// AzToolsFramework::MainEditorViewportInteractionRequestBus overrides ... // AzToolsFramework::MainEditorViewportInteractionRequestBus overrides ...
AZ::EntityId PickEntity(const AzFramework::ScreenPoint& point) override;
AZ::Vector3 PickTerrain(const AzFramework::ScreenPoint& point) override; AZ::Vector3 PickTerrain(const AzFramework::ScreenPoint& point) override;
float TerrainHeight(const AZ::Vector2& position) override; float TerrainHeight(const AZ::Vector2& position) override;
bool ShowingWorldSpace() override; bool ShowingWorldSpace() override;
@ -306,8 +304,8 @@ private:
const DisplayContext& GetDisplayContext() const { return m_displayContext; } const DisplayContext& GetDisplayContext() const { return m_displayContext; }
CBaseObject* GetCameraObject() const; CBaseObject* GetCameraObject() const;
void UnProjectFromScreen(float sx, float sy, float sz, float* px, float* py, float* pz) const; void UnProjectFromScreen(float sx, float sy, float* px, float* py, float* pz) const;
void ProjectToScreen(float ptx, float pty, float ptz, float* sx, float* sy, float* sz) const; void ProjectToScreen(float ptx, float pty, float ptz, float* sx, float* sy) const;
AZ::RPI::ViewPtr GetCurrentAtomView() const; AZ::RPI::ViewPtr GetCurrentAtomView() const;

@ -47,7 +47,6 @@ struct IDisplayViewport
virtual const Matrix34& GetViewTM() const = 0; virtual const Matrix34& GetViewTM() const = 0;
virtual const Matrix34& GetScreenTM() const = 0; virtual const Matrix34& GetScreenTM() const = 0;
virtual QPoint WorldToView(const Vec3& worldPoint) const = 0; virtual QPoint WorldToView(const Vec3& worldPoint) const = 0;
virtual QPoint WorldToViewParticleEditor(const Vec3& worldPoint, int width, int height) const = 0;
virtual Vec3 WorldToView3D(const Vec3& worldPoint, int flags = 0) const = 0; virtual Vec3 WorldToView3D(const Vec3& worldPoint, int flags = 0) const = 0;
virtual Vec3 ViewToWorld(const QPoint& vp, bool* collideWithTerrain = nullptr, bool onlyTerrain = false, bool bSkipVegetation = false, bool bTestRenderMesh = false, bool* collideWithObject = nullptr) const = 0; virtual Vec3 ViewToWorld(const QPoint& vp, bool* collideWithTerrain = nullptr, bool onlyTerrain = false, bool bSkipVegetation = false, bool bTestRenderMesh = false, bool* collideWithObject = nullptr) const = 0;
virtual void ViewToWorldRay(const QPoint& vp, Vec3& raySrc, Vec3& rayDir) const = 0; virtual void ViewToWorldRay(const QPoint& vp, Vec3& raySrc, Vec3& rayDir) const = 0;

@ -38,7 +38,9 @@ namespace UnitTest
AzFramework::ViewportControllerListPtr m_controllerList; AzFramework::ViewportControllerListPtr m_controllerList;
AZStd::unique_ptr<AZ::Entity> m_entity; AZStd::unique_ptr<AZ::Entity> m_entity;
static const AzFramework::ViewportId TestViewportId; static inline constexpr AzFramework::ViewportId TestViewportId = 2345;
static inline constexpr float HalfInterpolateToTransformDuration =
AtomToolsFramework::ModularViewportCameraControllerRequests::InterpolateToTransformDuration * 0.5f;
void SetUp() override void SetUp() override
{ {
@ -77,8 +79,6 @@ namespace UnitTest
} }
}; };
const AzFramework::ViewportId EditorCameraFixture::TestViewportId = AzFramework::ViewportId(1337);
TEST_F(EditorCameraFixture, ModularViewportCameraControllerReferenceFrameUpdatedWhenViewportEntityisChanged) TEST_F(EditorCameraFixture, ModularViewportCameraControllerReferenceFrameUpdatedWhenViewportEntityisChanged)
{ {
// Given // Given
@ -92,8 +92,8 @@ namespace UnitTest
&Camera::EditorCameraNotificationBus::Events::OnViewportViewEntityChanged, m_entity->GetId()); &Camera::EditorCameraNotificationBus::Events::OnViewportViewEntityChanged, m_entity->GetId());
// ensure the viewport updates after the viewport view entity change // ensure the viewport updates after the viewport view entity change
const float deltaTime = 1.0f / 60.0f; // note: do a large step to ensure smoothing finishes (e.g. not 1.0f/60.0f)
m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() }); m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(2.0f), AZ::ScriptTimePoint() });
// retrieve updated camera transform // retrieve updated camera transform
const AZ::Transform cameraTransform = m_cameraViewportContextView->GetCameraTransform(); const AZ::Transform cameraTransform = m_cameraViewportContextView->GetCameraTransform();
@ -103,61 +103,40 @@ namespace UnitTest
EXPECT_THAT(cameraTransform, IsClose(entityTransform)); EXPECT_THAT(cameraTransform, IsClose(entityTransform));
} }
TEST_F(EditorCameraFixture, ReferenceFrameRemainsIdentityAfterExternalCameraTransformChangeWhenNotSet) TEST_F(EditorCameraFixture, TrackingTransformIsTrueAfterTransformIsTracked)
{
// Given
m_cameraViewportContextView->SetCameraTransform(AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 20.0f, 30.0f)));
// When
AZ::Transform referenceFrame = AZ::Transform::CreateTranslation(AZ::Vector3(1.0f, 2.0f, 3.0f));
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
referenceFrame, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::GetReferenceFrame);
// Then
// reference frame is still the identity
EXPECT_THAT(referenceFrame, IsClose(AZ::Transform::CreateIdentity()));
}
TEST_F(EditorCameraFixture, ExternalCameraTransformChangeWhenReferenceFrameIsSetUpdatesReferenceFrame)
{ {
// Given // Given/When
const AZ::Transform referenceFrame = AZ::Transform::CreateFromQuaternionAndTranslation( const AZ::Transform referenceFrame = AZ::Transform::CreateFromQuaternionAndTranslation(
AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)), AZ::Vector3(1.0f, 2.0f, 3.0f)); AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)), AZ::Vector3(1.0f, 2.0f, 3.0f));
AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event( AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::SetReferenceFrame, referenceFrame); TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, referenceFrame);
const AZ::Transform nextTransform = AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 20.0f, 30.0f));
m_cameraViewportContextView->SetCameraTransform(nextTransform);
// When bool trackingTransform = false;
AZ::Transform currentReferenceFrame = AZ::Transform::CreateTranslation(AZ::Vector3(1.0f, 2.0f, 3.0f));
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult( AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
currentReferenceFrame, TestViewportId, trackingTransform, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsTrackingTransform);
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::GetReferenceFrame);
// Then // Then
EXPECT_THAT(currentReferenceFrame, IsClose(nextTransform)); EXPECT_THAT(trackingTransform, ::testing::IsTrue());
} }
TEST_F(EditorCameraFixture, ReferenceFrameReturnedToIdentityAfterClear) TEST_F(EditorCameraFixture, TrackingTransformIsFalseAfterTransformIsStoppedBeingTracked)
{ {
// Given // Given
const AZ::Transform referenceFrame = AZ::Transform::CreateFromQuaternionAndTranslation( const AZ::Transform referenceFrame = AZ::Transform::CreateFromQuaternionAndTranslation(
AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)), AZ::Vector3(1.0f, 2.0f, 3.0f)); AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)), AZ::Vector3(1.0f, 2.0f, 3.0f));
AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event( AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::SetReferenceFrame, referenceFrame); TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, referenceFrame);
// When // When
AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event( AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::ClearReferenceFrame); TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StopTrackingTransform);
AZ::Transform currentReferenceFrame = AZ::Transform::CreateTranslation(AZ::Vector3(1.0f, 2.0f, 3.0f)); // Then
bool trackingTransform = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult( AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
currentReferenceFrame, TestViewportId, trackingTransform, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsTrackingTransform);
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::GetReferenceFrame);
// Then EXPECT_THAT(trackingTransform, ::testing::IsFalse());
EXPECT_THAT(currentReferenceFrame, IsClose(AZ::Transform::CreateIdentity()));
} }
TEST_F(EditorCameraFixture, InterpolateToTransform) TEST_F(EditorCameraFixture, InterpolateToTransform)
@ -170,8 +149,10 @@ namespace UnitTest
transformToInterpolateTo); transformToInterpolateTo);
// simulate interpolation // simulate interpolation
m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(0.5f), AZ::ScriptTimePoint() }); m_controllerList->UpdateViewport(
m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(0.5f), AZ::ScriptTimePoint() }); { TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
m_controllerList->UpdateViewport(
{ TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
const auto finalTransform = m_cameraViewportContextView->GetCameraTransform(); const auto finalTransform = m_cameraViewportContextView->GetCameraTransform();
@ -185,7 +166,7 @@ namespace UnitTest
const AZ::Transform referenceFrame = AZ::Transform::CreateFromQuaternionAndTranslation( const AZ::Transform referenceFrame = AZ::Transform::CreateFromQuaternionAndTranslation(
AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)), AZ::Vector3(1.0f, 2.0f, 3.0f)); AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)), AZ::Vector3(1.0f, 2.0f, 3.0f));
AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event( AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::SetReferenceFrame, referenceFrame); TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, referenceFrame);
AZ::Transform transformToInterpolateTo = AZ::Transform::CreateFromQuaternionAndTranslation( AZ::Transform transformToInterpolateTo = AZ::Transform::CreateFromQuaternionAndTranslation(
AZ::Quaternion::CreateRotationZ(AZ::DegToRad(90.0f)), AZ::Vector3(20.0f, 40.0f, 60.0f)); AZ::Quaternion::CreateRotationZ(AZ::DegToRad(90.0f)), AZ::Vector3(20.0f, 40.0f, 60.0f));
@ -196,19 +177,86 @@ namespace UnitTest
transformToInterpolateTo); transformToInterpolateTo);
// simulate interpolation // simulate interpolation
m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(0.5f), AZ::ScriptTimePoint() }); m_controllerList->UpdateViewport(
m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(0.5f), AZ::ScriptTimePoint() }); { TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
m_controllerList->UpdateViewport(
AZ::Transform currentReferenceFrame = AZ::Transform::CreateTranslation(AZ::Vector3(1.0f, 2.0f, 3.0f)); { TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
currentReferenceFrame, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::GetReferenceFrame);
const auto finalTransform = m_cameraViewportContextView->GetCameraTransform(); const auto finalTransform = m_cameraViewportContextView->GetCameraTransform();
// Then // Then
EXPECT_THAT(finalTransform, IsClose(transformToInterpolateTo)); EXPECT_THAT(finalTransform, IsClose(transformToInterpolateTo));
EXPECT_THAT(currentReferenceFrame, IsClose(AZ::Transform::CreateIdentity())); }
TEST_F(EditorCameraFixture, BeginningCameraInterpolationReturnsTrue)
{
// Given/When
bool interpolationBegan = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
interpolationBegan, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
// Then
EXPECT_THAT(interpolationBegan, ::testing::IsTrue());
}
TEST_F(EditorCameraFixture, CameraInterpolationDoesNotBeginDuringAnExistingInterpolation)
{
// Given/When
bool initialInterpolationBegan = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
initialInterpolationBegan, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
m_controllerList->UpdateViewport(
{ TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
bool nextInterpolationBegan = true;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
nextInterpolationBegan, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
bool interpolating = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
interpolating, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsInterpolating);
// Then
EXPECT_THAT(initialInterpolationBegan, ::testing::IsTrue());
EXPECT_THAT(nextInterpolationBegan, ::testing::IsFalse());
EXPECT_THAT(interpolating, ::testing::IsTrue());
}
TEST_F(EditorCameraFixture, CameraInterpolationCanBeginAfterAnInterpolationCompletes)
{
// Given/When
bool initialInterpolationBegan = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
initialInterpolationBegan, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
m_controllerList->UpdateViewport(
{ TestViewportId,
AzFramework::FloatSeconds(AtomToolsFramework::ModularViewportCameraControllerRequests::InterpolateToTransformDuration + 0.5f),
AZ::ScriptTimePoint() });
bool interpolating = true;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
interpolating, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsInterpolating);
bool nextInterpolationBegan = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
nextInterpolationBegan, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
// Then
EXPECT_THAT(initialInterpolationBegan, ::testing::IsTrue());
EXPECT_THAT(interpolating, ::testing::IsFalse());
EXPECT_THAT(nextInterpolationBegan, ::testing::IsTrue());
} }
} // namespace UnitTest } // namespace UnitTest

@ -74,7 +74,7 @@ namespace UnitTest
class ModularViewportCameraControllerFixture : public AllocatorsTestFixture class ModularViewportCameraControllerFixture : public AllocatorsTestFixture
{ {
public: public:
static const AzFramework::ViewportId TestViewportId; static inline constexpr AzFramework::ViewportId TestViewportId = 1234;
void SetUp() override void SetUp() override
{ {
@ -146,6 +146,17 @@ namespace UnitTest
controller->SetCameraPropsBuilderCallback( controller->SetCameraPropsBuilderCallback(
[](AzFramework::CameraProps& cameraProps) [](AzFramework::CameraProps& cameraProps)
{ {
// note: rotateSmoothness is also used for roll (not related to camera input directly)
cameraProps.m_rotateSmoothnessFn = []
{
return 5.0f;
};
cameraProps.m_translateSmoothnessFn = []
{
return 5.0f;
};
cameraProps.m_rotateSmoothingEnabledFn = [] cameraProps.m_rotateSmoothingEnabledFn = []
{ {
return false; return false;
@ -209,8 +220,6 @@ namespace UnitTest
AZStd::unique_ptr<SandboxEditor::EditorModularViewportCameraComposer> m_editorModularViewportCameraComposer; AZStd::unique_ptr<SandboxEditor::EditorModularViewportCameraComposer> m_editorModularViewportCameraComposer;
}; };
const AzFramework::ViewportId ModularViewportCameraControllerFixture::TestViewportId = AzFramework::ViewportId(0);
TEST_F(ModularViewportCameraControllerFixture, MouseMovementDoesNotAccumulateExcessiveDriftInModularViewportCameraWithVaryingDeltaTime) TEST_F(ModularViewportCameraControllerFixture, MouseMovementDoesNotAccumulateExcessiveDriftInModularViewportCameraWithVaryingDeltaTime)
{ {
SandboxEditor::SetCameraCaptureCursorForLook(false); SandboxEditor::SetCameraCaptureCursorForLook(false);

@ -1675,6 +1675,12 @@ void SandboxIntegrationManager::GoToEntitiesInViewports(const AzToolsFramework::
if (auto viewportContext = viewportContextManager->GetViewportContextById(viewIndex)) if (auto viewportContext = viewportContextManager->GetViewportContextById(viewIndex))
{ {
const AZ::Transform cameraTransform = viewportContext->GetCameraTransform(); const AZ::Transform cameraTransform = viewportContext->GetCameraTransform();
// do not attempt to interpolate to where we currently are
if (cameraTransform.GetTranslation().IsClose(center))
{
continue;
}
const AZ::Vector3 forward = (center - cameraTransform.GetTranslation()).GetNormalized(); const AZ::Vector3 forward = (center - cameraTransform.GetTranslation()).GetNormalized();
// move camera 25% further back than required // move camera 25% further back than required

@ -8,13 +8,15 @@
#include "ViewportManipulatorController.h" #include "ViewportManipulatorController.h"
#include <AzToolsFramework/Manipulators/ManipulatorManager.h> #include <AzCore/Script/ScriptTimePoint.h>
#include <AzToolsFramework/ViewportSelection/EditorInteractionSystemViewportSelectionRequestBus.h>
#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
#include <AzFramework/Input/Buses/Requests/InputSystemCursorRequestBus.h> #include <AzFramework/Input/Buses/Requests/InputSystemCursorRequestBus.h>
#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
#include <AzFramework/Viewport/ScreenGeometry.h> #include <AzFramework/Viewport/ScreenGeometry.h>
#include <AzCore/Script/ScriptTimePoint.h> #include <AzFramework/Viewport/ViewportScreen.h>
#include <AzToolsFramework/Manipulators/ManipulatorManager.h>
#include <AzToolsFramework/ViewportSelection/EditorInteractionSystemViewportSelectionRequestBus.h>
#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
#include <QApplication> #include <QApplication>
@ -87,8 +89,14 @@ namespace SandboxEditor
} }
using InteractionBus = AzToolsFramework::EditorInteractionSystemViewportSelectionRequestBus; using InteractionBus = AzToolsFramework::EditorInteractionSystemViewportSelectionRequestBus;
using namespace AzToolsFramework::ViewportInteraction;
using AzFramework::InputChannel; using AzFramework::InputChannel;
using AzToolsFramework::ViewportInteraction::KeyboardModifier;
using AzToolsFramework::ViewportInteraction::MouseButton;
using AzToolsFramework::ViewportInteraction::MouseEvent;
using AzToolsFramework::ViewportInteraction::MouseInteraction;
using AzToolsFramework::ViewportInteraction::MouseInteractionEvent;
using AzToolsFramework::ViewportInteraction::ProjectedViewportRay;
using AzToolsFramework::ViewportInteraction::ViewportInteractionRequestBus;
bool interactionHandled = false; bool interactionHandled = false;
float wheelDelta = 0.0f; float wheelDelta = 0.0f;
@ -117,16 +125,13 @@ namespace SandboxEditor
aznumeric_cast<int>(position->m_normalizedPosition.GetX() * windowSize.m_width), aznumeric_cast<int>(position->m_normalizedPosition.GetX() * windowSize.m_width),
aznumeric_cast<int>(position->m_normalizedPosition.GetY() * windowSize.m_height)); aznumeric_cast<int>(position->m_normalizedPosition.GetY() * windowSize.m_height));
m_mouseInteraction.m_mousePick.m_screenCoordinates = screenPoint; ProjectedViewportRay ray{};
AZStd::optional<ProjectedViewportRay> ray;
ViewportInteractionRequestBus::EventResult( ViewportInteractionRequestBus::EventResult(
ray, GetViewportId(), &ViewportInteractionRequestBus::Events::ViewportScreenToWorldRay, screenPoint); ray, GetViewportId(), &ViewportInteractionRequestBus::Events::ViewportScreenToWorldRay, screenPoint);
if (ray.has_value()) m_mouseInteraction.m_mousePick.m_rayOrigin = ray.origin;
{ m_mouseInteraction.m_mousePick.m_rayDirection = ray.direction;
m_mouseInteraction.m_mousePick.m_rayOrigin = ray.value().origin; m_mouseInteraction.m_mousePick.m_screenCoordinates = screenPoint;
m_mouseInteraction.m_mousePick.m_rayDirection = ray.value().direction;
}
} }
eventType = MouseEvent::Move; eventType = MouseEvent::Move;
@ -160,8 +165,8 @@ namespace SandboxEditor
else if (state == InputChannel::State::Ended) else if (state == InputChannel::State::Ended)
{ {
// If we've actually logged a mouse down event, forward a mouse up event. // If we've actually logged a mouse down event, forward a mouse up event.
// This prevents corner cases like the context menu thinking it should be opened even though no one clicked in this viewport, // This prevents corner cases like the context menu thinking it should be opened even though no one clicked in this
// due to RenderViewportWidget ensuring all controllers get InputChannel::State::Ended events. // viewport, due to RenderViewportWidget ensuring all controllers get InputChannel::State::Ended events.
if (m_mouseInteraction.m_mouseButtons.m_mouseButtons & mouseButtonValue) if (m_mouseInteraction.m_mouseButtons.m_mouseButtons & mouseButtonValue)
{ {
// Erase the button from our state if we're done processing events. // Erase the button from our state if we're done processing events.
@ -259,4 +264,4 @@ namespace SandboxEditor
const double doubleClickThresholdMilliseconds = qApp->doubleClickInterval(); const double doubleClickThresholdMilliseconds = qApp->doubleClickInterval();
return (m_curTime.GetMilliseconds() - clickIt->second.GetMilliseconds()) < doubleClickThresholdMilliseconds; return (m_curTime.GetMilliseconds() - clickIt->second.GetMilliseconds()) < doubleClickThresholdMilliseconds;
} }
} //namespace SandboxEditor } // namespace SandboxEditor

@ -94,27 +94,27 @@ namespace AzFramework
float y; float y;
float z; float z;
// 2.4 Factor as RzRyRx // 2.5 Factor as RzRxRy
if (orientation.GetElement(2, 0) < 1.0f) if (orientation.GetElement(2, 1) < 1.0f)
{ {
if (orientation.GetElement(2, 0) > -1.0f) if (orientation.GetElement(2, 1) > -1.0f)
{ {
x = AZStd::atan2(orientation.GetElement(2, 1), orientation.GetElement(2, 2)); x = AZStd::asin(orientation.GetElement(2, 1));
y = AZStd::asin(-orientation.GetElement(2, 0)); y = AZStd::atan2(-orientation.GetElement(2, 0), orientation.GetElement(2, 2));
z = AZStd::atan2(orientation.GetElement(1, 0), orientation.GetElement(0, 0)); z = AZStd::atan2(-orientation.GetElement(0, 1), orientation.GetElement(1, 1));
} }
else else
{ {
x = 0.0f; x = -AZ::Constants::Pi * 0.5f;
y = AZ::Constants::Pi * 0.5f; y = 0.0f;
z = -AZStd::atan2(-orientation.GetElement(2, 1), orientation.GetElement(1, 1)); z = -AZStd::atan2(orientation.GetElement(0, 2), orientation.GetElement(0, 0));
} }
} }
else else
{ {
x = 0.0f; x = AZ::Constants::Pi * 0.5f;
y = -AZ::Constants::Pi * 0.5f; y = 0.0f;
z = AZStd::atan2(-orientation.GetElement(1, 2), orientation.GetElement(1, 1)); z = AZStd::atan2(orientation.GetElement(0, 2), orientation.GetElement(0, 0));
} }
return { x, y, z }; return { x, y, z };
@ -122,14 +122,36 @@ namespace AzFramework
void UpdateCameraFromTransform(Camera& camera, const AZ::Transform& transform) void UpdateCameraFromTransform(Camera& camera, const AZ::Transform& transform)
{ {
const auto eulerAngles = AzFramework::EulerAngles(AZ::Matrix3x3::CreateFromTransform(transform)); UpdateCameraFromTranslationAndRotation(
camera, transform.GetTranslation(), AzFramework::EulerAngles(AZ::Matrix3x3::CreateFromTransform(transform)));
}
void UpdateCameraFromTranslationAndRotation(Camera& camera, const AZ::Vector3& translation, const AZ::Vector3& eulerAngles)
{
camera.m_pitch = eulerAngles.GetX(); camera.m_pitch = eulerAngles.GetX();
camera.m_yaw = eulerAngles.GetZ(); camera.m_yaw = eulerAngles.GetZ();
camera.m_pivot = transform.GetTranslation(); camera.m_pivot = translation;
camera.m_offset = AZ::Vector3::CreateZero(); camera.m_offset = AZ::Vector3::CreateZero();
} }
float SmoothValueTime(const float smoothness, float deltaTime)
{
// note: the math for the lerp smoothing implementation for camera rotation and translation was inspired by this excellent
// article by Scott Lembcke: https://www.gamasutra.com/blogs/ScottLembcke/20180404/316046/Improved_Lerp_Smoothing.php
const float rate = AZStd::exp2(smoothness);
return AZStd::exp2(-rate * deltaTime);
}
float SmoothValue(const float target, const float current, const float time)
{
return AZ::Lerp(target, current, time);
}
float SmoothValue(const float target, const float current, const float smoothness, const float deltaTime)
{
return SmoothValue(target, current, SmoothValueTime(smoothness, deltaTime));
}
bool CameraSystem::HandleEvents(const InputEvent& event) bool CameraSystem::HandleEvents(const InputEvent& event)
{ {
if (const auto& cursor = AZStd::get_if<CursorEvent>(&event)) if (const auto& cursor = AZStd::get_if<CursorEvent>(&event))
@ -291,6 +313,11 @@ namespace AzFramework
{ {
return false; return false;
}; };
m_constrainPitch = []() constexpr
{
return true;
};
} }
bool RotateCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, [[maybe_unused]] const float scrollDelta) bool RotateCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, [[maybe_unused]] const float scrollDelta)
@ -312,7 +339,10 @@ namespace AzFramework
nextCamera.m_yaw -= float(cursorDelta.m_x) * rotateSpeed * Invert(m_invertYawFn()); nextCamera.m_yaw -= float(cursorDelta.m_x) * rotateSpeed * Invert(m_invertYawFn());
nextCamera.m_yaw = WrapYawRotation(nextCamera.m_yaw); nextCamera.m_yaw = WrapYawRotation(nextCamera.m_yaw);
nextCamera.m_pitch = ClampPitchRotation(nextCamera.m_pitch); if (m_constrainPitch())
{
nextCamera.m_pitch = ClampPitchRotation(nextCamera.m_pitch);
}
return nextCamera; return nextCamera;
} }
@ -726,14 +756,14 @@ namespace AzFramework
Camera SmoothCamera(const Camera& currentCamera, const Camera& targetCamera, const CameraProps& cameraProps, const float deltaTime) Camera SmoothCamera(const Camera& currentCamera, const Camera& targetCamera, const CameraProps& cameraProps, const float deltaTime)
{ {
const auto clamp_rotation = [](const float angle) const auto clampRotation = [](const float angle)
{ {
return AZStd::fmod(angle + AZ::Constants::TwoPi, AZ::Constants::TwoPi); return AZStd::fmod(angle + AZ::Constants::TwoPi, AZ::Constants::TwoPi);
}; };
// keep yaw in 0 - 360 range // keep yaw in 0 - 360 range
float targetYaw = clamp_rotation(targetCamera.m_yaw); float targetYaw = clampRotation(targetCamera.m_yaw);
const float currentYaw = clamp_rotation(currentCamera.m_yaw); const float currentYaw = clampRotation(currentCamera.m_yaw);
// return the sign of the float input (-1, 0, 1) // return the sign of the float input (-1, 0, 1)
const auto sign = [](const float value) const auto sign = [](const float value)
@ -742,21 +772,17 @@ namespace AzFramework
}; };
// ensure smooth transition when moving across 0 - 360 boundary // ensure smooth transition when moving across 0 - 360 boundary
const float yawDelta = targetYaw - currentYaw; if (const float yawDelta = targetYaw - currentYaw; AZStd::abs(yawDelta) >= AZ::Constants::Pi)
if (AZStd::abs(yawDelta) >= AZ::Constants::Pi)
{ {
targetYaw -= AZ::Constants::TwoPi * sign(yawDelta); targetYaw -= AZ::Constants::TwoPi * sign(yawDelta);
} }
Camera camera; Camera camera;
// note: the math for the lerp smoothing implementation for camera rotation and translation was inspired by this excellent
// article by Scott Lembcke: https://www.gamasutra.com/blogs/ScottLembcke/20180404/316046/Improved_Lerp_Smoothing.php
if (cameraProps.m_rotateSmoothingEnabledFn()) if (cameraProps.m_rotateSmoothingEnabledFn())
{ {
const float lookRate = AZStd::exp2(cameraProps.m_rotateSmoothnessFn()); const float lookTime = SmoothValueTime(cameraProps.m_rotateSmoothnessFn(), deltaTime);
const float lookTime = AZStd::exp2(-lookRate * deltaTime); camera.m_pitch = SmoothValue(targetCamera.m_pitch, currentCamera.m_pitch, lookTime);
camera.m_pitch = AZ::Lerp(targetCamera.m_pitch, currentCamera.m_pitch, lookTime); camera.m_yaw = SmoothValue(targetYaw, currentYaw, lookTime);
camera.m_yaw = AZ::Lerp(targetYaw, currentYaw, lookTime);
} }
else else
{ {
@ -766,8 +792,7 @@ namespace AzFramework
if (cameraProps.m_translateSmoothingEnabledFn()) if (cameraProps.m_translateSmoothingEnabledFn())
{ {
const float moveRate = AZStd::exp2(cameraProps.m_translateSmoothnessFn()); const float moveTime = SmoothValueTime(cameraProps.m_rotateSmoothnessFn(), deltaTime);
const float moveTime = AZStd::exp2(-moveRate * deltaTime);
camera.m_pivot = targetCamera.m_pivot.Lerp(currentCamera.m_pivot, moveTime); camera.m_pivot = targetCamera.m_pivot.Lerp(currentCamera.m_pivot, moveTime);
camera.m_offset = targetCamera.m_offset.Lerp(currentCamera.m_offset, moveTime); camera.m_offset = targetCamera.m_offset.Lerp(currentCamera.m_offset, moveTime);
} }

@ -85,6 +85,19 @@ namespace AzFramework
//! Extracts Euler angles (orientation) and translation from the transform and writes the values to the camera. //! Extracts Euler angles (orientation) and translation from the transform and writes the values to the camera.
void UpdateCameraFromTransform(Camera& camera, const AZ::Transform& transform); void UpdateCameraFromTransform(Camera& camera, const AZ::Transform& transform);
//! Writes the translation value and Euler angles to the camera.
void UpdateCameraFromTranslationAndRotation(Camera& camera, const AZ::Vector3& translation, const AZ::Vector3& eulerAngles);
//! Returns the time ('t') input value to use with SmoothValue.
//! Useful if it is to be reused for multiple calls to SmoothValue.
float SmoothValueTime(float smoothness, float deltaTime);
// Smoothly interpolate a value from current to target according to a smoothing parameter.
float SmoothValue(float target, float current, float smoothness, float deltaTime);
// Overload of SmoothValue that takes time ('t') value directly.
float SmoothValue(float target, float current, float time);
//! Generic motion type. //! Generic motion type.
template<typename MotionTag> template<typename MotionTag>
struct MotionEvent struct MotionEvent
@ -334,6 +347,7 @@ namespace AzFramework
AZStd::function<float()> m_rotateSpeedFn; AZStd::function<float()> m_rotateSpeedFn;
AZStd::function<bool()> m_invertPitchFn; AZStd::function<bool()> m_invertPitchFn;
AZStd::function<bool()> m_invertYawFn; AZStd::function<bool()> m_invertYawFn;
AZStd::function<bool()> m_constrainPitch;
private: private:
InputChannelId m_rotateChannelId; //!< Input channel to begin the rotate camera input. InputChannelId m_rotateChannelId; //!< Input channel to begin the rotate camera input.

@ -8,18 +8,18 @@
#include "CameraState.h" #include "CameraState.h"
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Math/Matrix3x4.h> #include <AzCore/Math/Matrix3x4.h>
#include <AzCore/Math/Transform.h> #include <AzCore/Math/Transform.h>
#include <AzCore/Serialization/SerializeContext.h>
namespace AzFramework namespace AzFramework
{ {
void SetCameraClippingVolume( void SetCameraClippingVolume(
AzFramework::CameraState& cameraState, const float nearPlane, const float farPlane, const float fovRad) AzFramework::CameraState& cameraState, const float nearPlane, const float farPlane, const float verticalFovRad)
{ {
cameraState.m_nearClip = nearPlane; cameraState.m_nearClip = nearPlane;
cameraState.m_farClip = farPlane; cameraState.m_farClip = farPlane;
cameraState.m_fovOrZoom = fovRad; cameraState.m_fovOrZoom = verticalFovRad;
} }
void SetCameraTransform(CameraState& cameraState, const AZ::Transform& transform) void SetCameraTransform(CameraState& cameraState, const AZ::Transform& transform)
@ -35,20 +35,34 @@ namespace AzFramework
SetCameraClippingVolume(cameraState, 0.1f, 1000.0f, AZ::DegToRad(60.0f)); SetCameraClippingVolume(cameraState, 0.1f, 1000.0f, AZ::DegToRad(60.0f));
} }
AzFramework::CameraState CreateDefaultCamera( CameraState CreateCamera(
const AZ::Transform& transform, const AZ::Vector2& viewportSize) const AZ::Transform& transform,
const float nearPlane,
const float farPlane,
const float verticalFovRad,
const AZ::Vector2& viewportSize)
{ {
AzFramework::CameraState cameraState; AzFramework::CameraState cameraState;
SetDefaultCameraClippingVolume(cameraState);
SetCameraTransform(cameraState, transform); SetCameraTransform(cameraState, transform);
SetCameraClippingVolume(cameraState, nearPlane, farPlane, verticalFovRad);
cameraState.m_viewportSize = viewportSize;
return cameraState;
}
AzFramework::CameraState CreateDefaultCamera(const AZ::Transform& transform, const AZ::Vector2& viewportSize)
{
AzFramework::CameraState cameraState;
SetCameraTransform(cameraState, transform);
SetDefaultCameraClippingVolume(cameraState);
cameraState.m_viewportSize = viewportSize; cameraState.m_viewportSize = viewportSize;
return cameraState; return cameraState;
} }
AzFramework::CameraState CreateIdentityDefaultCamera( AzFramework::CameraState CreateIdentityDefaultCamera(const AZ::Vector3& position, const AZ::Vector2& viewportSize)
const AZ::Vector3& position, const AZ::Vector2& viewportSize)
{ {
return CreateDefaultCamera(AZ::Transform::CreateTranslation(position), viewportSize); return CreateDefaultCamera(AZ::Transform::CreateTranslation(position), viewportSize);
} }
@ -89,15 +103,15 @@ namespace AzFramework
void CameraState::Reflect(AZ::SerializeContext& serializeContext) void CameraState::Reflect(AZ::SerializeContext& serializeContext)
{ {
serializeContext.Class<CameraState>()-> serializeContext.Class<CameraState>()
Field("Position", &CameraState::m_position)-> ->Field("Position", &CameraState::m_position)
Field("Forward", &CameraState::m_forward)-> ->Field("Forward", &CameraState::m_forward)
Field("Side", &CameraState::m_side)-> ->Field("Side", &CameraState::m_side)
Field("Up", &CameraState::m_up)-> ->Field("Up", &CameraState::m_up)
Field("ViewportSize", &CameraState::m_viewportSize)-> ->Field("ViewportSize", &CameraState::m_viewportSize)
Field("NearClip", &CameraState::m_nearClip)-> ->Field("NearClip", &CameraState::m_nearClip)
Field("FarClip", &CameraState::m_farClip)-> ->Field("FarClip", &CameraState::m_farClip)
Field("FovZoom", &CameraState::m_fovOrZoom)-> ->Field("FovZoom", &CameraState::m_fovOrZoom)
Field("Ortho", &CameraState::m_orthographic); ->Field("Ortho", &CameraState::m_orthographic);
} }
} // namespace AzFramework } // namespace AzFramework

@ -40,10 +40,14 @@ namespace AzFramework
AZ::Vector2 m_viewportSize = AZ::Vector2::CreateZero(); //!< Dimensions of the viewport. AZ::Vector2 m_viewportSize = AZ::Vector2::CreateZero(); //!< Dimensions of the viewport.
float m_nearClip = 0.01f; //!< Near clip plane of the camera. float m_nearClip = 0.01f; //!< Near clip plane of the camera.
float m_farClip = 100.0f; //!< Far clip plane of the camera. float m_farClip = 100.0f; //!< Far clip plane of the camera.
float m_fovOrZoom = 0.0f; //!< Fov or zoom of camera depending on if it is using orthographic projection or not. float m_fovOrZoom = 0.0f; //!< Vertical fov or zoom of camera depending on if it is using orthographic projection or not.
bool m_orthographic = false; //!< Is the camera using orthographic projection or not. bool m_orthographic = false; //!< Is the camera using orthographic projection or not.
}; };
//! Create a camera at the given transform, specifying the near and far clip planes as well as the fov with a specific viewport size.
CameraState CreateCamera(
const AZ::Transform& transform, float nearPlane, float farPlane, float verticalFovRad, const AZ::Vector2& viewportSize);
//! Create a camera at the given transform with a specific viewport size. //! Create a camera at the given transform with a specific viewport size.
//! @note The near/far clip planes and fov are sensible default values - please //! @note The near/far clip planes and fov are sensible default values - please
//! use SetCameraClippingVolume to override them. //! use SetCameraClippingVolume to override them.
@ -60,7 +64,7 @@ namespace AzFramework
CameraState CreateCameraFromWorldFromViewMatrix(const AZ::Matrix4x4& worldFromView, const AZ::Vector2& viewportSize); CameraState CreateCameraFromWorldFromViewMatrix(const AZ::Matrix4x4& worldFromView, const AZ::Vector2& viewportSize);
//! Override the default near/far clipping planes and fov of the camera. //! Override the default near/far clipping planes and fov of the camera.
void SetCameraClippingVolume(CameraState& cameraState, float nearPlane, float farPlane, float fovRad); void SetCameraClippingVolume(CameraState& cameraState, float nearPlane, float farPlane, float verticalFovRad);
//! Override the default near/far clipping planes and fov of the camera by inferring them the specified right handed transform into clip space. //! Override the default near/far clipping planes and fov of the camera by inferring them the specified right handed transform into clip space.
void SetCameraClippingVolumeFromPerspectiveFovMatrixRH(CameraState& cameraState, const AZ::Matrix4x4& clipFromView); void SetCameraClippingVolumeFromPerspectiveFovMatrixRH(CameraState& cameraState, const AZ::Matrix4x4& clipFromView);

@ -24,6 +24,10 @@ namespace AzFramework
serializeContext->Class<ScreenVector>()-> serializeContext->Class<ScreenVector>()->
Field("X", &ScreenVector::m_x)-> Field("X", &ScreenVector::m_x)->
Field("Y", &ScreenVector::m_y); Field("Y", &ScreenVector::m_y);
serializeContext->Class<ScreenSize>()->
Field("Width", &ScreenSize::m_width)->
Field("Height", &ScreenSize::m_height);
} }
} }
} // namespace AzFramework } // namespace AzFramework

@ -26,7 +26,7 @@ namespace AzFramework
AZ_TYPE_INFO(ScreenPoint, "{8472B6C2-527F-44FC-87F8-C226B1A57A97}"); AZ_TYPE_INFO(ScreenPoint, "{8472B6C2-527F-44FC-87F8-C226B1A57A97}");
ScreenPoint() = default; ScreenPoint() = default;
ScreenPoint(int x, int y) constexpr ScreenPoint(int x, int y)
: m_x(x) : m_x(x)
, m_y(y) , m_y(y)
{ {
@ -45,7 +45,7 @@ namespace AzFramework
AZ_TYPE_INFO(ScreenVector, "{1EAA2C62-8FDB-4A28-9FE3-1FA4F1418894}"); AZ_TYPE_INFO(ScreenVector, "{1EAA2C62-8FDB-4A28-9FE3-1FA4F1418894}");
ScreenVector() = default; ScreenVector() = default;
ScreenVector(int x, int y) constexpr ScreenVector(int x, int y)
: m_x(x) : m_x(x)
, m_y(y) , m_y(y)
{ {
@ -55,6 +55,22 @@ namespace AzFramework
int m_y; //!< Y screen delta. int m_y; //!< Y screen delta.
}; };
//! A wrapper around a screen width and height.
struct ScreenSize
{
AZ_TYPE_INFO(ScreenSize, "{26D28916-6E8E-44B8-83F9-C44BCDA370E2}");
ScreenSize() = default;
constexpr ScreenSize(int width, int height)
: m_width(width)
, m_height(height)
{
}
int m_width; //!< Screen size width.
int m_height; //!< Screen size height.
};
void ScreenGeometryReflect(AZ::ReflectContext* context); void ScreenGeometryReflect(AZ::ReflectContext* context);
inline const ScreenVector operator-(const ScreenPoint& lhs, const ScreenPoint& rhs) inline const ScreenVector operator-(const ScreenPoint& lhs, const ScreenPoint& rhs)
@ -138,6 +154,16 @@ namespace AzFramework
return !operator==(lhs, rhs); return !operator==(lhs, rhs);
} }
inline const bool operator==(const ScreenSize& lhs, const ScreenSize& rhs)
{
return lhs.m_width == rhs.m_width && lhs.m_height == rhs.m_height;
}
inline const bool operator!=(const ScreenSize& lhs, const ScreenSize& rhs)
{
return !operator==(lhs, rhs);
}
inline ScreenVector& operator*=(ScreenVector& lhs, const float rhs) inline ScreenVector& operator*=(ScreenVector& lhs, const float rhs)
{ {
lhs.m_x = aznumeric_cast<int>(AZStd::lround(aznumeric_cast<float>(lhs.m_x) * rhs)); lhs.m_x = aznumeric_cast<int>(AZStd::lround(aznumeric_cast<float>(lhs.m_x) * rhs));
@ -152,6 +178,20 @@ namespace AzFramework
return result; return result;
} }
inline ScreenSize& operator*=(ScreenSize& lhs, const float rhs)
{
lhs.m_width = aznumeric_cast<int>(AZStd::lround(aznumeric_cast<float>(lhs.m_width) * rhs));
lhs.m_height = aznumeric_cast<int>(AZStd::lround(aznumeric_cast<float>(lhs.m_height) * rhs));
return lhs;
}
inline const ScreenSize operator*(const ScreenSize& lhs, const float rhs)
{
ScreenSize result{ lhs };
result *= rhs;
return result;
}
inline float ScreenVectorLength(const ScreenVector& screenVector) inline float ScreenVectorLength(const ScreenVector& screenVector)
{ {
return aznumeric_cast<float>(AZStd::sqrt(screenVector.m_x * screenVector.m_x + screenVector.m_y * screenVector.m_y)); return aznumeric_cast<float>(AZStd::sqrt(screenVector.m_x * screenVector.m_x + screenVector.m_y * screenVector.m_y));
@ -168,4 +208,28 @@ namespace AzFramework
{ {
return AZ::Vector2(aznumeric_cast<float>(screenVector.m_x), aznumeric_cast<float>(screenVector.m_y)); return AZ::Vector2(aznumeric_cast<float>(screenVector.m_x), aznumeric_cast<float>(screenVector.m_y));
} }
//! Return an AZ::Vector2 from a ScreenSize.
inline AZ::Vector2 Vector2FromScreenSize(const ScreenSize& screenSize)
{
return AZ::Vector2(aznumeric_cast<float>(screenSize.m_width), aznumeric_cast<float>(screenSize.m_height));
}
//! Return a ScreenPoint from an AZ::Vector2.
inline ScreenPoint ScreenPointFromVector2(const AZ::Vector2& vector2)
{
return ScreenPoint(aznumeric_cast<int>(AZStd::lround(vector2.GetX())), aznumeric_cast<int>(AZStd::lround(vector2.GetY())));
}
//! Return a ScreenVector from an AZ::Vector2.
inline ScreenVector ScreenVectorFromVector2(const AZ::Vector2& vector2)
{
return ScreenVector(aznumeric_cast<int>(AZStd::lround(vector2.GetX())), aznumeric_cast<int>(AZStd::lround(vector2.GetY())));
}
//! Return a ScreenSize from an AZ::Vector2.
inline ScreenSize ScreenSizeFromVector2(const AZ::Vector2& vector2)
{
return ScreenSize(aznumeric_cast<int>(AZStd::lround(vector2.GetX())), aznumeric_cast<int>(AZStd::lround(vector2.GetY())));
}
} // namespace AzFramework } // namespace AzFramework

@ -10,6 +10,7 @@
#include <AzCore/Math/Frustum.h> #include <AzCore/Math/Frustum.h>
#include <AzCore/Math/Matrix4x4.h> #include <AzCore/Math/Matrix4x4.h>
#include <AzCore/Math/MatrixUtils.h>
#include <AzCore/Math/Vector4.h> #include <AzCore/Math/Vector4.h>
#include <AzCore/Math/VectorConversions.h> #include <AzCore/Math/VectorConversions.h>
#include <AzFramework/Entity/EntityDebugDisplayBus.h> #include <AzFramework/Entity/EntityDebugDisplayBus.h>
@ -112,9 +113,8 @@ namespace AzFramework
const AZ::Matrix4x4& cameraProjection, const AZ::Matrix4x4& cameraProjection,
const AZ::Vector2& viewportSize) const AZ::Vector2& viewportSize)
{ {
const auto ndcNormalizedPosition = WorldToScreenNdc(worldPosition, cameraView, cameraProjection);
// scale ndc position by screen dimensions to return screen position // scale ndc position by screen dimensions to return screen position
return ScreenPointFromNdc(AZ::Vector3ToVector2(ndcNormalizedPosition), viewportSize); return ScreenPointFromNdc(AZ::Vector3ToVector2(WorldToScreenNdc(worldPosition, cameraView, cameraProjection)), viewportSize);
} }
ScreenPoint WorldToScreen(const AZ::Vector3& worldPosition, const CameraState& cameraState) ScreenPoint WorldToScreen(const AZ::Vector3& worldPosition, const CameraState& cameraState)
@ -144,9 +144,7 @@ namespace AzFramework
const AZ::Matrix4x4& inverseCameraProjection, const AZ::Matrix4x4& inverseCameraProjection,
const AZ::Vector2& viewportSize) const AZ::Vector2& viewportSize)
{ {
const auto normalizedScreenPosition = NdcFromScreenPoint(screenPosition, viewportSize); return ScreenNdcToWorld(NdcFromScreenPoint(screenPosition, viewportSize), inverseCameraView, inverseCameraProjection);
return ScreenNdcToWorld(normalizedScreenPosition, inverseCameraView, inverseCameraProjection);
} }
AZ::Vector3 ScreenToWorld(const ScreenPoint& screenPosition, const CameraState& cameraState) AZ::Vector3 ScreenToWorld(const ScreenPoint& screenPosition, const CameraState& cameraState)

@ -104,9 +104,10 @@ namespace UnitTest
AZStd::shared_ptr<AzFramework::OrbitCameraInput> m_orbitCamera; AZStd::shared_ptr<AzFramework::OrbitCameraInput> m_orbitCamera;
AZ::Vector3 m_pivot = AZ::Vector3::CreateZero(); AZ::Vector3 m_pivot = AZ::Vector3::CreateZero();
//! This is approximately Pi/2 * 1000 - this can be used to rotate the camera 90 degrees (pitch or yaw based // this is approximately Pi/2 * 1000 - this can be used to rotate the camera 90 degrees (pitch or yaw based
//! on vertical or horizontal motion) as the rotate speed function is set to be 1/1000. // on vertical or horizontal motion) as the rotate speed function is set to be 1/1000.
inline static const int PixelMotionDelta = 1570; inline static const int PixelMotionDelta90Degrees = 1570;
inline static const int PixelMotionDelta135Degrees = 2356;
}; };
TEST_F(CameraInputFixture, BeginAndEndOrbitCameraInputConsumesCorrectEvents) TEST_F(CameraInputFixture, BeginAndEndOrbitCameraInputConsumesCorrectEvents)
@ -292,7 +293,7 @@ namespace UnitTest
HandleEventAndUpdate( HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began }); AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ PixelMotionDelta }); HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ PixelMotionDelta90Degrees });
const float expectedYaw = AzFramework::WrapYawRotation(-AZ::Constants::HalfPi); const float expectedYaw = AzFramework::WrapYawRotation(-AZ::Constants::HalfPi);
@ -310,7 +311,7 @@ namespace UnitTest
HandleEventAndUpdate( HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began }); AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta }); HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta90Degrees });
const float expectedPitch = AzFramework::ClampPitchRotation(-AZ::Constants::HalfPi); const float expectedPitch = AzFramework::ClampPitchRotation(-AZ::Constants::HalfPi);
@ -331,7 +332,7 @@ namespace UnitTest
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_orbitChannelId, AzFramework::InputChannel::State::Began }); HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_orbitChannelId, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate( HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began }); AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta }); HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta90Degrees });
const auto expectedCameraEndingPosition = AZ::Vector3(0.0f, -10.0f, 10.0f); const auto expectedCameraEndingPosition = AZ::Vector3(0.0f, -10.0f, 10.0f);
const float expectedPitch = AzFramework::ClampPitchRotation(-AZ::Constants::HalfPi); const float expectedPitch = AzFramework::ClampPitchRotation(-AZ::Constants::HalfPi);
@ -354,7 +355,7 @@ namespace UnitTest
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_orbitChannelId, AzFramework::InputChannel::State::Began }); HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_orbitChannelId, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate( HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began }); AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ -PixelMotionDelta }); HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ -PixelMotionDelta90Degrees });
const auto expectedCameraEndingPosition = AZ::Vector3(20.0f, -5.0f, 0.0f); const auto expectedCameraEndingPosition = AZ::Vector3(20.0f, -5.0f, 0.0f);
const float expectedYaw = AzFramework::WrapYawRotation(AZ::Constants::HalfPi); const float expectedYaw = AzFramework::WrapYawRotation(AZ::Constants::HalfPi);
@ -366,4 +367,42 @@ namespace UnitTest
EXPECT_THAT(m_camera.m_offset, IsClose(AZ::Vector3(5.0f, -10.0f, 0.0f))); EXPECT_THAT(m_camera.m_offset, IsClose(AZ::Vector3(5.0f, -10.0f, 0.0f)));
EXPECT_THAT(m_camera.Translation(), IsCloseTolerance(expectedCameraEndingPosition, 0.01f)); EXPECT_THAT(m_camera.Translation(), IsCloseTolerance(expectedCameraEndingPosition, 0.01f));
} }
TEST_F(CameraInputFixture, CameraPitchCanNotBeMovedPastNinetyDegreesWhenConstrained)
{
const auto cameraStartingPosition = AZ::Vector3(15.0f, -20.0f, 0.0f);
m_targetCamera.m_pivot = cameraStartingPosition;
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
// pitch by 135.0 degrees
HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ -PixelMotionDelta135Degrees });
// clamped to 90.0 degrees
const float expectedPitch = AZ::DegToRad(90.0f);
using ::testing::FloatNear;
EXPECT_THAT(m_camera.m_pitch, FloatNear(expectedPitch, 0.001f));
}
TEST_F(CameraInputFixture, CameraPitchCanBeMovedPastNinetyDegreesWhenUnconstrained)
{
m_firstPersonRotateCamera->m_constrainPitch = []
{
return false;
};
const auto cameraStartingPosition = AZ::Vector3(15.0f, -20.0f, 0.0f);
m_targetCamera.m_pivot = cameraStartingPosition;
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
// pitch by 135.0 degrees
HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ -PixelMotionDelta135Degrees });
const float expectedPitch = AZ::DegToRad(135.0f);
using ::testing::FloatNear;
EXPECT_THAT(m_camera.m_pitch, FloatNear(expectedPitch, 0.001f));
}
} // namespace UnitTest } // namespace UnitTest

@ -11,6 +11,7 @@
#include <AzFramework/Viewport/CameraState.h> #include <AzFramework/Viewport/CameraState.h>
#include <AZTestShared/Math/MathTestHelpers.h> #include <AZTestShared/Math/MathTestHelpers.h>
#include <AzCore/Math/SimdMath.h> #include <AzCore/Math/SimdMath.h>
#include <AzCore/Math/MatrixUtils.h>
#include <AzCore/Math/Matrix4x4.h> #include <AzCore/Math/Matrix4x4.h>
namespace UnitTest namespace UnitTest
@ -51,22 +52,6 @@ namespace UnitTest
{ {
}; };
// Taken from Atom::MatrixUtils for testing purposes, this can be removed if MakePerspectiveFovMatrixRH makes it into AZ
static AZ::Matrix4x4 MakePerspectiveMatrixRH(float fovY, float aspectRatio, float nearClip, float farClip)
{
float sinFov, cosFov;
AZ::SinCos(0.5f * fovY, sinFov, cosFov);
float yScale = cosFov / sinFov; //cot(fovY/2)
float xScale = yScale / aspectRatio;
AZ::Matrix4x4 out;
out.SetRow(0, xScale, 0.f, 0.f, 0.f );
out.SetRow(1, 0.f, yScale, 0.f, 0.f );
out.SetRow(2, 0.f, 0.f, farClip / (nearClip - farClip), nearClip*farClip / (nearClip - farClip) );
out.SetRow(3, 0.f, 0.f, -1.f, 0.f );
return out;
}
TEST_P(Translation, Permutation) TEST_P(Translation, Permutation)
{ {
// Given a position // Given a position
@ -176,7 +161,8 @@ namespace UnitTest
{ {
auto [fovY, aspectRatio, nearClip, farClip] = GetParam(); auto [fovY, aspectRatio, nearClip, farClip] = GetParam();
AZ::Matrix4x4 clipFromView = MakePerspectiveMatrixRH(fovY, aspectRatio, nearClip, farClip); AZ::Matrix4x4 clipFromView;
MakePerspectiveFovMatrixRH(clipFromView, fovY, aspectRatio, nearClip, farClip);
AzFramework::SetCameraClippingVolumeFromPerspectiveFovMatrixRH(m_cameraState, clipFromView); AzFramework::SetCameraClippingVolumeFromPerspectiveFovMatrixRH(m_cameraState, clipFromView);

@ -8,12 +8,13 @@
#include <AzCore/UnitTest/TestTypes.h> #include <AzCore/UnitTest/TestTypes.h>
#include <AzFramework/Viewport/CursorState.h> #include <AzFramework/Viewport/CursorState.h>
#include <Tests/Utils/Printers.h>
namespace UnitTest namespace UnitTest
{ {
using AzFramework::CursorState; using AzFramework::CursorState;
using AzFramework::ScreenVector;
using AzFramework::ScreenPoint; using AzFramework::ScreenPoint;
using AzFramework::ScreenVector;
class CursorStateFixture : public ::testing::Test class CursorStateFixture : public ::testing::Test
{ {

@ -0,0 +1,32 @@
/*
* 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 "Printers.h"
#include <AzFramework/Viewport/ScreenGeometry.h>
#include <ostream>
#include <string>
namespace AzFramework
{
void PrintTo(const ScreenPoint& screenPoint, std::ostream* os)
{
*os << "(x: " << screenPoint.m_x << ", y: " << screenPoint.m_y << ")";
}
void PrintTo(const ScreenVector& screenVector, std::ostream* os)
{
*os << "(x: " << screenVector.m_x << ", y: " << screenVector.m_y << ")";
}
void PrintTo(const ScreenSize& screenSize, std::ostream* os)
{
*os << "(width: " << screenSize.m_width << ", height: " << screenSize.m_height << ")";
}
} // namespace AzFramework

@ -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
*
*/
#include <iosfwd>
namespace AzFramework
{
struct ScreenPoint;
struct ScreenVector;
struct ScreenSize;
void PrintTo(const ScreenPoint& screenPoint, std::ostream* os);
void PrintTo(const ScreenVector& screenVector, std::ostream* os);
void PrintTo(const ScreenSize& screenSize, std::ostream* os);
} // namespace AzFramework

@ -11,5 +11,7 @@ set(FILES
Mocks/MockWindowRequests.h Mocks/MockWindowRequests.h
Utils/Utils.h Utils/Utils.h
Utils/Utils.cpp Utils/Utils.cpp
Utils/Printers.h
Utils/Printers.cpp
FrameworkApplicationFixture.h FrameworkApplicationFixture.h
) )

@ -41,8 +41,8 @@ namespace AzManipulatorTestFramework
// ViewportInteractionRequestBus overrides ... // ViewportInteractionRequestBus overrides ...
AzFramework::CameraState GetCameraState() override; AzFramework::CameraState GetCameraState() override;
AzFramework::ScreenPoint ViewportWorldToScreen(const AZ::Vector3& worldPosition) override; AzFramework::ScreenPoint ViewportWorldToScreen(const AZ::Vector3& worldPosition) override;
AZStd::optional<AZ::Vector3> ViewportScreenToWorld(const AzFramework::ScreenPoint& screenPosition, float depth) override; AZ::Vector3 ViewportScreenToWorld(const AzFramework::ScreenPoint& screenPosition) override;
AZStd::optional<AzToolsFramework::ViewportInteraction::ProjectedViewportRay> ViewportScreenToWorldRay( AzToolsFramework::ViewportInteraction::ProjectedViewportRay ViewportScreenToWorldRay(
const AzFramework::ScreenPoint& screenPosition) override; const AzFramework::ScreenPoint& screenPosition) override;
float DeviceScalingFactor() override; float DeviceScalingFactor() override;

@ -95,7 +95,7 @@ namespace AzManipulatorTestFramework
AzToolsFramework::ViewportInteraction::MousePick mousePick; AzToolsFramework::ViewportInteraction::MousePick mousePick;
mousePick.m_screenCoordinates = screenPoint; mousePick.m_screenCoordinates = screenPoint;
mousePick.m_rayOrigin = cameraState.m_position; mousePick.m_rayOrigin = nearPlaneWorldPosition;
mousePick.m_rayDirection = (nearPlaneWorldPosition - cameraState.m_position).GetNormalized(); mousePick.m_rayDirection = (nearPlaneWorldPosition - cameraState.m_position).GetNormalized();
return mousePick; return mousePick;

@ -69,8 +69,6 @@ namespace AzManipulatorTestFramework
void ImmediateModeActionDispatcher::CameraStateImpl(const AzFramework::CameraState& cameraState) void ImmediateModeActionDispatcher::CameraStateImpl(const AzFramework::CameraState& cameraState)
{ {
m_viewportManipulatorInteraction.GetViewportInteraction().SetCameraState(cameraState); m_viewportManipulatorInteraction.GetViewportInteraction().SetCameraState(cameraState);
GetMouseInteractionEvent()->m_mouseInteraction.m_mousePick.m_rayOrigin = cameraState.m_position;
GetMouseInteractionEvent()->m_mouseInteraction.m_mousePick.m_rayDirection = cameraState.m_forward;
} }
void ImmediateModeActionDispatcher::MouseLButtonDownImpl() void ImmediateModeActionDispatcher::MouseLButtonDownImpl()

@ -20,7 +20,8 @@ namespace AzManipulatorTestFramework
{ {
public: public:
IndirectCallManipulatorManager(ViewportInteractionInterface& viewportInteraction); IndirectCallManipulatorManager(ViewportInteractionInterface& viewportInteraction);
// ManipulatorManagerInterface ...
// ManipulatorManagerInterface overrides ...
void ConsumeMouseInteractionEvent(const MouseInteractionEvent& event) override; void ConsumeMouseInteractionEvent(const MouseInteractionEvent& event) override;
AzToolsFramework::ManipulatorManagerId GetId() const override; AzToolsFramework::ManipulatorManagerId GetId() const override;
bool ManipulatorBeingInteracted() const override; bool ManipulatorBeingInteracted() const override;

@ -140,13 +140,12 @@ namespace AzManipulatorTestFramework
return m_viewportId; return m_viewportId;
} }
AZStd::optional<AZ::Vector3> ViewportInteraction::ViewportScreenToWorld( AZ::Vector3 ViewportInteraction::ViewportScreenToWorld([[maybe_unused]] const AzFramework::ScreenPoint& screenPosition)
[[maybe_unused]] const AzFramework::ScreenPoint& screenPosition, [[maybe_unused]] float depth)
{ {
return {}; return AZ::Vector3::CreateZero();
} }
AZStd::optional<AzToolsFramework::ViewportInteraction::ProjectedViewportRay> ViewportInteraction::ViewportScreenToWorldRay( AzToolsFramework::ViewportInteraction::ProjectedViewportRay ViewportInteraction::ViewportScreenToWorldRay(
[[maybe_unused]] const AzFramework::ScreenPoint& screenPosition) [[maybe_unused]] const AzFramework::ScreenPoint& screenPosition)
{ {
return {}; return {};

@ -140,8 +140,8 @@ namespace UnitTest
// given a left mouse down ray in world space // given a left mouse down ray in world space
// consume the mouse move event // consume the mouse move event
state.m_actionDispatcher->CameraState(m_cameraState) state.m_actionDispatcher->CameraState(m_cameraState)
->MouseLButtonDown()
->MousePosition(AzManipulatorTestFramework::GetCameraStateViewportCenter(m_cameraState)) ->MousePosition(AzManipulatorTestFramework::GetCameraStateViewportCenter(m_cameraState))
->MouseLButtonDown()
->ExpectTrue(state.m_linearManipulator->PerformingAction()) ->ExpectTrue(state.m_linearManipulator->PerformingAction())
->ExpectManipulatorBeingInteracted() ->ExpectManipulatorBeingInteracted()
->MouseLButtonUp() ->MouseLButtonUp()

@ -0,0 +1,25 @@
/*
* 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
namespace AzToolsFramework::EmbeddedPython
{
// When using embedded Python, some platforms need to explicitly load the python library.
// For any modules that depend on 3rdParty::Python package, the AZ::Module should inherit this class.
class PythonLoader
{
public:
PythonLoader();
~PythonLoader();
private:
void* m_embeddedLibPythonHandle{ nullptr };
};
} // namespace AzToolsFramework::EmbeddedPython

@ -1196,14 +1196,25 @@ namespace AzToolsFramework
AZ::EntityId ToolsApplication::GetCurrentLevelEntityId() AZ::EntityId ToolsApplication::GetCurrentLevelEntityId()
{ {
AzFramework::EntityContextId editorEntityContextId = AzFramework::EntityContextId::CreateNull(); if (IsPrefabSystemEnabled())
AzToolsFramework::EditorEntityContextRequestBus::BroadcastResult(editorEntityContextId, &AzToolsFramework::EditorEntityContextRequestBus::Events::GetEditorEntityContextId); {
AZ::SliceComponent* rootSliceComponent = nullptr; if (auto prefabPublicInterface = AZ::Interface<Prefab::PrefabPublicInterface>::Get())
AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(rootSliceComponent, editorEntityContextId, {
&AzFramework::SliceEntityOwnershipServiceRequestBus::Events::GetRootSlice); return prefabPublicInterface->GetLevelInstanceContainerEntityId();
if (rootSliceComponent && rootSliceComponent->GetMetadataEntity()) }
{ }
return rootSliceComponent->GetMetadataEntity()->GetId(); else
{
AzFramework::EntityContextId editorEntityContextId = AzFramework::EntityContextId::CreateNull();
AzToolsFramework::EditorEntityContextRequestBus::BroadcastResult(
editorEntityContextId, &AzToolsFramework::EditorEntityContextRequestBus::Events::GetEditorEntityContextId);
AZ::SliceComponent* rootSliceComponent = nullptr;
AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(
rootSliceComponent, editorEntityContextId, &AzFramework::SliceEntityOwnershipServiceRequestBus::Events::GetRootSlice);
if (rootSliceComponent && rootSliceComponent->GetMetadataEntity())
{
return rootSliceComponent->GetMetadataEntity()->GetId();
}
} }
return AZ::EntityId(); return AZ::EntityId();

@ -448,7 +448,11 @@ namespace AzToolsFramework
ViewportUi::ViewportUiRequestBus::Event( ViewportUi::ViewportUiRequestBus::Event(
ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder,
componentMode.m_componentMode->GetComponentModeName().c_str()); componentMode.m_componentMode->GetComponentModeName().c_str(),
[]
{
ComponentModeSystemRequestBus::Broadcast(&ComponentModeSystemRequests::EndComponentMode);
});
} }
RefreshActions(); RefreshActions();

@ -55,8 +55,11 @@ namespace AzToolsFramework
GetEntityComponentIdPair(), elementIdsToDisplay); GetEntityComponentIdPair(), elementIdsToDisplay);
// create the component mode border with the specific name for this component mode // create the component mode border with the specific name for this component mode
ViewportUi::ViewportUiRequestBus::Event( ViewportUi::ViewportUiRequestBus::Event(
ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, GetComponentModeName(),
GetComponentModeName()); []
{
ComponentModeSystemRequestBus::Broadcast(&ComponentModeSystemRequests::EndComponentMode);
});
// set the EntityComponentId for this ComponentMode to active in the ComponentModeViewportUi system // set the EntityComponentId for this ComponentMode to active in the ComponentModeViewportUi system
ComponentModeViewportUiRequestBus::Event( ComponentModeViewportUiRequestBus::Event(
GetComponentType(), &ComponentModeViewportUiRequestBus::Events::SetViewportUiActiveEntityComponentId, GetComponentType(), &ComponentModeViewportUiRequestBus::Events::SetViewportUiActiveEntityComponentId,

@ -158,7 +158,10 @@ namespace UnitTest
{ {
// Create & Start a new ToolsApplication if there's no existing one // Create & Start a new ToolsApplication if there's no existing one
m_app = CreateTestApplication(); m_app = CreateTestApplication();
m_app->Start(AzFramework::Application::Descriptor()); AZ::ComponentApplication::StartupParameters startupParameters;
startupParameters.m_loadAssetCatalog = false;
m_app->Start(AzFramework::Application::Descriptor(), startupParameters);
} }
// without this, the user settings component would attempt to save on finalize/shutdown. Since the file is // without this, the user settings component would attempt to save on finalize/shutdown. Since the file is

@ -162,12 +162,11 @@ namespace AzToolsFramework
//! Multiply by DeviceScalingFactor to get the position in viewport pixel space. //! Multiply by DeviceScalingFactor to get the position in viewport pixel space.
virtual AzFramework::ScreenPoint ViewportWorldToScreen(const AZ::Vector3& worldPosition) = 0; virtual AzFramework::ScreenPoint ViewportWorldToScreen(const AZ::Vector3& worldPosition) = 0;
//! Transforms a point from Qt widget screen space to world space based on the given clip space depth. //! Transforms a point from Qt widget screen space to world space based on the given clip space depth.
//! Depth specifies a relative camera depth to project in the range of [0.f, 1.f].
//! Returns the world space position if successful. //! Returns the world space position if successful.
virtual AZStd::optional<AZ::Vector3> ViewportScreenToWorld(const AzFramework::ScreenPoint& screenPosition, float depth) = 0; virtual AZ::Vector3 ViewportScreenToWorld(const AzFramework::ScreenPoint& screenPosition) = 0;
//! Casts a point in screen space to a ray in world space originating from the viewport camera frustum's near plane. //! Casts a point in screen space to a ray in world space originating from the viewport camera frustum's near plane.
//! Returns a ray containing the ray's origin and a direction normal, if successful. //! Returns a ray containing the ray's origin and a direction normal, if successful.
virtual AZStd::optional<ProjectedViewportRay> ViewportScreenToWorldRay(const AzFramework::ScreenPoint& screenPosition) = 0; virtual ProjectedViewportRay ViewportScreenToWorldRay(const AzFramework::ScreenPoint& screenPosition) = 0;
//! Gets the DPI scaling factor that translates Qt widget space into viewport pixel space. //! Gets the DPI scaling factor that translates Qt widget space into viewport pixel space.
virtual float DeviceScalingFactor() = 0; virtual float DeviceScalingFactor() = 0;
@ -229,9 +228,6 @@ namespace AzToolsFramework
class MainEditorViewportInteractionRequests class MainEditorViewportInteractionRequests
{ {
public: public:
//! Given a point in screen space, return the picked entity (if any).
//! Picked EntityId will be returned, InvalidEntityId will be returned on failure.
virtual AZ::EntityId PickEntity(const AzFramework::ScreenPoint& point) = 0;
//! Given a point in screen space, return the terrain position in world space. //! Given a point in screen space, return the terrain position in world space.
virtual AZ::Vector3 PickTerrain(const AzFramework::ScreenPoint& point) = 0; virtual AZ::Vector3 PickTerrain(const AzFramework::ScreenPoint& point) = 0;
//! Return the terrain height given a world position in 2d (xy plane). //! Return the terrain height given a world position in 2d (xy plane).
@ -266,7 +262,6 @@ namespace AzToolsFramework
{ {
public: public:
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
//! Returns the current state of the keyboard modifier keys. //! Returns the current state of the keyboard modifier keys.
virtual KeyboardModifiers QueryKeyboardModifiers() = 0; virtual KeyboardModifiers QueryKeyboardModifiers() = 0;
@ -290,7 +285,6 @@ namespace AzToolsFramework
{ {
public: public:
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
//! Returns the current time in seconds. //! Returns the current time in seconds.
//! This interface can be overridden for the purposes of testing to simplify viewport input requests. //! This interface can be overridden for the purposes of testing to simplify viewport input requests.

@ -148,7 +148,6 @@ namespace AzToolsFramework
return false; return false;
} }
EditorHelpers::EditorHelpers(const EditorVisibleEntityDataCache* entityDataCache) EditorHelpers::EditorHelpers(const EditorVisibleEntityDataCache* entityDataCache)
: m_entityDataCache(entityDataCache) : m_entityDataCache(entityDataCache)
{ {
@ -190,7 +189,10 @@ namespace AzToolsFramework
if (helpersVisible) if (helpersVisible)
{ {
// some components choose to hide their icons (e.g. meshes) // some components choose to hide their icons (e.g. meshes)
if (!m_entityDataCache->IsVisibleEntityIconHidden(entityCacheIndex)) // we also do not want to test against icons that may not be showing as they're inside a 'closed' entity container
// (these icons only become visible when it is opened for editing)
if (!m_entityDataCache->IsVisibleEntityIconHidden(entityCacheIndex) &&
m_entityDataCache->IsVisibleEntityIndividuallySelectableInViewport(entityCacheIndex))
{ {
const AZ::Vector3& entityPosition = m_entityDataCache->GetVisibleEntityPosition(entityCacheIndex); const AZ::Vector3& entityPosition = m_entityDataCache->GetVisibleEntityPosition(entityCacheIndex);
@ -235,7 +237,7 @@ namespace AzToolsFramework
viewportId, &ViewportInteraction::ViewportMouseCursorRequestBus::Events::SetOverrideCursor, viewportId, &ViewportInteraction::ViewportMouseCursorRequestBus::Events::SetOverrideCursor,
ViewportInteraction::CursorStyleOverride::Forbidden); ViewportInteraction::CursorStyleOverride::Forbidden);
} }
if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() && if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() &&
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down || mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down ||
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::DoubleClick) mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::DoubleClick)

@ -27,6 +27,7 @@
#include <AzToolsFramework/Manipulators/ScaleManipulators.h> #include <AzToolsFramework/Manipulators/ScaleManipulators.h>
#include <AzToolsFramework/Manipulators/TranslationManipulators.h> #include <AzToolsFramework/Manipulators/TranslationManipulators.h>
#include <AzToolsFramework/Maths/TransformUtils.h> #include <AzToolsFramework/Maths/TransformUtils.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h> #include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h> #include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <AzToolsFramework/ToolsComponents/EditorLockComponentBus.h> #include <AzToolsFramework/ToolsComponents/EditorLockComponentBus.h>
@ -409,7 +410,7 @@ namespace AzToolsFramework
const AzFramework::CameraState cameraState = GetCameraState(viewportId); const AzFramework::CameraState cameraState = GetCameraState(viewportId);
for (size_t entityCacheIndex = 0; entityCacheIndex < entityDataCache.VisibleEntityDataCount(); ++entityCacheIndex) for (size_t entityCacheIndex = 0; entityCacheIndex < entityDataCache.VisibleEntityDataCount(); ++entityCacheIndex)
{ {
if (!entityDataCache.IsVisibleEntitySelectableInViewport(entityCacheIndex)) if (!entityDataCache.IsVisibleEntityIndividuallySelectableInViewport(entityCacheIndex))
{ {
continue; continue;
} }
@ -983,7 +984,7 @@ namespace AzToolsFramework
{ {
if (auto entityIndex = entityDataCache.GetVisibleEntityIndexFromId(entityId)) if (auto entityIndex = entityDataCache.GetVisibleEntityIndexFromId(entityId))
{ {
if (entityDataCache.IsVisibleEntitySelectableInViewport(*entityIndex)) if (entityDataCache.IsVisibleEntityIndividuallySelectableInViewport(*entityIndex))
{ {
return *entityIndex; return *entityIndex;
} }
@ -1014,6 +1015,15 @@ namespace AzToolsFramework
ToolsApplicationNotificationBus::Broadcast(&ToolsApplicationNotificationBus::Events::InvalidatePropertyDisplay, Refresh_Values); ToolsApplicationNotificationBus::Broadcast(&ToolsApplicationNotificationBus::Events::InvalidatePropertyDisplay, Refresh_Values);
} }
// leaves focus mode by focusing on the parent of the current perfab in the entity outliner
static void LeaveFocusMode()
{
if (auto prefabFocusPublicInterface = AZ::Interface<Prefab::PrefabFocusPublicInterface>::Get())
{
prefabFocusPublicInterface->FocusOnParentOfFocusedPrefab(GetEntityContextId());
}
}
EditorTransformComponentSelection::EditorTransformComponentSelection(const EditorVisibleEntityDataCache* entityDataCache) EditorTransformComponentSelection::EditorTransformComponentSelection(const EditorVisibleEntityDataCache* entityDataCache)
: m_entityDataCache(entityDataCache) : m_entityDataCache(entityDataCache)
{ {
@ -3674,7 +3684,8 @@ namespace AzToolsFramework
case ViewportEditorMode::Focus: case ViewportEditorMode::Focus:
{ {
ViewportUi::ViewportUiRequestBus::Event( ViewportUi::ViewportUiRequestBus::Event(
ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, "Focus Mode"); ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, "Focus Mode",
LeaveFocusMode);
} }
break; break;
case ViewportEditorMode::Default: case ViewportEditorMode::Default:
@ -3703,12 +3714,14 @@ namespace AzToolsFramework
if (editorModeState.IsModeActive(ViewportEditorMode::Focus)) if (editorModeState.IsModeActive(ViewportEditorMode::Focus))
{ {
ViewportUi::ViewportUiRequestBus::Event( ViewportUi::ViewportUiRequestBus::Event(
ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, "Focus Mode"); ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, "Focus Mode",
LeaveFocusMode);
} }
} }
break; break;
case ViewportEditorMode::Focus: case ViewportEditorMode::Focus:
{ {
ViewportUi::ViewportUiRequestBus::Event( ViewportUi::ViewportUiRequestBus::Event(
ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::RemoveViewportBorder); ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::RemoveViewportBorder);
} }

@ -293,12 +293,10 @@ namespace AzToolsFramework
return m_impl->m_visibleEntityDatas[index].m_iconHidden; return m_impl->m_visibleEntityDatas[index].m_iconHidden;
} }
bool EditorVisibleEntityDataCache::IsVisibleEntitySelectableInViewport(size_t index) const bool EditorVisibleEntityDataCache::IsVisibleEntityIndividuallySelectableInViewport(const size_t index) const
{ {
return m_impl->m_visibleEntityDatas[index].m_visible return m_impl->m_visibleEntityDatas[index].m_visible && !m_impl->m_visibleEntityDatas[index].m_locked &&
&& !m_impl->m_visibleEntityDatas[index].m_locked m_impl->m_visibleEntityDatas[index].m_inFocus && !m_impl->m_visibleEntityDatas[index].m_descendantOfClosedContainer;
&& m_impl->m_visibleEntityDatas[index].m_inFocus
&& !m_impl->m_visibleEntityDatas[index].m_descendantOfClosedContainer;
} }
AZStd::optional<size_t> EditorVisibleEntityDataCache::GetVisibleEntityIndexFromId(const AZ::EntityId entityId) const AZStd::optional<size_t> EditorVisibleEntityDataCache::GetVisibleEntityIndexFromId(const AZ::EntityId entityId) const

@ -55,7 +55,10 @@ namespace AzToolsFramework
bool IsVisibleEntityVisible(size_t index) const; bool IsVisibleEntityVisible(size_t index) const;
bool IsVisibleEntitySelected(size_t index) const; bool IsVisibleEntitySelected(size_t index) const;
bool IsVisibleEntityIconHidden(size_t index) const; bool IsVisibleEntityIconHidden(size_t index) const;
bool IsVisibleEntitySelectableInViewport(size_t index) const; //! Returns true if the entity is individually selectable (none of its ancestors are a closed container entity).
//! @note It may still be desirable to be able to 'click' an entity that is a descendant of a closed container
//! to select the container itself, not the individual entity.
bool IsVisibleEntityIndividuallySelectableInViewport(size_t index) const;
AZStd::optional<size_t> GetVisibleEntityIndexFromId(AZ::EntityId entityId) const; AZStd::optional<size_t> GetVisibleEntityIndexFromId(AZ::EntityId entityId) const;

@ -21,6 +21,9 @@ namespace AzToolsFramework::ViewportUi::Internal
{ {
const static int HighlightBorderSize = 5; const static int HighlightBorderSize = 5;
const static char* HighlightBorderColor = "#4A90E2"; const static char* HighlightBorderColor = "#4A90E2";
const static int HighlightBorderBackButtonMargin = 5;
const static int HighlightBorderBackButtonIconSize = 20;
const static char* HighlightBorderBackButtonIconFile = "X_axis.svg";
static void UnparentWidgets(ViewportUiElementIdInfoLookup& viewportUiElementIdInfoLookup) static void UnparentWidgets(ViewportUiElementIdInfoLookup& viewportUiElementIdInfoLookup)
{ {
@ -62,6 +65,7 @@ namespace AzToolsFramework::ViewportUi::Internal
, m_fullScreenLayout(&m_uiOverlay) , m_fullScreenLayout(&m_uiOverlay)
, m_uiOverlayLayout() , m_uiOverlayLayout()
, m_viewportBorderText(&m_uiOverlay) , m_viewportBorderText(&m_uiOverlay)
, m_viewportBorderBackButton(&m_uiOverlay)
{ {
} }
@ -291,7 +295,8 @@ namespace AzToolsFramework::ViewportUi::Internal
return false; return false;
} }
void ViewportUiDisplay::CreateViewportBorder(const AZStd::string& borderTitle) void ViewportUiDisplay::CreateViewportBorder(
const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback)
{ {
const AZStd::string styleSheet = AZStd::string::format( const AZStd::string styleSheet = AZStd::string::format(
"border: %dpx solid %s; border-top: %dpx solid %s;", HighlightBorderSize, HighlightBorderColor, ViewportUiTopBorderSize, "border: %dpx solid %s; border-top: %dpx solid %s;", HighlightBorderSize, HighlightBorderColor, ViewportUiTopBorderSize,
@ -302,6 +307,10 @@ namespace AzToolsFramework::ViewportUi::Internal
HighlightBorderSize + ViewportUiOverlayMargin, HighlightBorderSize + ViewportUiOverlayMargin); HighlightBorderSize + ViewportUiOverlayMargin, HighlightBorderSize + ViewportUiOverlayMargin);
m_viewportBorderText.setVisible(true); m_viewportBorderText.setVisible(true);
m_viewportBorderText.setText(borderTitle.c_str()); m_viewportBorderText.setText(borderTitle.c_str());
// only display the back button if a callback was provided
m_viewportBorderBackButtonCallback = backButtonCallback;
m_viewportBorderBackButton.setVisible(m_viewportBorderBackButtonCallback.has_value());
} }
void ViewportUiDisplay::RemoveViewportBorder() void ViewportUiDisplay::RemoveViewportBorder()
@ -311,6 +320,8 @@ namespace AzToolsFramework::ViewportUi::Internal
m_uiOverlayLayout.setContentsMargins( m_uiOverlayLayout.setContentsMargins(
ViewportUiOverlayMargin, ViewportUiOverlayMargin + ViewportUiOverlayTopMarginPadding, ViewportUiOverlayMargin, ViewportUiOverlayMargin, ViewportUiOverlayMargin + ViewportUiOverlayTopMarginPadding, ViewportUiOverlayMargin,
ViewportUiOverlayMargin); ViewportUiOverlayMargin);
m_viewportBorderBackButtonCallback.reset();
m_viewportBorderBackButton.setVisible(false);
} }
void ViewportUiDisplay::PositionViewportUiElementFromWorldSpace(ViewportUiElementId elementId, const AZ::Vector3& pos) void ViewportUiDisplay::PositionViewportUiElementFromWorldSpace(ViewportUiElementId elementId, const AZ::Vector3& pos)
@ -347,6 +358,8 @@ namespace AzToolsFramework::ViewportUi::Internal
void ViewportUiDisplay::InitializeUiOverlay() void ViewportUiDisplay::InitializeUiOverlay()
{ {
AZStd::string styleSheet;
m_uiMainWindow.setObjectName(QString("ViewportUiWindow")); m_uiMainWindow.setObjectName(QString("ViewportUiWindow"));
ConfigureWindowForViewportUi(&m_uiMainWindow); ConfigureWindowForViewportUi(&m_uiMainWindow);
m_uiMainWindow.setVisible(false); m_uiMainWindow.setVisible(false);
@ -361,11 +374,37 @@ namespace AzToolsFramework::ViewportUi::Internal
m_fullScreenLayout.addLayout(&m_uiOverlayLayout, 0, 0, 1, 1); m_fullScreenLayout.addLayout(&m_uiOverlayLayout, 0, 0, 1, 1);
// format the label which will appear on top of the highlight border // format the label which will appear on top of the highlight border
AZStd::string styleSheet = AZStd::string::format("background-color: %s; border: none;", HighlightBorderColor); styleSheet = AZStd::string::format("background-color: %s; border: none;", HighlightBorderColor);
m_viewportBorderText.setStyleSheet(styleSheet.c_str()); m_viewportBorderText.setStyleSheet(styleSheet.c_str());
m_viewportBorderText.setFixedHeight(ViewportUiTopBorderSize); m_viewportBorderText.setFixedHeight(ViewportUiTopBorderSize);
m_viewportBorderText.setVisible(false); m_viewportBorderText.setVisible(false);
m_fullScreenLayout.addWidget(&m_viewportBorderText, 0, 0, Qt::AlignTop | Qt::AlignHCenter); m_fullScreenLayout.addWidget(&m_viewportBorderText, 0, 0, Qt::AlignTop | Qt::AlignHCenter);
// format the back button which will appear in the top right of the highlight border
styleSheet = AZStd::string::format(
"border: 0px; padding-left: %dpx; padding-right: %dpx", HighlightBorderBackButtonMargin, HighlightBorderBackButtonMargin);
m_viewportBorderBackButton.setStyleSheet(styleSheet.c_str());
m_viewportBorderBackButton.setVisible(false);
QIcon backButtonIcon(QString(AZStd::string::format(":/stylesheet/img/UI20/toolbar/%s", HighlightBorderBackButtonIconFile).c_str()));
m_viewportBorderBackButton.setIcon(backButtonIcon);
m_viewportBorderBackButton.setIconSize(QSize(HighlightBorderBackButtonIconSize, HighlightBorderBackButtonIconSize));
// setup the handler for the back button to call the user provided callback (if any)
QObject::connect(
&m_viewportBorderBackButton, &QPushButton::clicked,
[this]()
{
if (m_viewportBorderBackButtonCallback.has_value())
{
// we need to swap out the existing back button callback because it will be reset in RemoveViewportBorder()
// so preserve the lifetime with this temporary callback until after the call to RemoveViewportBorder()
AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback;
m_viewportBorderBackButtonCallback.swap(backButtonCallback);
RemoveViewportBorder();
(*backButtonCallback)();
}
});
m_fullScreenLayout.addWidget(&m_viewportBorderBackButton, 0, 0, Qt::AlignTop | Qt::AlignRight);
} }
void ViewportUiDisplay::PrepareWidgetForViewportUi(QPointer<QWidget> widget) void ViewportUiDisplay::PrepareWidgetForViewportUi(QPointer<QWidget> widget)

@ -17,6 +17,7 @@
#include <QLabel> #include <QLabel>
#include <QMainWindow> #include <QMainWindow>
#include <QPointer> #include <QPointer>
#include <QPushButton>
AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option") AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option")
#include <QGridLayout> #include <QGridLayout>
@ -89,7 +90,7 @@ namespace AzToolsFramework::ViewportUi::Internal
AZStd::shared_ptr<QWidget> GetViewportUiElement(ViewportUiElementId elementId); AZStd::shared_ptr<QWidget> GetViewportUiElement(ViewportUiElementId elementId);
bool IsViewportUiElementVisible(ViewportUiElementId elementId); bool IsViewportUiElementVisible(ViewportUiElementId elementId);
void CreateViewportBorder(const AZStd::string& borderTitle); void CreateViewportBorder(const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback);
void RemoveViewportBorder(); void RemoveViewportBorder();
private: private:
@ -113,7 +114,10 @@ namespace AzToolsFramework::ViewportUi::Internal
QWidget m_uiOverlay; //!< The UI Overlay which displays Viewport UI Elements. QWidget m_uiOverlay; //!< The UI Overlay which displays Viewport UI Elements.
QGridLayout m_fullScreenLayout; //!< The layout which extends across the full screen. QGridLayout m_fullScreenLayout; //!< The layout which extends across the full screen.
ViewportUiDisplayLayout m_uiOverlayLayout; //!< The layout used for optionally anchoring Viewport UI Elements. ViewportUiDisplayLayout m_uiOverlayLayout; //!< The layout used for optionally anchoring Viewport UI Elements.
QLabel m_viewportBorderText; //!< The text used for the viewport border. QLabel m_viewportBorderText; //!< The text used for the viewport highlight border.
QPushButton m_viewportBorderBackButton; //!< The button to return from the viewport highlight border (only displayed if callback provided).
AZStd::optional<ViewportUiBackButtonCallback>
m_viewportBorderBackButtonCallback; //!< The optional callback for when the viewport highlight border back button is pressed.
QWidget* m_renderOverlay; QWidget* m_renderOverlay;
QPointer<QWidget> m_fullScreenWidget; //!< Reference to the widget attached to m_fullScreenLayout if any. QPointer<QWidget> m_fullScreenWidget; //!< Reference to the widget attached to m_fullScreenLayout if any.

@ -240,9 +240,10 @@ namespace AzToolsFramework::ViewportUi
} }
} }
void ViewportUiManager::CreateViewportBorder(const AZStd::string& borderTitle) void ViewportUiManager::CreateViewportBorder(
const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback)
{ {
m_viewportUi->CreateViewportBorder(borderTitle); m_viewportUi->CreateViewportBorder(borderTitle, backButtonCallback);
} }
void ViewportUiManager::RemoveViewportBorder() void ViewportUiManager::RemoveViewportBorder()

@ -50,7 +50,8 @@ namespace AzToolsFramework::ViewportUi
void RegisterTextFieldCallback(TextFieldId textFieldId, AZ::Event<AZStd::string>::Handler& handler) override; void RegisterTextFieldCallback(TextFieldId textFieldId, AZ::Event<AZStd::string>::Handler& handler) override;
void RemoveTextField(TextFieldId textFieldId) override; void RemoveTextField(TextFieldId textFieldId) override;
void SetTextFieldVisible(TextFieldId textFieldId, bool visible) override; void SetTextFieldVisible(TextFieldId textFieldId, bool visible) override;
void CreateViewportBorder(const AZStd::string& borderTitle) override; void CreateViewportBorder(
const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback) override;
void RemoveViewportBorder() override; void RemoveViewportBorder() override;
void PressButton(ClusterId clusterId, ButtonId buttonId) override; void PressButton(ClusterId clusterId, ButtonId buttonId) override;
void PressButton(SwitcherId switcherId, ButtonId buttonId) override; void PressButton(SwitcherId switcherId, ButtonId buttonId) override;

@ -22,6 +22,9 @@ namespace AzToolsFramework::ViewportUi
using SwitcherId = IdType<struct SwitcherIdType>; using SwitcherId = IdType<struct SwitcherIdType>;
using TextFieldId = IdType<struct TextFieldIdType>; using TextFieldId = IdType<struct TextFieldIdType>;
//! Callback function for viewport UI back button.
using ViewportUiBackButtonCallback = AZStd::function<void()>;
inline const ViewportUiElementId InvalidViewportUiElementId = ViewportUiElementId(0); inline const ViewportUiElementId InvalidViewportUiElementId = ViewportUiElementId(0);
inline const ButtonId InvalidButtonId = ButtonId(0); inline const ButtonId InvalidButtonId = ButtonId(0);
inline const ClusterId InvalidClusterId = ClusterId(0); inline const ClusterId InvalidClusterId = ClusterId(0);
@ -95,9 +98,9 @@ namespace AzToolsFramework::ViewportUi
virtual void RemoveTextField(TextFieldId textFieldId) = 0; virtual void RemoveTextField(TextFieldId textFieldId) = 0;
//! Sets the visibility of the text field. //! Sets the visibility of the text field.
virtual void SetTextFieldVisible(TextFieldId textFieldId, bool visible) = 0; virtual void SetTextFieldVisible(TextFieldId textFieldId, bool visible) = 0;
//! Create the highlight border for Component Mode. //! Create the highlight border with optional back button to exit the given editor mode.
virtual void CreateViewportBorder(const AZStd::string& borderTitle) = 0; virtual void CreateViewportBorder(const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback) = 0;
//! Remove the highlight border for Component Mode. //! Remove the highlight border.
virtual void RemoveViewportBorder() = 0; virtual void RemoveViewportBorder() = 0;
//! Invoke a button press on a cluster. //! Invoke a button press on a cluster.
virtual void PressButton(ClusterId clusterId, ButtonId buttonId) = 0; virtual void PressButton(ClusterId clusterId, ButtonId buttonId) = 0;

@ -47,6 +47,7 @@ set(FILES
API/EntityCompositionRequestBus.h API/EntityCompositionRequestBus.h
API/EntityCompositionNotificationBus.h API/EntityCompositionNotificationBus.h
API/EditorViewportIconDisplayInterface.h API/EditorViewportIconDisplayInterface.h
API/PythonLoader.h
API/ViewPaneOptions.h API/ViewPaneOptions.h
API/ViewportEditorModeTrackerInterface.h API/ViewportEditorModeTrackerInterface.h
Application/Ticker.h Application/Ticker.h

@ -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
*
*/
#include <AzToolsFramework/API/PythonLoader.h>
namespace AzToolsFramework::EmbeddedPython
{
PythonLoader::PythonLoader()
{
}
PythonLoader::~PythonLoader()
{
}
}

@ -0,0 +1,34 @@
/*
* 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/API/PythonLoader.h>
#include <AzCore/Debug/Trace.h>
#include <dlfcn.h>
namespace AzToolsFramework::EmbeddedPython
{
PythonLoader::PythonLoader()
{
constexpr char libPythonName[] = "libpython3.7m.so.1.0";
if (m_embeddedLibPythonHandle = dlopen(libPythonName, RTLD_NOW | RTLD_GLOBAL);
m_embeddedLibPythonHandle == nullptr)
{
char* err = dlerror();
AZ_Error("PythonLoader", false, "Failed to load %s with error: %s\n", libPythonName, err ? err : "Unknown Error");
}
}
PythonLoader::~PythonLoader()
{
if (m_embeddedLibPythonHandle)
{
dlclose(m_embeddedLibPythonHandle);
}
}
} // namespace AzToolsFramework::EmbeddedPython

@ -7,4 +7,5 @@
# #
set(FILES set(FILES
AzToolsFramework/API/PythonLoader_Linux.cpp
) )

@ -7,4 +7,5 @@
# #
set(FILES set(FILES
../Common/Default/AzToolsFramework/API/PythonLoader_Default.cpp
) )

@ -7,4 +7,5 @@
# #
set(FILES set(FILES
../Common/Default/AzToolsFramework/API/PythonLoader_Default.cpp
) )

@ -40,6 +40,9 @@ namespace UnitTest
{ {
AzFramework::BoundsRequestBus::Handler::BusConnect(GetEntityId()); AzFramework::BoundsRequestBus::Handler::BusConnect(GetEntityId());
AzToolsFramework::EditorComponentSelectionRequestsBus::Handler::BusConnect(GetEntityId()); AzToolsFramework::EditorComponentSelectionRequestsBus::Handler::BusConnect(GetEntityId());
// default local bounds to unit cube
m_localBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-0.5f), AZ::Vector3(0.5f));
} }
void BoundsTestComponent::Deactivate() void BoundsTestComponent::Deactivate()
@ -57,7 +60,6 @@ namespace UnitTest
AZ::Aabb BoundsTestComponent::GetLocalBounds() AZ::Aabb BoundsTestComponent::GetLocalBounds()
{ {
return AZ::Aabb::CreateFromMinMax(AZ::Vector3(-0.5f), AZ::Vector3(0.5f)); return m_localBounds;
} }
} // namespace UnitTest } // namespace UnitTest

@ -41,5 +41,7 @@ namespace UnitTest
// BoundsRequestBus overrides ... // BoundsRequestBus overrides ...
AZ::Aabb GetWorldBounds() override; AZ::Aabb GetWorldBounds() override;
AZ::Aabb GetLocalBounds() override; AZ::Aabb GetLocalBounds() override;
AZ::Aabb m_localBounds; //!< Local bounds that can be modified for certain tests (defaults to unit cube).
}; };
} // namespace UnitTest } // namespace UnitTest

@ -38,7 +38,7 @@
#include <AzToolsFramework/ViewportSelection/EditorVisibleEntityDataCache.h> #include <AzToolsFramework/ViewportSelection/EditorVisibleEntityDataCache.h>
#include <AzToolsFramework/ViewportUi/ViewportUiManager.h> #include <AzToolsFramework/ViewportUi/ViewportUiManager.h>
#include<Tests/BoundsTestComponent.h> #include <Tests/BoundsTestComponent.h>
namespace AZ namespace AZ
{ {
@ -493,12 +493,8 @@ namespace UnitTest
/////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Then // Then
AzToolsFramework::EntityIdList selectedEntities; const AzToolsFramework::EntityIdList selectedEntities = SelectedEntities();
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult( const AzToolsFramework::EntityIdList expectedSelectedEntities = { entity4, entity5, entity6 };
selectedEntities, &AzToolsFramework::ToolsApplicationRequestBus::Events::GetSelectedEntities);
AzToolsFramework::EntityIdList expectedSelectedEntities = { entity4, entity5, entity6 };
EXPECT_THAT(selectedEntities, UnorderedElementsAreArray(expectedSelectedEntities)); EXPECT_THAT(selectedEntities, UnorderedElementsAreArray(expectedSelectedEntities));
/////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
} }
@ -527,12 +523,8 @@ namespace UnitTest
/////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Then // Then
AzToolsFramework::EntityIdList selectedEntities; const AzToolsFramework::EntityIdList selectedEntities = SelectedEntities();
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult( const AzToolsFramework::EntityIdList expectedSelectedEntities = { m_entityId1, entity2, entity3, entity4 };
selectedEntities, &AzToolsFramework::ToolsApplicationRequestBus::Events::GetSelectedEntities);
AzToolsFramework::EntityIdList expectedSelectedEntities = { m_entityId1, entity2, entity3, entity4 };
EXPECT_THAT(selectedEntities, UnorderedElementsAreArray(expectedSelectedEntities)); EXPECT_THAT(selectedEntities, UnorderedElementsAreArray(expectedSelectedEntities));
/////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
} }
@ -946,6 +938,42 @@ namespace UnitTest
EXPECT_THAT(selectedEntitiesAfter, UnorderedElementsAre(m_entityId1)); EXPECT_THAT(selectedEntitiesAfter, UnorderedElementsAre(m_entityId1));
} }
TEST_F(
EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, BoundsBetweenCameraAndNearClipPlaneDoesNotIntersectMouseRay)
{
// move camera to 10 units along the y-axis
AzFramework::SetCameraTransform(m_cameraState, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f)));
// send a very narrow bounds for entity1
AZ::Entity* entity1 = AzToolsFramework::GetEntityById(m_entityId1);
auto* boundTestComponent = entity1->FindComponent<BoundsTestComponent>();
boundTestComponent->m_localBounds =
AZ::Aabb::CreateFromMinMax(AZ::Vector3(-0.5f, -0.0025f, -0.5f), AZ::Vector3(0.5f, 0.0025f, 0.5f));
// move entity1 in front of the camera between it and the near clip plane
AZ::TransformBus::Event(
m_entityId1, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.05f)));
// move entity2 behind entity1
AZ::TransformBus::Event(
m_entityId2, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(15.0f)));
const auto entity2ScreenPosition = AzFramework::WorldToScreen(AzToolsFramework::GetWorldTranslation(m_entityId2), m_cameraState);
// click the entity in the viewport
m_actionDispatcher->SetStickySelect(true)
->CameraState(m_cameraState)
->MousePosition(entity2ScreenPosition)
->CameraState(m_cameraState)
->MouseLButtonDown()
->MouseLButtonUp();
// ensure entity1 is not selected as it is before the near clip plane
using ::testing::UnorderedElementsAreArray;
const AzToolsFramework::EntityIdList selectedEntities = SelectedEntities();
const AzToolsFramework::EntityIdList expectedSelectedEntities = { m_entityId2 };
EXPECT_THAT(selectedEntities, UnorderedElementsAreArray(expectedSelectedEntities));
}
class EditorTransformComponentSelectionViewportPickingManipulatorTestFixtureParam class EditorTransformComponentSelectionViewportPickingManipulatorTestFixtureParam
: public EditorTransformComponentSelectionViewportPickingManipulatorTestFixture : public EditorTransformComponentSelectionViewportPickingManipulatorTestFixture
, public ::testing::WithParamInterface<bool> , public ::testing::WithParamInterface<bool>

@ -23,6 +23,7 @@
#include <AzManipulatorTestFramework/AzManipulatorTestFrameworkTestHelpers.h> #include <AzManipulatorTestFramework/AzManipulatorTestFrameworkTestHelpers.h>
#include <AzManipulatorTestFramework/IndirectManipulatorViewportInteraction.h> #include <AzManipulatorTestFramework/IndirectManipulatorViewportInteraction.h>
#include <AzManipulatorTestFramework/ImmediateModeActionDispatcher.h> #include <AzManipulatorTestFramework/ImmediateModeActionDispatcher.h>
#include <Tests/Utils/Printers.h>
using namespace AzToolsFramework; using namespace AzToolsFramework;

@ -17,6 +17,7 @@
#include <AzTest/AzTest.h> #include <AzTest/AzTest.h>
#include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h> #include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
#include <AzToolsFramework/Viewport/ViewportTypes.h> #include <AzToolsFramework/Viewport/ViewportTypes.h>
#include <Tests/Utils/Printers.h>
namespace UnitTest namespace UnitTest
{ {
@ -35,6 +36,7 @@ namespace UnitTest
const auto worldResult = AzFramework::ScreenToWorld(screenPoint, cameraState); const auto worldResult = AzFramework::ScreenToWorld(screenPoint, cameraState);
return AzFramework::WorldToScreen(worldResult, cameraState); return AzFramework::WorldToScreen(worldResult, cameraState);
} }
//////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////
// ScreenPoint tests // ScreenPoint tests
TEST(ViewportScreen, WorldToScreenAndScreenToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin) TEST(ViewportScreen, WorldToScreenAndScreenToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin)
@ -102,8 +104,8 @@ namespace UnitTest
} }
//////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////
// NDC tests // Ndc tests
TEST(ViewportScreen, WorldToScreenNDCAndScreenNDCToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin) TEST(ViewportScreen, WorldToScreenNdcAndScreenNdcToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin)
{ {
using NdcPoint = AZ::Vector2; using NdcPoint = AZ::Vector2;
@ -136,7 +138,7 @@ namespace UnitTest
} }
} }
TEST(ViewportScreen, WorldToScreenNDCAndScreenNDCToWorldReturnsTheSameValueOrientatedCamera) TEST(ViewportScreen, WorldToScreenNdcAndScreenNdcToWorldReturnsTheSameValueOrientatedCamera)
{ {
using NdcPoint = AZ::Vector2; using NdcPoint = AZ::Vector2;
@ -153,7 +155,7 @@ namespace UnitTest
// note: nearClip is 0.1 - the world space value returned will be aligned to the near clip // note: nearClip is 0.1 - the world space value returned will be aligned to the near clip
// plane of the camera so use that to confirm the mapping to/from is correct // plane of the camera so use that to confirm the mapping to/from is correct
TEST(ViewportScreen, ScreenNDCToWorldReturnsPositionOnNearClipPlaneInWorldSpace) TEST(ViewportScreen, ScreenNdcToWorldReturnsPositionOnNearClipPlaneInWorldSpace)
{ {
using NdcPoint = AZ::Vector2; using NdcPoint = AZ::Vector2;

@ -23,6 +23,8 @@
#include <AzCore/RTTI/BehaviorContext.h> #include <AzCore/RTTI/BehaviorContext.h>
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
#include <xxhash/xxhash.h>
namespace AssetBuilderSDK namespace AssetBuilderSDK
{ {
const char* const ErrorWindow = "Error"; //Use this window name to log error messages. const char* const ErrorWindow = "Error"; //Use this window name to log error messages.
@ -1599,4 +1601,70 @@ namespace AssetBuilderSDK
{ {
return m_errorsOccurred; return m_errorsOccurred;
} }
AZ::u64 GetHashFromIOStream(AZ::IO::GenericStream& readStream, AZ::IO::SizeType* bytesReadOut, int hashMsDelay)
{
constexpr AZ::u64 HashBufferSize = 1024 * 64;
char buffer[HashBufferSize];
if(readStream.IsOpen() && readStream.CanRead())
{
AZ::IO::SizeType bytesRead;
auto* state = XXH64_createState();
if(state == nullptr)
{
AZ_Assert(false, "Failed to create hash state");
return 0;
}
if (XXH64_reset(state, 0) == XXH_ERROR)
{
AZ_Assert(false, "Failed to reset hash state");
return 0;
}
do
{
// In edge cases where another process is writing to this file while this hashing is occuring and that file wasn't locked,
// the following read check can fail because it performs an end of file check, and asserts and shuts down if the read size
// was smaller than the buffer and the read is not at the end of the file. The logic used to check end of file internal to read
// will be out of date in the edge cases where another process is actively writing to this file while this hash is running.
// The stream's length ends up more accurate in this case, preventing this assert and shut down.
// One area this occurs is the navigation mesh file (mnmnavmission0.bai) that's temporarily created when exporting a level,
// the navigation system can still be writing to this file when hashing begins, causing the EoF marker to change.
AZ::IO::SizeType remainingToRead = AZStd::min(readStream.GetLength() - readStream.GetCurPos(), aznumeric_cast<AZ::IO::SizeType>(AZ_ARRAY_SIZE(buffer)));
bytesRead = readStream.Read(remainingToRead, buffer);
if(bytesReadOut)
{
*bytesReadOut += bytesRead;
}
XXH64_update(state, buffer, bytesRead);
// Used by unit tests to force the race condition mentioned above, to verify the crash fix.
if(hashMsDelay > 0)
{
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(hashMsDelay));
}
} while (bytesRead > 0);
auto hash = XXH64_digest(state);
XXH64_freeState(state);
return hash;
}
return 0;
}
AZ::u64 GetFileHash(const char* filePath, AZ::IO::SizeType* bytesReadOut, int hashMsDelay)
{
constexpr bool ErrorOnReadFailure = true;
AZ::IO::FileIOStream readStream(filePath, AZ::IO::OpenMode::ModeRead | AZ::IO::OpenMode::ModeBinary, ErrorOnReadFailure);
return GetHashFromIOStream(readStream, bytesReadOut, hashMsDelay);
}
} }

@ -910,6 +910,19 @@ namespace AssetBuilderSDK
//! There can be multiple builders running at once, so we need to filter out ones coming from other builders //! There can be multiple builders running at once, so we need to filter out ones coming from other builders
AZStd::thread_id m_jobThreadId; AZStd::thread_id m_jobThreadId;
}; };
//! Get hash for a whole file
//! @filePath the path for the file
//! @bytesReadOut output the read file size in bytes
//! @hashMsDelay [Do not use except for unit test] add a delay in ms for between each block reading.
AZ::u64 GetFileHash(const char* filePath, AZ::IO::SizeType* bytesReadOut = nullptr, int hashMsDelay = 0);
//! Get hash for a generic IO stream
//! @readStream the input readable stream
//! @bytesReadOut output the read size in bytes
//! @hashMsDelay [Do not use except for unit test] add a delay in ms for between each block reading.
AZ::u64 GetHashFromIOStream(AZ::IO::GenericStream& readStream, AZ::IO::SizeType* bytesReadOut = nullptr, int hashMsDelay = 0);
} // namespace AssetBuilderSDK } // namespace AssetBuilderSDK
namespace AZ namespace AZ

@ -32,6 +32,7 @@ ly_add_target(
PUBLIC PUBLIC
AZ::AzFramework AZ::AzFramework
AZ::AzToolsFramework AZ::AzToolsFramework
3rdParty::xxhash
) )
ly_add_source_properties( ly_add_source_properties(
SOURCES AssetBuilderSDK/AssetBuilderSDK.cpp SOURCES AssetBuilderSDK/AssetBuilderSDK.cpp

@ -32,7 +32,8 @@ struct FolderRootWatch::PlatformImplementation
{ {
if (m_iNotifyHandle < 0) if (m_iNotifyHandle < 0)
{ {
m_iNotifyHandle = inotify_init(); // The CLOEXEC flag prevents the inotify watchers from copying on fork/exec
m_iNotifyHandle = inotify_init1(IN_CLOEXEC);
} }
return (m_iNotifyHandle >= 0); return (m_iNotifyHandle >= 0);
} }

@ -1161,7 +1161,7 @@ namespace AssetUtilities
{ {
#ifndef AZ_TESTS_ENABLED #ifndef AZ_TESTS_ENABLED
// Only used for unit tests, speed is critical for GetFileHash. // Only used for unit tests, speed is critical for GetFileHash.
AZ_UNUSED(hashMsDelay); hashMsDelay = 0;
#endif #endif
bool useFileHashing = ShouldUseFileHashing(); bool useFileHashing = ShouldUseFileHashing();
@ -1170,10 +1170,10 @@ namespace AssetUtilities
return 0; return 0;
} }
AZ::u64 hash = 0;
if(!force) if(!force)
{ {
auto* fileStateInterface = AZ::Interface<AssetProcessor::IFileStateRequests>::Get(); auto* fileStateInterface = AZ::Interface<AssetProcessor::IFileStateRequests>::Get();
AZ::u64 hash = 0;
if (fileStateInterface && fileStateInterface->GetHash(filePath, &hash)) if (fileStateInterface && fileStateInterface->GetHash(filePath, &hash))
{ {
@ -1181,64 +1181,8 @@ namespace AssetUtilities
} }
} }
char buffer[FileHashBufferSize]; hash = AssetBuilderSDK::GetFileHash(filePath, bytesReadOut, hashMsDelay);
return hash;
constexpr bool ErrorOnReadFailure = true;
AZ::IO::FileIOStream readStream(filePath, AZ::IO::OpenMode::ModeRead | AZ::IO::OpenMode::ModeBinary, ErrorOnReadFailure);
if(readStream.IsOpen() && readStream.CanRead())
{
AZ::IO::SizeType bytesRead;
auto* state = XXH64_createState();
if(state == nullptr)
{
AZ_Assert(false, "Failed to create hash state");
return 0;
}
if (XXH64_reset(state, 0) == XXH_ERROR)
{
AZ_Assert(false, "Failed to reset hash state");
return 0;
}
do
{
// In edge cases where another process is writing to this file while this hashing is occuring and that file wasn't locked,
// the following read check can fail because it performs an end of file check, and asserts and shuts down if the read size
// was smaller than the buffer and the read is not at the end of the file. The logic used to check end of file internal to read
// will be out of date in the edge cases where another process is actively writing to this file while this hash is running.
// The stream's length ends up more accurate in this case, preventing this assert and shut down.
// One area this occurs is the navigation mesh file (mnmnavmission0.bai) that's temporarily created when exporting a level,
// the navigation system can still be writing to this file when hashing begins, causing the EoF marker to change.
AZ::IO::SizeType remainingToRead = AZStd::min(readStream.GetLength() - readStream.GetCurPos(), aznumeric_cast<AZ::IO::SizeType>(AZ_ARRAY_SIZE(buffer)));
bytesRead = readStream.Read(remainingToRead, buffer);
if(bytesReadOut)
{
*bytesReadOut += bytesRead;
}
XXH64_update(state, buffer, bytesRead);
#ifdef AZ_TESTS_ENABLED
// Used by unit tests to force the race condition mentioned above, to verify the crash fix.
if(hashMsDelay > 0)
{
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(hashMsDelay));
}
#endif
} while (bytesRead > 0);
auto hash = XXH64_digest(state);
XXH64_freeState(state);
return hash;
}
return 0;
} }
AZ::u64 AdjustTimestamp(QDateTime timestamp) AZ::u64 AdjustTimestamp(QDateTime timestamp)

@ -238,7 +238,6 @@ namespace AssetUtilities
// hashMsDelay is only for automated tests to test that writing to a file while it's hashing does not cause a crash. // hashMsDelay is only for automated tests to test that writing to a file while it's hashing does not cause a crash.
// hashMsDelay is not used in non-unit test builds. // hashMsDelay is not used in non-unit test builds.
AZ::u64 GetFileHash(const char* filePath, bool force = false, AZ::IO::SizeType* bytesReadOut = nullptr, int hashMsDelay = 0); AZ::u64 GetFileHash(const char* filePath, bool force = false, AZ::IO::SizeType* bytesReadOut = nullptr, int hashMsDelay = 0);
inline constexpr AZ::u64 FileHashBufferSize = 1024 * 64;
//! Adjusts a timestamp to fix timezone settings and account for any precision adjustment needed //! Adjusts a timestamp to fix timezone settings and account for any precision adjustment needed
AZ::u64 AdjustTimestamp(QDateTime timestamp); AZ::u64 AdjustTimestamp(QDateTime timestamp);

@ -72,12 +72,7 @@ namespace O3DE::ProjectManager
bool EngineScreenCtrl::ContainsScreen(ProjectManagerScreen screen) bool EngineScreenCtrl::ContainsScreen(ProjectManagerScreen screen)
{ {
if (screen == m_engineSettingsScreen->GetScreenEnum() || screen == m_gemRepoScreen->GetScreenEnum()) return screen == m_engineSettingsScreen->GetScreenEnum() || screen == m_gemRepoScreen->GetScreenEnum();
{
return true;
}
return false;
} }
void EngineScreenCtrl::NotifyCurrentScreen() void EngineScreenCtrl::NotifyCurrentScreen()

@ -188,7 +188,7 @@ namespace O3DE::ProjectManager
} }
// add all the gem repos into the hash // add all the gem repos into the hash
const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetAllGemRepoGemsInfos(); const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos();
if (allRepoGemInfosResult.IsSuccess()) if (allRepoGemInfosResult.IsSuccess())
{ {
const QVector<GemInfo>& allRepoGemInfos = allRepoGemInfosResult.GetValue(); const QVector<GemInfo>& allRepoGemInfos = allRepoGemInfosResult.GetValue();
@ -363,7 +363,10 @@ namespace O3DE::ProjectManager
{ {
const QString selectedGemPath = m_gemModel->GetPath(modelIndex); const QString selectedGemPath = m_gemModel->GetPath(modelIndex);
// Remove gem from gems to be added const bool wasAdded = GemModel::WasPreviouslyAdded(modelIndex);
const bool wasAddedDependency = GemModel::WasPreviouslyAddedDependency(modelIndex);
// Remove gem from gems to be added to update any dependencies
GemModel::SetIsAdded(*m_gemModel, modelIndex, false); GemModel::SetIsAdded(*m_gemModel, modelIndex, false);
// Unregister the gem // Unregister the gem
@ -391,6 +394,8 @@ namespace O3DE::ProjectManager
// Select remote gem // Select remote gem
QModelIndex remoteGemIndex = m_gemModel->FindIndexByNameString(selectedGemName); QModelIndex remoteGemIndex = m_gemModel->FindIndexByNameString(selectedGemName);
GemModel::SetWasPreviouslyAdded(*m_gemModel, remoteGemIndex, wasAdded);
GemModel::SetWasPreviouslyAddedDependency(*m_gemModel, remoteGemIndex, wasAddedDependency);
QModelIndex proxyIndex = m_proxyModel->mapFromSource(remoteGemIndex); QModelIndex proxyIndex = m_proxyModel->mapFromSource(remoteGemIndex);
m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect); m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect);
} }
@ -435,7 +440,7 @@ namespace O3DE::ProjectManager
m_gemModel->AddGem(gemInfo); m_gemModel->AddGem(gemInfo);
} }
const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetAllGemRepoGemsInfos(); const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos();
if (allRepoGemInfosResult.IsSuccess()) if (allRepoGemInfosResult.IsSuccess())
{ {
const QVector<GemInfo>& allRepoGemInfos = allRepoGemInfosResult.GetValue(); const QVector<GemInfo>& allRepoGemInfos = allRepoGemInfosResult.GetValue();
@ -523,7 +528,9 @@ namespace O3DE::ProjectManager
const QString& gemPath = GemModel::GetPath(modelIndex); const QString& gemPath = GemModel::GetPath(modelIndex);
// make sure any remote gems we added were downloaded successfully // make sure any remote gems we added were downloaded successfully
if (GemModel::GetGemOrigin(modelIndex) == GemInfo::Remote && GemModel::GetDownloadStatus(modelIndex) != GemInfo::Downloaded) const GemInfo::DownloadStatus status = GemModel::GetDownloadStatus(modelIndex);
if (GemModel::GetGemOrigin(modelIndex) == GemInfo::Remote &&
!(status == GemInfo::Downloaded || status == GemInfo::DownloadSuccessful))
{ {
QMessageBox::critical( QMessageBox::critical(
nullptr, "Cannot add gem that isn't downloaded", nullptr, "Cannot add gem that isn't downloaded",

@ -53,10 +53,13 @@ namespace O3DE::ProjectManager
Update(selectedIndices[0]); Update(selectedIndices[0]);
} }
void SetLabelElidedText(QLabel* label, QString text) void SetLabelElidedText(QLabel* label, QString text, int labelWidth = 0)
{ {
QFontMetrics nameFontMetrics(label->font()); QFontMetrics nameFontMetrics(label->font());
int labelWidth = label->width(); if (!labelWidth)
{
labelWidth = label->width();
}
// Don't elide if the widgets are sized too small (sometimes occurs when loading gem catalog) // Don't elide if the widgets are sized too small (sometimes occurs when loading gem catalog)
if (labelWidth > 100) if (labelWidth > 100)
@ -84,7 +87,8 @@ namespace O3DE::ProjectManager
m_summaryLabel->setText(m_model->GetSummary(modelIndex)); m_summaryLabel->setText(m_model->GetSummary(modelIndex));
m_summaryLabel->adjustSize(); m_summaryLabel->adjustSize();
m_licenseLinkLabel->setText(m_model->GetLicenseText(modelIndex)); // Manually define remaining space to elide text because spacer would like to take all of the space
SetLabelElidedText(m_licenseLinkLabel, m_model->GetLicenseText(modelIndex), width() - m_licenseLabel->width() - 35);
m_licenseLinkLabel->SetUrl(m_model->GetLicenseLink(modelIndex)); m_licenseLinkLabel->SetUrl(m_model->GetLicenseLink(modelIndex));
m_directoryLinkLabel->SetUrl(m_model->GetDirectoryLink(modelIndex)); m_directoryLinkLabel->SetUrl(m_model->GetDirectoryLink(modelIndex));
@ -175,8 +179,8 @@ namespace O3DE::ProjectManager
licenseHLayout->setAlignment(Qt::AlignLeft); licenseHLayout->setAlignment(Qt::AlignLeft);
m_mainLayout->addLayout(licenseHLayout); m_mainLayout->addLayout(licenseHLayout);
QLabel* licenseLabel = CreateStyledLabel(licenseHLayout, s_baseFontSize, s_headerColor); m_licenseLabel = CreateStyledLabel(licenseHLayout, s_baseFontSize, s_headerColor);
licenseLabel->setText(tr("License: ")); m_licenseLabel->setText(tr("License: "));
m_licenseLinkLabel = new LinkLabel("", QUrl(), s_baseFontSize); m_licenseLinkLabel = new LinkLabel("", QUrl(), s_baseFontSize);
licenseHLayout->addWidget(m_licenseLinkLabel); licenseHLayout->addWidget(m_licenseLinkLabel);

@ -64,6 +64,7 @@ namespace O3DE::ProjectManager
QLabel* m_nameLabel = nullptr; QLabel* m_nameLabel = nullptr;
QLabel* m_creatorLabel = nullptr; QLabel* m_creatorLabel = nullptr;
QLabel* m_summaryLabel = nullptr; QLabel* m_summaryLabel = nullptr;
QLabel* m_licenseLabel = nullptr;
LinkLabel* m_licenseLinkLabel = nullptr; LinkLabel* m_licenseLinkLabel = nullptr;
LinkLabel* m_directoryLinkLabel = nullptr; LinkLabel* m_directoryLinkLabel = nullptr;
LinkLabel* m_documentationLinkLabel = nullptr; LinkLabel* m_documentationLinkLabel = nullptr;

@ -37,7 +37,7 @@ namespace O3DE::ProjectManager
QString m_additionalInfo = ""; QString m_additionalInfo = "";
QString m_directoryLink = ""; QString m_directoryLink = "";
QString m_repoUri = ""; QString m_repoUri = "";
QStringList m_includedGemPaths = {}; QStringList m_includedGemUris = {};
QDateTime m_lastUpdated; QDateTime m_lastUpdated;
}; };
} // namespace O3DE::ProjectManager } // namespace O3DE::ProjectManager

@ -8,6 +8,7 @@
#include <GemRepo/GemRepoInspector.h> #include <GemRepo/GemRepoInspector.h>
#include <GemRepo/GemRepoItemDelegate.h> #include <GemRepo/GemRepoItemDelegate.h>
#include <PythonBindingsInterface.h>
#include <QFrame> #include <QFrame>
#include <QLabel> #include <QLabel>
@ -60,8 +61,10 @@ namespace O3DE::ProjectManager
// Repo name and url link // Repo name and url link
m_nameLabel->setText(m_model->GetName(modelIndex)); m_nameLabel->setText(m_model->GetName(modelIndex));
m_repoLinkLabel->setText(m_model->GetRepoUri(modelIndex));
m_repoLinkLabel->SetUrl(m_model->GetRepoUri(modelIndex)); const QString repoUri = m_model->GetRepoUri(modelIndex);
m_repoLinkLabel->setText(repoUri);
m_repoLinkLabel->SetUrl(repoUri);
// Repo summary // Repo summary
m_summaryLabel->setText(m_model->GetSummary(modelIndex)); m_summaryLabel->setText(m_model->GetSummary(modelIndex));

@ -41,7 +41,7 @@ namespace O3DE::ProjectManager
item->setData(gemRepoInfo.m_lastUpdated, RoleLastUpdated); item->setData(gemRepoInfo.m_lastUpdated, RoleLastUpdated);
item->setData(gemRepoInfo.m_path, RolePath); item->setData(gemRepoInfo.m_path, RolePath);
item->setData(gemRepoInfo.m_additionalInfo, RoleAdditionalInfo); item->setData(gemRepoInfo.m_additionalInfo, RoleAdditionalInfo);
item->setData(gemRepoInfo.m_includedGemPaths, RoleIncludedGems); item->setData(gemRepoInfo.m_includedGemUris, RoleIncludedGems);
appendRow(item); appendRow(item);
@ -98,7 +98,7 @@ namespace O3DE::ProjectManager
return modelIndex.data(RolePath).toString(); return modelIndex.data(RolePath).toString();
} }
QStringList GemRepoModel::GetIncludedGemPaths(const QModelIndex& modelIndex) QStringList GemRepoModel::GetIncludedGemUris(const QModelIndex& modelIndex)
{ {
return modelIndex.data(RoleIncludedGems).toStringList(); return modelIndex.data(RoleIncludedGems).toStringList();
} }
@ -118,23 +118,19 @@ namespace O3DE::ProjectManager
QVector<GemInfo> GemRepoModel::GetIncludedGemInfos(const QModelIndex& modelIndex) QVector<GemInfo> GemRepoModel::GetIncludedGemInfos(const QModelIndex& modelIndex)
{ {
QVector<GemInfo> allGemInfos; QString repoUri = GetRepoUri(modelIndex);
QStringList repoGemPaths = GetIncludedGemPaths(modelIndex);
for (const QString& gemPath : repoGemPaths) const AZ::Outcome<QVector<GemInfo>, AZStd::string>& gemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForRepo(repoUri);
if (gemInfosResult.IsSuccess())
{ {
AZ::Outcome<GemInfo> gemInfoResult = PythonBindingsInterface::Get()->GetGemInfo(gemPath); return gemInfosResult.GetValue();
if (gemInfoResult.IsSuccess()) }
{ else
allGemInfos.append(gemInfoResult.GetValue()); {
} QMessageBox::critical(nullptr, tr("Gems not found"), tr("Cannot find info for gems from repo %1").arg(GetName(modelIndex)));
else
{
QMessageBox::critical(nullptr, tr("Gem Not Found"), tr("Cannot find info for gem %1.").arg(gemPath));
}
} }
return allGemInfos; return QVector<GemInfo>();
} }
bool GemRepoModel::IsEnabled(const QModelIndex& modelIndex) bool GemRepoModel::IsEnabled(const QModelIndex& modelIndex)

@ -39,7 +39,7 @@ namespace O3DE::ProjectManager
static QDateTime GetLastUpdated(const QModelIndex& modelIndex); static QDateTime GetLastUpdated(const QModelIndex& modelIndex);
static QString GetPath(const QModelIndex& modelIndex); static QString GetPath(const QModelIndex& modelIndex);
static QStringList GetIncludedGemPaths(const QModelIndex& modelIndex); static QStringList GetIncludedGemUris(const QModelIndex& modelIndex);
static QVector<Tag> GetIncludedGemTags(const QModelIndex& modelIndex); static QVector<Tag> GetIncludedGemTags(const QModelIndex& modelIndex);
static QVector<GemInfo> GetIncludedGemInfos(const QModelIndex& modelIndex); static QVector<GemInfo> GetIncludedGemInfos(const QModelIndex& modelIndex);

@ -9,12 +9,14 @@
#include <ProjectBuilderController.h> #include <ProjectBuilderController.h>
#include <ProjectBuilderWorker.h> #include <ProjectBuilderWorker.h>
#include <ProjectButtonWidget.h> #include <ProjectButtonWidget.h>
#include <ProjectManagerSettings.h>
#include <AzCore/Settings/SettingsRegistry.h>
#include <QMessageBox> #include <QMessageBox>
#include <QDesktopServices> #include <QDesktopServices>
#include <QUrl> #include <QUrl>
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
ProjectBuilderController::ProjectBuilderController(const ProjectInfo& projectInfo, ProjectButton* projectButton, QWidget* parent) ProjectBuilderController::ProjectBuilderController(const ProjectInfo& projectInfo, ProjectButton* projectButton, QWidget* parent)
@ -27,6 +29,15 @@ namespace O3DE::ProjectManager
m_worker = new ProjectBuilderWorker(m_projectInfo); m_worker = new ProjectBuilderWorker(m_projectInfo);
m_worker->moveToThread(&m_workerThread); m_worker->moveToThread(&m_workerThread);
auto settingsRegistry = AZ::SettingsRegistry::Get();
if (settingsRegistry)
{
// Remove key here in case Project Manager crashing while building that causes HandleResults to not be called
QString settingsKey = GetProjectBuiltSuccessfullyKey(m_projectInfo.m_projectName);
settingsRegistry->Remove(settingsKey.toStdString().c_str());
SaveProjectManagerSettings();
}
connect(&m_workerThread, &QThread::finished, m_worker, &ProjectBuilderWorker::deleteLater); connect(&m_workerThread, &QThread::finished, m_worker, &ProjectBuilderWorker::deleteLater);
connect(&m_workerThread, &QThread::started, m_worker, &ProjectBuilderWorker::BuildProject); connect(&m_workerThread, &QThread::started, m_worker, &ProjectBuilderWorker::BuildProject);
connect(m_worker, &ProjectBuilderWorker::Done, this, &ProjectBuilderController::HandleResults); connect(m_worker, &ProjectBuilderWorker::Done, this, &ProjectBuilderController::HandleResults);
@ -80,6 +91,8 @@ namespace O3DE::ProjectManager
void ProjectBuilderController::HandleResults(const QString& result) void ProjectBuilderController::HandleResults(const QString& result)
{ {
QString settingsKey = GetProjectBuiltSuccessfullyKey(m_projectInfo.m_projectName);
if (!result.isEmpty()) if (!result.isEmpty())
{ {
if (result.contains(tr("log"))) if (result.contains(tr("log")))
@ -109,12 +122,26 @@ namespace O3DE::ProjectManager
emit NotifyBuildProject(m_projectInfo); emit NotifyBuildProject(m_projectInfo);
} }
auto settingsRegistry = AZ::SettingsRegistry::Get();
if (settingsRegistry)
{
settingsRegistry->Remove(settingsKey.toStdString().c_str());
SaveProjectManagerSettings();
}
emit Done(false); emit Done(false);
return; return;
} }
else else
{ {
m_projectInfo.m_buildFailed = false; m_projectInfo.m_buildFailed = false;
auto settingsRegistry = AZ::SettingsRegistry::Get();
if (settingsRegistry)
{
settingsRegistry->Set(settingsKey.toStdString().c_str(), true);
SaveProjectManagerSettings();
}
} }
emit Done(true); emit Done(true);

@ -203,6 +203,7 @@ namespace O3DE::ProjectManager
QMenu* menu = new QMenu(this); QMenu* menu = new QMenu(this);
menu->addAction(tr("Edit Project Settings..."), this, [this]() { emit EditProject(m_projectInfo.m_path); }); menu->addAction(tr("Edit Project Settings..."), this, [this]() { emit EditProject(m_projectInfo.m_path); });
menu->addAction(tr("Configure Gems..."), this, [this]() { emit EditProjectGems(m_projectInfo.m_path); });
menu->addAction(tr("Build"), this, [this]() { emit BuildProject(m_projectInfo); }); menu->addAction(tr("Build"), this, [this]() { emit BuildProject(m_projectInfo); });
menu->addAction(tr("Open CMake GUI..."), this, [this]() { emit OpenCMakeGUI(m_projectInfo); }); menu->addAction(tr("Open CMake GUI..."), this, [this]() { emit OpenCMakeGUI(m_projectInfo); });
menu->addSeparator(); menu->addSeparator();

@ -95,6 +95,7 @@ namespace O3DE::ProjectManager
signals: signals:
void OpenProject(const QString& projectName); void OpenProject(const QString& projectName);
void EditProject(const QString& projectName); void EditProject(const QString& projectName);
void EditProjectGems(const QString& projectName);
void CopyProject(const ProjectInfo& projectInfo); void CopyProject(const ProjectInfo& projectInfo);
void RemoveProject(const QString& projectName); void RemoveProject(const QString& projectName);
void DeleteProject(const QString& projectName); void DeleteProject(const QString& projectName);

@ -0,0 +1,54 @@
/*
* 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 "ProjectManagerSettings.h"
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/IO/ByteContainerStream.h>
#include <AzCore/Utils/Utils.h>
namespace O3DE::ProjectManager
{
void SaveProjectManagerSettings()
{
auto settingsRegistry = AZ::SettingsRegistry::Get();
AZ::SettingsRegistryMergeUtils::DumperSettings dumperSettings;
dumperSettings.m_prettifyOutput = true;
dumperSettings.m_jsonPointerPrefix = ProjectManagerKeyPrefix;
AZStd::string stringBuffer;
AZ::IO::ByteContainerStream stringStream(&stringBuffer);
if (!AZ::SettingsRegistryMergeUtils::DumpSettingsRegistryToStream(
*settingsRegistry, ProjectManagerKeyPrefix, stringStream, dumperSettings))
{
AZ_Warning("ProjectManager", false, "Could not save Project Manager settings to stream");
return;
}
AZ::IO::FixedMaxPath o3deUserPath = AZ::Utils::GetO3deManifestDirectory();
o3deUserPath /= AZ::SettingsRegistryInterface::RegistryFolder;
o3deUserPath /= "ProjectManager.setreg";
bool saved = false;
constexpr auto configurationMode =
AZ::IO::SystemFile::SF_OPEN_CREATE | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY;
AZ::IO::SystemFile outputFile;
if (outputFile.Open(o3deUserPath.c_str(), configurationMode))
{
saved = outputFile.Write(stringBuffer.data(), stringBuffer.size()) == stringBuffer.size();
}
AZ_Warning("ProjectManager", saved, "Unable to save Project Manager registry file to path: %s", o3deUserPath.c_str());
}
QString GetProjectBuiltSuccessfullyKey(const QString& projectName)
{
return QString("%1/Projects/%2/BuiltSuccessfully").arg(ProjectManagerKeyPrefix).arg(projectName);
}
}

@ -0,0 +1,21 @@
/*
* 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
#if !defined(Q_MOC_RUN)
#include <QString>
#endif
namespace O3DE::ProjectManager
{
static constexpr char ProjectManagerKeyPrefix[] = "/O3DE/ProjectManager";
void SaveProjectManagerSettings();
QString GetProjectBuiltSuccessfullyKey(const QString& projectName);
}

@ -19,6 +19,7 @@
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QStandardPaths> #include <QStandardPaths>
#include <QScrollArea>
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
@ -33,11 +34,23 @@ namespace O3DE::ProjectManager
// if we don't set this in a frame (just use a sub-layout) all the content will align incorrectly horizontally // if we don't set this in a frame (just use a sub-layout) all the content will align incorrectly horizontally
QFrame* projectSettingsFrame = new QFrame(this); QFrame* projectSettingsFrame = new QFrame(this);
projectSettingsFrame->setObjectName("projectSettings"); projectSettingsFrame->setObjectName("projectSettings");
m_verticalLayout = new QVBoxLayout();
// you cannot remove content margins in qss QVBoxLayout* vLayout = new QVBoxLayout();
m_verticalLayout->setContentsMargins(0, 0, 0, 0); vLayout->setMargin(0);
vLayout->setAlignment(Qt::AlignTop);
projectSettingsFrame->setLayout(vLayout);
QScrollArea* scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
vLayout->addWidget(scrollArea);
QWidget* scrollWidget = new QWidget(this);
scrollArea->setWidget(scrollWidget);
m_verticalLayout = new QVBoxLayout();
m_verticalLayout->setMargin(0);
m_verticalLayout->setAlignment(Qt::AlignTop); m_verticalLayout->setAlignment(Qt::AlignTop);
scrollWidget->setLayout(m_verticalLayout);
m_projectName = new FormLineEditWidget(tr("Project name"), "", this); m_projectName = new FormLineEditWidget(tr("Project name"), "", this);
connect(m_projectName->lineEdit(), &QLineEdit::textChanged, this, &ProjectSettingsScreen::OnProjectNameUpdated); connect(m_projectName->lineEdit(), &QLineEdit::textChanged, this, &ProjectSettingsScreen::OnProjectNameUpdated);

@ -14,6 +14,7 @@
#include <ProjectUtils.h> #include <ProjectUtils.h>
#include <ProjectBuilderController.h> #include <ProjectBuilderController.h>
#include <ScreensCtrl.h> #include <ScreensCtrl.h>
#include <ProjectManagerSettings.h>
#include <AzQtComponents/Components/FlowLayout.h> #include <AzQtComponents/Components/FlowLayout.h>
#include <AzCore/Platform.h> #include <AzCore/Platform.h>
@ -22,6 +23,7 @@
#include <AzFramework/Process/ProcessCommon.h> #include <AzFramework/Process/ProcessCommon.h>
#include <AzFramework/Process/ProcessWatcher.h> #include <AzFramework/Process/ProcessWatcher.h>
#include <AzCore/Utils/Utils.h> #include <AzCore/Utils/Utils.h>
#include <AzCore/Settings/SettingsRegistry.h>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
@ -181,6 +183,7 @@ namespace O3DE::ProjectManager
connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject); connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject);
connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject); connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject);
connect(projectButton, &ProjectButton::EditProjectGems, this, &ProjectsScreen::HandleEditProjectGems);
connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject); connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject);
connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject); connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject);
connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject); connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject);
@ -269,17 +272,36 @@ namespace O3DE::ProjectManager
// Add any missing project buttons and restore buttons to default state // Add any missing project buttons and restore buttons to default state
for (const ProjectInfo& project : projectsVector) for (const ProjectInfo& project : projectsVector)
{ {
ProjectButton* currentButton = nullptr;
if (!m_projectButtons.contains(QDir::toNativeSeparators(project.m_path))) if (!m_projectButtons.contains(QDir::toNativeSeparators(project.m_path)))
{ {
m_projectButtons.insert(QDir::toNativeSeparators(project.m_path), CreateProjectButton(project)); currentButton = CreateProjectButton(project);
m_projectButtons.insert(QDir::toNativeSeparators(project.m_path), currentButton);
} }
else else
{ {
auto projectButtonIter = m_projectButtons.find(QDir::toNativeSeparators(project.m_path)); auto projectButtonIter = m_projectButtons.find(QDir::toNativeSeparators(project.m_path));
if (projectButtonIter != m_projectButtons.end()) if (projectButtonIter != m_projectButtons.end())
{ {
projectButtonIter.value()->RestoreDefaultState(); currentButton = projectButtonIter.value();
m_projectsFlowLayout->addWidget(projectButtonIter.value()); currentButton->RestoreDefaultState();
m_projectsFlowLayout->addWidget(currentButton);
}
}
// Check whether project manager has successfully built the project
if (currentButton)
{
auto settingsRegistry = AZ::SettingsRegistry::Get();
bool projectBuiltSuccessfully = false;
if (settingsRegistry)
{
QString settingsKey = GetProjectBuiltSuccessfullyKey(project.m_projectName);
settingsRegistry->Get(projectBuiltSuccessfully, settingsKey.toStdString().c_str());
}
if (!projectBuiltSuccessfully)
{
currentButton->ShowBuildRequired();
} }
} }
} }
@ -448,6 +470,14 @@ namespace O3DE::ProjectManager
emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject); emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
} }
} }
void ProjectsScreen::HandleEditProjectGems(const QString& projectPath)
{
if (!WarnIfInBuildQueue(projectPath))
{
emit NotifyCurrentProject(projectPath);
emit ChangeScreenRequest(ProjectManagerScreen::GemCatalog);
}
}
void ProjectsScreen::HandleCopyProject(const ProjectInfo& projectInfo) void ProjectsScreen::HandleCopyProject(const ProjectInfo& projectInfo)
{ {
if (!WarnIfInBuildQueue(projectInfo.m_path)) if (!WarnIfInBuildQueue(projectInfo.m_path))

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

Loading…
Cancel
Save