Reorganized Prefab tests and standarization (#4786)
* Reorganized Prefab tests and standarization * Restore changed file by accident
This commit is contained in:
+26
-6
@@ -122,17 +122,32 @@ class EditorEntity:
|
||||
|
||||
# Creation functions
|
||||
@classmethod
|
||||
def find_editor_entity(cls, entity_name: str) -> EditorEntity:
|
||||
def find_editor_entity(cls, entity_name: str, must_be_unique : bool = False) -> EditorEntity:
|
||||
"""
|
||||
Given Entity name, outputs entity object
|
||||
:param entity_name: Name of entity to find
|
||||
:return: EditorEntity class object
|
||||
"""
|
||||
entity_id = general.find_editor_entity(entity_name)
|
||||
assert entity_id.IsValid(), f"Failure: Couldn't find entity with name: '{entity_name}'"
|
||||
entity = cls(entity_id)
|
||||
entities = cls.find_editor_entities([entity_name])
|
||||
assert len(entities) != 0, f"Failure: Couldn't find entity with name: '{entity_name}'"
|
||||
if must_be_unique:
|
||||
assert len(entities) == 1, f"Failure: Multiple entities with name: '{entity_name}' when expected only one"
|
||||
|
||||
entity = cls(entities[0])
|
||||
return entity
|
||||
|
||||
@classmethod
|
||||
def find_editor_entities(cls, entity_names: List[str]) -> EditorEntity:
|
||||
"""
|
||||
Given Entities names, returns a list of EditorEntity
|
||||
:param entity_name: Name of entity to find
|
||||
:return: List[EditorEntity] class object
|
||||
"""
|
||||
searchFilter = azlmbr.entity.SearchFilter()
|
||||
searchFilter.names = entity_names
|
||||
ids = azlmbr.entity.SearchBus(bus.Broadcast, 'SearchEntities', searchFilter)
|
||||
return [cls(id) for id in ids]
|
||||
|
||||
@classmethod
|
||||
def create_editor_entity(cls, name: str = None, parent_id=None) -> EditorEntity:
|
||||
"""
|
||||
@@ -157,8 +172,7 @@ class EditorEntity:
|
||||
cls,
|
||||
entity_position: Union[List, Tuple, math.Vector3],
|
||||
name: str = None,
|
||||
parent_id: azlmbr.entity.EntityId = None,
|
||||
) -> EditorEntity:
|
||||
parent_id: azlmbr.entity.EntityId = None) -> EditorEntity:
|
||||
"""
|
||||
Used to create entity at position using 'CreateNewEntityAtPosition' Bus.
|
||||
:param entity_position: World Position(X, Y, Z) of entity in viewport.
|
||||
@@ -227,6 +241,12 @@ class EditorEntity:
|
||||
"""
|
||||
return editor.EditorEntityInfoRequestBus(bus.Event, "GetChildren", self.id)
|
||||
|
||||
def get_children(self) -> List[EditorEntity]:
|
||||
"""
|
||||
:return: List of EditorEntity children. Type: [EditorEntity]
|
||||
"""
|
||||
return [EditorEntity(child_id) for child_id in self.get_children_ids()]
|
||||
|
||||
def add_component(self, component_name: str) -> EditorComponent:
|
||||
"""
|
||||
Used to add new component to Entity.
|
||||
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from collections import Counter
|
||||
from collections import deque
|
||||
from os import path
|
||||
|
||||
from PySide2 import QtWidgets
|
||||
|
||||
import azlmbr.legacy.general as general
|
||||
from azlmbr.entity import EntityId
|
||||
from azlmbr.math import Vector3
|
||||
from editor_python_test_tools.editor_entity_utils import EditorEntity
|
||||
from editor_python_test_tools.utils import Report
|
||||
|
||||
import azlmbr.entity as entity
|
||||
import azlmbr.bus as bus
|
||||
import azlmbr.prefab as prefab
|
||||
import editor_python_test_tools.pyside_utils as pyside_utils
|
||||
|
||||
|
||||
def get_prefab_file_path(prefab_path):
|
||||
if not path.isabs(prefab_path):
|
||||
prefab_path = path.join(general.get_file_alias("@projectroot@"), prefab_path)
|
||||
|
||||
# Append prefab if it doesn't contain .prefab on it
|
||||
name, ext = path.splitext(prefab_path)
|
||||
if ext != ".prefab":
|
||||
prefab_path = name + ".prefab"
|
||||
return prefab_path
|
||||
|
||||
|
||||
def get_all_entity_ids():
|
||||
return entity.SearchBus(bus.Broadcast, 'SearchEntities', entity.SearchFilter())
|
||||
|
||||
def wait_for_propagation():
|
||||
general.idle_wait_frames(1)
|
||||
|
||||
# This is a helper class which contains some of the useful information about a prefab instance.
|
||||
class PrefabInstance:
|
||||
|
||||
def __init__(self, prefab_file_name: str = None, container_entity: EditorEntity = None):
|
||||
self.prefab_file_name: str = prefab_file_name
|
||||
self.container_entity: EditorEntity = container_entity
|
||||
|
||||
def __eq__(self, other):
|
||||
return other and self.container_entity.id == other.container_entity.id
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.container_entity.id)
|
||||
|
||||
"""
|
||||
See if this instance is valid to be used with other prefab operations.
|
||||
:return: Whether the target instance is valid or not.
|
||||
"""
|
||||
def is_valid(self) -> bool:
|
||||
return self.container_entity.id.IsValid() and self.prefab_file_name in Prefab.existing_prefabs
|
||||
|
||||
"""
|
||||
Reparent this instance to target parent entity.
|
||||
The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs.
|
||||
:param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next.
|
||||
"""
|
||||
async def ui_reparent_prefab_instance(self, parent_entity_id: EntityId):
|
||||
container_entity_id_before_reparent = self.container_entity.id
|
||||
|
||||
original_parent = EditorEntity(self.container_entity.get_parent_id())
|
||||
original_parent_before_reparent_children_ids = set(original_parent.get_children_ids())
|
||||
|
||||
new_parent = EditorEntity(parent_entity_id)
|
||||
new_parent_before_reparent_children_ids = set(new_parent.get_children_ids())
|
||||
|
||||
pyside_utils.run_soon(lambda: self.container_entity.set_parent_entity(parent_entity_id))
|
||||
pyside_utils.run_soon(lambda: wait_for_propagation())
|
||||
|
||||
try:
|
||||
active_modal_widget = await pyside_utils.wait_for_modal_widget()
|
||||
error_message_box = active_modal_widget.findChild(QtWidgets.QMessageBox)
|
||||
ok_button = error_message_box.button(QtWidgets.QMessageBox.Ok)
|
||||
ok_button.click()
|
||||
assert False, "Cyclical dependency detected while reparenting prefab"
|
||||
except pyside_utils.EventLoopTimeoutException:
|
||||
pass
|
||||
|
||||
original_parent_after_reparent_children_ids = set(original_parent.get_children_ids())
|
||||
assert len(original_parent_after_reparent_children_ids) == len(original_parent_before_reparent_children_ids) - 1, \
|
||||
"The children count of the Prefab Instance's original parent should be decreased by 1."
|
||||
assert not container_entity_id_before_reparent in original_parent_after_reparent_children_ids, \
|
||||
"This Prefab Instance is still a child entity of its original parent entity."
|
||||
|
||||
new_parent_after_reparent_children_ids = set(new_parent.get_children_ids())
|
||||
assert len(new_parent_after_reparent_children_ids) == len(new_parent_before_reparent_children_ids) + 1, \
|
||||
"The children count of the Prefab Instance's new parent should be increased by 1."
|
||||
|
||||
container_entity_id_after_reparent = set(new_parent_after_reparent_children_ids).difference(new_parent_before_reparent_children_ids).pop()
|
||||
reparented_container_entity = EditorEntity(container_entity_id_after_reparent)
|
||||
reparented_container_entity_parent_id = reparented_container_entity.get_parent_id()
|
||||
has_correct_parent = reparented_container_entity_parent_id.ToString() == parent_entity_id.ToString()
|
||||
assert has_correct_parent, "Prefab Instance reparented is *not* under the expected parent entity"
|
||||
|
||||
self.container_entity = reparented_container_entity
|
||||
|
||||
# This is a helper class which contains some of the useful information about a prefab template.
|
||||
class Prefab:
|
||||
|
||||
existing_prefabs = {}
|
||||
|
||||
def __init__(self, file_path: str):
|
||||
self.file_path: str = get_prefab_file_path(file_path)
|
||||
self.instances: set[PrefabInstance] = set()
|
||||
|
||||
"""
|
||||
Check if a prefab is ready to be used to generate its instances.
|
||||
:param file_path: A unique file path of the target prefab.
|
||||
:return: Whether the target prefab is loaded or not.
|
||||
"""
|
||||
@classmethod
|
||||
def is_prefab_loaded(cls, file_path: str) -> bool:
|
||||
return file_path in Prefab.existing_prefabs
|
||||
|
||||
"""
|
||||
Check if a prefab exists in the directory for files of prefab tests.
|
||||
:param file_name: A unique file name of the target prefab.
|
||||
:return: Whether the target prefab exists or not.
|
||||
"""
|
||||
@classmethod
|
||||
def prefab_exists(cls, file_path: str) -> bool:
|
||||
return path.exists(get_prefab_file_path(file_path))
|
||||
|
||||
"""
|
||||
Return a prefab which can be used immediately.
|
||||
:param file_name: A unique file name of the target prefab.
|
||||
:return: The prefab with given file name.
|
||||
"""
|
||||
@classmethod
|
||||
def get_prefab(cls, file_name: str) -> Prefab:
|
||||
assert file_name, "Received an empty file_name"
|
||||
if Prefab.is_prefab_loaded(file_name):
|
||||
return Prefab.existing_prefabs[file_name]
|
||||
else:
|
||||
assert Prefab.prefab_exists(file_name), f"Attempted to get a prefab \"{file_name}\" that doesn't exist"
|
||||
new_prefab = Prefab(file_name)
|
||||
Prefab.existing_prefabs[file_name] = Prefab(file_name)
|
||||
return new_prefab
|
||||
|
||||
"""
|
||||
Create a prefab in memory and return it. The very first instance of this prefab will also be created.
|
||||
:param entities: The entities that should form the new prefab (along with their descendants).
|
||||
:param file_name: A unique file name of new prefab.
|
||||
:param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name.
|
||||
:return: Created Prefab object and the very first PrefabInstance object owned by the prefab.
|
||||
"""
|
||||
@classmethod
|
||||
def create_prefab(cls, entities: list[EditorEntity], file_name: str, prefab_instance_name: str=None) -> tuple(Prefab, PrefabInstance):
|
||||
assert not Prefab.is_prefab_loaded(file_name), f"Can't create Prefab '{file_name}' since the prefab already exists"
|
||||
|
||||
new_prefab = Prefab(file_name)
|
||||
entity_ids = [entity.id for entity in entities]
|
||||
create_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'CreatePrefabInMemory', entity_ids, new_prefab.file_path)
|
||||
assert create_prefab_result.IsSuccess(), f"Prefab operation 'CreatePrefab' failed. Error: {create_prefab_result.GetError()}"
|
||||
|
||||
container_entity_id = create_prefab_result.GetValue()
|
||||
container_entity = EditorEntity(container_entity_id)
|
||||
|
||||
if prefab_instance_name:
|
||||
container_entity.set_name(prefab_instance_name)
|
||||
|
||||
wait_for_propagation()
|
||||
|
||||
new_prefab_instance = PrefabInstance(file_name, EditorEntity(container_entity_id))
|
||||
new_prefab.instances.add(new_prefab_instance)
|
||||
Prefab.existing_prefabs[file_name] = new_prefab
|
||||
return new_prefab, new_prefab_instance
|
||||
|
||||
"""
|
||||
Remove target prefab instances.
|
||||
:param prefab_instances: Instances to be removed.
|
||||
"""
|
||||
@classmethod
|
||||
def remove_prefabs(cls, prefab_instances: list[PrefabInstance]):
|
||||
entity_ids_to_remove = []
|
||||
entity_id_queue = [prefab_instance.container_entity for prefab_instance in prefab_instances]
|
||||
while entity_id_queue:
|
||||
entity = entity_id_queue.pop(0)
|
||||
children_entity_ids = entity.get_children_ids()
|
||||
for child_entity_id in children_entity_ids:
|
||||
entity_id_queue.append(EditorEntity(child_entity_id))
|
||||
|
||||
entity_ids_to_remove.append(entity.id)
|
||||
|
||||
container_entity_ids = [prefab_instance.container_entity.id for prefab_instance in prefab_instances]
|
||||
delete_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DeleteEntitiesAndAllDescendantsInInstance', container_entity_ids)
|
||||
assert delete_prefab_result.IsSuccess(), f"Prefab operation 'DeleteEntitiesAndAllDescendantsInInstance' failed. Error: {delete_prefab_result.GetError()}"
|
||||
|
||||
wait_for_propagation()
|
||||
|
||||
entity_ids_after_delete = set(get_all_entity_ids())
|
||||
|
||||
for entity_id_removed in entity_ids_to_remove:
|
||||
if entity_id_removed in entity_ids_after_delete:
|
||||
assert False, "Not all entities and descendants in target prefabs are deleted."
|
||||
|
||||
for instance in prefab_instances:
|
||||
instance_deleted_prefab = Prefab.get_prefab(instance.prefab_file_name)
|
||||
instance_deleted_prefab.instances.remove(instance)
|
||||
instance = PrefabInstance()
|
||||
|
||||
"""
|
||||
Instantiate an instance of this prefab.
|
||||
:param parent_entity: The entity the prefab should be a child of in the transform hierarchy.
|
||||
:param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name.
|
||||
:param prefab_position: The position in world space the prefab should be instantiated in.
|
||||
:return: Instantiated PrefabInstance object owned by this prefab.
|
||||
"""
|
||||
def instantiate(self, parent_entity: EditorEntity=None, name: str=None, prefab_position: Vector3=Vector3()) -> PrefabInstance:
|
||||
parent_entity_id = parent_entity.id if parent_entity is not None else EntityId()
|
||||
|
||||
instantiate_prefab_result = prefab.PrefabPublicRequestBus(
|
||||
bus.Broadcast, 'InstantiatePrefab', self.file_path, parent_entity_id, prefab_position)
|
||||
|
||||
assert instantiate_prefab_result.IsSuccess(), f"Prefab operation 'InstantiatePrefab' failed. Error: {instantiate_prefab_result.GetError()}"
|
||||
|
||||
container_entity_id = instantiate_prefab_result.GetValue()
|
||||
container_entity = EditorEntity(container_entity_id)
|
||||
|
||||
if name:
|
||||
container_entity.set_name(name)
|
||||
|
||||
wait_for_propagation()
|
||||
|
||||
new_prefab_instance = PrefabInstance(self.file_path, EditorEntity(container_entity_id))
|
||||
assert not new_prefab_instance in self.instances, "This prefab instance is already existed before this instantiation."
|
||||
self.instances.add(new_prefab_instance)
|
||||
|
||||
return new_prefab_instance
|
||||
Reference in New Issue
Block a user