You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
222 lines
11 KiB
Python
222 lines
11 KiB
Python
"""
|
|
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
|
|
|
|
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.bus as bus
|
|
import azlmbr.prefab as prefab
|
|
import editor_python_test_tools.pyside_utils as pyside_utils
|
|
import prefab.Prefab_Test_Utils as prefab_test_utils
|
|
|
|
# This is a helper class which contains some of the useful information about a prefab instance.
|
|
class PrefabInstance:
|
|
|
|
def __init__(self, name: str=None, prefab_file_name: str=None, container_entity: EditorEntity=EntityId()):
|
|
self.name = name
|
|
self.prefab_file_name: str = prefab_file_name
|
|
self.container_entity: EditorEntity = container_entity
|
|
|
|
"""
|
|
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() -> bool:
|
|
return self.container_entity.id.IsValid() and self.name is not None 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_name = self.container_entity.get_name()
|
|
current_children_entity_ids_having_prefab_name = prefab_test_utils.get_children_ids_by_name(parent_entity_id, container_entity_name)
|
|
Report.info(f'current_children_entity_ids_having_prefab_name: {current_children_entity_ids_having_prefab_name}')
|
|
|
|
pyside_utils.run_soon(lambda: self.container_entity.set_parent_entity(parent_entity_id))
|
|
pyside_utils.run_soon(lambda: prefab_test_utils.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
|
|
|
|
updated_children_entity_ids_having_prefab_name = prefab_test_utils.get_children_ids_by_name(parent_entity_id, container_entity_name)
|
|
Report.info(f'updated_children_entity_ids_having_prefab_name: {updated_children_entity_ids_having_prefab_name}')
|
|
new_child_with_reparented_prefab_name_added = len(updated_children_entity_ids_having_prefab_name) == len(current_children_entity_ids_having_prefab_name) + 1
|
|
assert new_child_with_reparented_prefab_name_added, "No entity with reparented prefab name become a child of target parent entity"
|
|
|
|
updated_container_entity_id = set(updated_children_entity_ids_having_prefab_name).difference(current_children_entity_ids_having_prefab_name).pop()
|
|
updated_container_entity = EditorEntity(updated_container_entity_id)
|
|
updated_container_entity_parent_id = updated_container_entity.get_parent_id()
|
|
has_correct_parent = updated_container_entity_parent_id.ToString() == parent_entity_id.ToString()
|
|
assert has_correct_parent, "Prefab reparented is *not* under the expected parent entity"
|
|
|
|
self.container_entity = EditorEntity(updated_container_entity_id)
|
|
|
|
# This is a helper class which contains some of the useful information about a prefab template.
|
|
class Prefab:
|
|
|
|
existing_prefabs = {}
|
|
|
|
def __init__(self, file_name: str):
|
|
self.file_name:str = file_name
|
|
self.file_path: str = prefab_test_utils.get_prefab_file_path(file_name)
|
|
self.instances: dict = {}
|
|
|
|
"""
|
|
Check if a prefab is ready to be used to generate its instances.
|
|
:param file_name: A unique file name of the target prefab.
|
|
:return: Whether the target prefab is loaded or not.
|
|
"""
|
|
@classmethod
|
|
def is_prefab_loaded(cls, file_name: str) -> bool:
|
|
return file_name 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_name: str) -> bool:
|
|
file_path = prefab_test_utils.get_prefab_file_path(file_name)
|
|
return path.exists(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:
|
|
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: An outcome object with an entityId of the new prefab's container entity; on failure, it comes with an error message detailing the cause of the error.
|
|
"""
|
|
@classmethod
|
|
def create_prefab(cls, entities: list[EditorEntity], file_name: str, prefab_instance_name: str=None) -> Prefab:
|
|
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 = EditorEntity(create_prefab_result.GetValue())
|
|
|
|
if prefab_instance_name:
|
|
container_entity.set_name(prefab_instance_name)
|
|
else:
|
|
prefab_instance_name = file_name
|
|
|
|
prefab_test_utils.wait_for_propagation()
|
|
container_entity_id = prefab_test_utils.find_entity_by_unique_name(prefab_instance_name)
|
|
new_prefab.instances[prefab_instance_name] = PrefabInstance(prefab_instance_name, file_name, EditorEntity(container_entity_id))
|
|
Prefab.existing_prefabs[file_name] = new_prefab
|
|
return new_prefab
|
|
|
|
"""
|
|
Remove target prefab instances.
|
|
:param prefab_instances: Instances to be removed.
|
|
"""
|
|
@classmethod
|
|
def remove_prefabs(cls, prefab_instances: list[PrefabInstance]):
|
|
instances_to_remove_name_counts = Counter()
|
|
instances_removed_expected_name_counts = Counter()
|
|
|
|
entities_to_remove = [prefab_instance.container_entity for prefab_instance in prefab_instances]
|
|
while entities_to_remove:
|
|
entity = entities_to_remove.pop(-1)
|
|
entity_name = entity.get_name()
|
|
instances_to_remove_name_counts[entity_name] += 1
|
|
|
|
children_entity_ids = entity.get_children_ids()
|
|
for child_entity_id in children_entity_ids:
|
|
entities_to_remove.append(EditorEntity(child_entity_id))
|
|
|
|
for entity_name, entity_count in instances_to_remove_name_counts.items():
|
|
entities = prefab_test_utils.find_entities_by_name(entity_name)
|
|
instances_removed_expected_name_counts[entity_name] = len(entities) - entity_count
|
|
|
|
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()}"
|
|
|
|
prefab_test_utils.wait_for_propagation()
|
|
|
|
prefab_entities_deleted = True
|
|
for entity_name, expected_entity_count in instances_removed_expected_name_counts.items():
|
|
actual_entity_count = len(prefab_test_utils.find_entities_by_name(entity_name))
|
|
if actual_entity_count is not expected_entity_count:
|
|
prefab_entities_deleted = False
|
|
break
|
|
|
|
assert prefab_entities_deleted, "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.pop(instance.name)
|
|
instance = PrefabInstance()
|
|
|
|
"""
|
|
Instantiate an instance of this prefab.
|
|
:param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name.
|
|
:param parent_entity: The entity the prefab should be a child of in the transform hierarchy.
|
|
:param prefab_position: The position in world space the prefab should be instantiated in.
|
|
:return: An outcome object with an entityId of the new prefab's container entity; on failure, it comes with an error message detailing the cause of the error.
|
|
"""
|
|
def instantiate(self, name: str=None, parent_entity: EditorEntity=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)
|
|
else:
|
|
name = self.file_name
|
|
|
|
prefab_test_utils.wait_for_propagation()
|
|
container_entity_id = prefab_test_utils.find_entity_by_unique_name(name)
|
|
self.instances[name] = PrefabInstance(name, self.file_name, EditorEntity(container_entity_id))
|
|
|
|
prefab_test_utils.check_entity_at_position(container_entity_id, prefab_position)
|
|
|
|
return container_entity_id
|