676 lines
22 KiB
Rust
676 lines
22 KiB
Rust
//! Hierarchy tree building, sibling ordering, and reparent helpers.
|
|
|
|
use bevy::prelude::*;
|
|
use shared::{
|
|
ActorKind, AuthoringLightKind, EditorVisibility, HierarchySiblingIndex, HydratedPrefabMember,
|
|
LevelObject, LightDesc, ModelRef, PhysicsBody, PlayerSpawn, PrefabInstance,
|
|
};
|
|
|
|
use super::helpers::{entity_name, is_level_object, HierarchyNodeKind};
|
|
use super::hierarchy_state::HierarchySort;
|
|
use crate::history::SiblingChange;
|
|
|
|
pub fn sibling_index(world: &World, entity: Entity) -> i32 {
|
|
world
|
|
.get::<HierarchySiblingIndex>(entity)
|
|
.map(|index| index.0)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
pub fn is_ancestor(world: &World, ancestor: Entity, descendant: Entity) -> bool {
|
|
if ancestor == descendant {
|
|
return true;
|
|
}
|
|
let mut current = world
|
|
.get::<ChildOf>(descendant)
|
|
.map(|child_of| child_of.parent());
|
|
while let Some(entity) = current {
|
|
if entity == ancestor {
|
|
return true;
|
|
}
|
|
current = world
|
|
.get::<ChildOf>(entity)
|
|
.map(|child_of| child_of.parent());
|
|
}
|
|
false
|
|
}
|
|
|
|
pub fn would_create_cycle(world: &World, targets: &[Entity], new_parent: Option<Entity>) -> bool {
|
|
let Some(parent) = new_parent else {
|
|
return false;
|
|
};
|
|
targets
|
|
.iter()
|
|
.any(|entity| is_ancestor(world, *entity, parent))
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum HierarchyDropViolation {
|
|
Cycle,
|
|
PrefabBoundary { root: Entity },
|
|
}
|
|
|
|
/// Returns the authored prefab-instance root containing `entity`, including the root itself.
|
|
pub fn prefab_instance_boundary(world: &World, entity: Entity) -> Option<Entity> {
|
|
let mut current = Some(entity);
|
|
while let Some(candidate) = current {
|
|
if world.get::<PrefabInstance>(candidate).is_some() {
|
|
return Some(candidate);
|
|
}
|
|
current = world
|
|
.get::<ChildOf>(candidate)
|
|
.map(|child_of| child_of.parent())
|
|
.filter(|parent| is_level_object(world, *parent));
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Authored local children may be attached to instances; hydrated source members remain read-only.
|
|
pub fn hierarchy_drop_violation(
|
|
world: &World,
|
|
targets: &[Entity],
|
|
new_parent: Option<Entity>,
|
|
) -> Option<HierarchyDropViolation> {
|
|
if would_create_cycle(world, targets, new_parent) {
|
|
return Some(HierarchyDropViolation::Cycle);
|
|
}
|
|
let prefab_targets: Vec<_> = targets
|
|
.iter()
|
|
.filter_map(|target| {
|
|
crate::assets::prefab_overrides::prefab_member_override_target(world, *target)
|
|
})
|
|
.collect();
|
|
if !prefab_targets.is_empty() {
|
|
let (root, path) = &prefab_targets[0];
|
|
let compatible_targets = prefab_targets.len() == targets.len()
|
|
&& prefab_targets.iter().all(|(candidate_root, candidate)| {
|
|
candidate_root == root && candidate.instance_chain == path.instance_chain
|
|
});
|
|
let compatible_parent = new_parent.is_none_or(|parent| {
|
|
crate::assets::prefab_overrides::prefab_member_override_target(world, parent)
|
|
.is_some_and(|(parent_root, parent_path)| {
|
|
parent_root == *root && parent_path.instance_chain == path.instance_chain
|
|
})
|
|
});
|
|
if compatible_targets && compatible_parent {
|
|
return None;
|
|
}
|
|
return Some(HierarchyDropViolation::PrefabBoundary { root: *root });
|
|
}
|
|
new_parent
|
|
.and_then(|parent| world.get::<HydratedPrefabMember>(parent))
|
|
.map(|owner| HierarchyDropViolation::PrefabBoundary {
|
|
root: owner.instance_root,
|
|
})
|
|
}
|
|
|
|
pub fn level_object_roots(world: &mut World) -> Vec<Entity> {
|
|
let mut query = world.query_filtered::<(Entity, Option<&ChildOf>), With<LevelObject>>();
|
|
query
|
|
.iter(world)
|
|
.filter_map(|(entity, parent)| match parent {
|
|
Some(parent) if is_level_object(world, parent.parent()) => None,
|
|
_ => Some(entity),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn authored_children(world: &World, parent: Entity) -> Vec<Entity> {
|
|
world
|
|
.get::<Children>(parent)
|
|
.map(|children| {
|
|
children
|
|
.iter()
|
|
.filter(|child| is_level_object(world, *child))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn mutable_authored_children(world: &World, parent: Entity) -> Vec<Entity> {
|
|
authored_children(world, parent)
|
|
.into_iter()
|
|
.filter(|entity| {
|
|
world.get::<HydratedPrefabMember>(*entity).is_none()
|
|
&& world
|
|
.get::<crate::scene_io::ComposedSceneMember>(*entity)
|
|
.is_none()
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn mutable_level_object_roots(world: &mut World) -> Vec<Entity> {
|
|
level_object_roots(world)
|
|
.into_iter()
|
|
.filter(|entity| {
|
|
world.get::<HydratedPrefabMember>(*entity).is_none()
|
|
&& world
|
|
.get::<crate::scene_io::ComposedSceneMember>(*entity)
|
|
.is_none()
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn entity_tie_break(a: Entity, b: Entity) -> std::cmp::Ordering {
|
|
a.to_bits().cmp(&b.to_bits())
|
|
}
|
|
|
|
pub fn sort_siblings(world: &World, entities: &mut [Entity], sort_mode: HierarchySort) {
|
|
match sort_mode {
|
|
HierarchySort::Manual => {
|
|
entities.sort_by(|a, b| {
|
|
linked_order_class(world, *a)
|
|
.cmp(&linked_order_class(world, *b))
|
|
.then_with(|| sibling_index(world, *a).cmp(&sibling_index(world, *b)))
|
|
.then_with(|| entity_tie_break(*a, *b))
|
|
});
|
|
}
|
|
HierarchySort::Name => {
|
|
entities.sort_by(|a, b| {
|
|
entity_name(world, *a)
|
|
.cmp(&entity_name(world, *b))
|
|
.then_with(|| sibling_index(world, *a).cmp(&sibling_index(world, *b)))
|
|
.then_with(|| entity_tie_break(*a, *b))
|
|
});
|
|
}
|
|
HierarchySort::Type => {
|
|
entities.sort_by(|a, b| {
|
|
actor_kind_sort_key(world, *a)
|
|
.cmp(&actor_kind_sort_key(world, *b))
|
|
.then_with(|| entity_name(world, *a).cmp(&entity_name(world, *b)))
|
|
.then_with(|| sibling_index(world, *a).cmp(&sibling_index(world, *b)))
|
|
.then_with(|| entity_tie_break(*a, *b))
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn linked_order_class(world: &World, entity: Entity) -> u8 {
|
|
if world.get::<HydratedPrefabMember>(entity).is_some()
|
|
|| world
|
|
.get::<crate::scene_io::ComposedSceneMember>(entity)
|
|
.is_some()
|
|
{
|
|
0
|
|
} else {
|
|
1
|
|
}
|
|
}
|
|
|
|
fn actor_kind_sort_key(world: &World, entity: Entity) -> u8 {
|
|
world
|
|
.get::<ActorKind>(entity)
|
|
.copied()
|
|
.map(|kind| match kind {
|
|
ActorKind::Empty => 0,
|
|
ActorKind::Brush | ActorKind::StaticMesh | ActorKind::ImportedModel => 1,
|
|
ActorKind::Light => 2,
|
|
ActorKind::AudioSource | ActorKind::AudioListener => 3,
|
|
ActorKind::PrefabAnchor => 3,
|
|
ActorKind::PlayerSpawn => 4,
|
|
ActorKind::WeaponSpawn => 5,
|
|
ActorKind::TriggerVolume => 6,
|
|
ActorKind::PostProcessVolume => 6,
|
|
ActorKind::TeamSpawn => 7,
|
|
ActorKind::Objective => 8,
|
|
})
|
|
.unwrap_or(9)
|
|
}
|
|
|
|
pub fn entity_hierarchy_path(world: &World, entity: Entity) -> String {
|
|
let mut parts = vec![entity_name(world, entity)];
|
|
let mut current = world
|
|
.get::<ChildOf>(entity)
|
|
.map(|child_of| child_of.parent());
|
|
while let Some(parent) = current {
|
|
if !is_level_object(world, parent) {
|
|
break;
|
|
}
|
|
parts.push(entity_name(world, parent));
|
|
current = world
|
|
.get::<ChildOf>(parent)
|
|
.map(|child_of| child_of.parent());
|
|
}
|
|
parts.reverse();
|
|
parts.join("/")
|
|
}
|
|
|
|
pub fn next_sibling_index(world: &mut World, parent: Option<Entity>) -> i32 {
|
|
let siblings = match parent {
|
|
Some(parent) => authored_children(world, parent),
|
|
None => level_object_roots(world),
|
|
};
|
|
siblings
|
|
.iter()
|
|
.map(|entity| sibling_index(world, *entity))
|
|
.max()
|
|
.map(|max| max + 1)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
pub fn renumber_siblings(world: &mut World, parent: Option<Entity>, ordered: &[Entity]) {
|
|
for (index, entity) in ordered.iter().enumerate() {
|
|
if let Ok(mut entity_mut) = world.get_entity_mut(*entity) {
|
|
entity_mut.insert(HierarchySiblingIndex(index as i32));
|
|
}
|
|
}
|
|
let _ = parent;
|
|
}
|
|
|
|
pub fn capture_sibling_state(world: &World, entity: Entity) -> SiblingChange {
|
|
let transform = world.get::<Transform>(entity).copied();
|
|
SiblingChange {
|
|
entity,
|
|
old_parent: world
|
|
.get::<ChildOf>(entity)
|
|
.map(|child_of| child_of.parent()),
|
|
new_parent: world
|
|
.get::<ChildOf>(entity)
|
|
.map(|child_of| child_of.parent()),
|
|
old_index: sibling_index(world, entity),
|
|
new_index: sibling_index(world, entity),
|
|
old_transform: transform,
|
|
new_transform: transform,
|
|
}
|
|
}
|
|
|
|
pub fn apply_sibling_change(world: &mut World, change: &SiblingChange) {
|
|
if let Ok(mut entity_mut) = world.get_entity_mut(change.entity) {
|
|
if let Some(parent) = change.old_parent {
|
|
entity_mut.insert(ChildOf(parent));
|
|
} else {
|
|
entity_mut.remove::<ChildOf>();
|
|
}
|
|
entity_mut.insert(HierarchySiblingIndex(change.old_index));
|
|
if let Some(transform) = change.old_transform {
|
|
entity_mut.insert(transform);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn apply_sibling_change_new(world: &mut World, change: &SiblingChange) {
|
|
if let Ok(mut entity_mut) = world.get_entity_mut(change.entity) {
|
|
if let Some(parent) = change.new_parent {
|
|
entity_mut.insert(ChildOf(parent));
|
|
} else {
|
|
entity_mut.remove::<ChildOf>();
|
|
}
|
|
entity_mut.insert(HierarchySiblingIndex(change.new_index));
|
|
if let Some(transform) = change.new_transform {
|
|
entity_mut.insert(transform);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn reorder_entities_under_parent(
|
|
world: &mut World,
|
|
moving: &[Entity],
|
|
new_parent: Option<Entity>,
|
|
insert_index: i32,
|
|
) -> Vec<SiblingChange> {
|
|
let mut changes = Vec::new();
|
|
let old_parents: std::collections::HashSet<Option<Entity>> = moving
|
|
.iter()
|
|
.map(|entity| {
|
|
world
|
|
.get::<ChildOf>(*entity)
|
|
.map(|child_of| child_of.parent())
|
|
})
|
|
.collect();
|
|
|
|
changes.extend(renumber_inserted_siblings(
|
|
world,
|
|
moving,
|
|
new_parent,
|
|
insert_index,
|
|
));
|
|
|
|
for old_parent in old_parents {
|
|
if old_parent == new_parent {
|
|
continue;
|
|
}
|
|
changes.extend(renumber_existing_siblings(world, old_parent, moving));
|
|
}
|
|
|
|
changes
|
|
}
|
|
|
|
fn renumber_inserted_siblings(
|
|
world: &mut World,
|
|
moving: &[Entity],
|
|
new_parent: Option<Entity>,
|
|
insert_index: i32,
|
|
) -> Vec<SiblingChange> {
|
|
let mut siblings = match new_parent {
|
|
Some(parent) => mutable_authored_children(world, parent),
|
|
None => mutable_level_object_roots(world),
|
|
};
|
|
siblings.retain(|entity| !moving.contains(entity));
|
|
let insert = insert_index.clamp(0, siblings.len() as i32) as usize;
|
|
for (offset, entity) in moving.iter().enumerate() {
|
|
siblings.insert(insert + offset, *entity);
|
|
}
|
|
apply_sibling_order(world, new_parent, &siblings)
|
|
}
|
|
|
|
fn renumber_existing_siblings(
|
|
world: &mut World,
|
|
parent: Option<Entity>,
|
|
exclude: &[Entity],
|
|
) -> Vec<SiblingChange> {
|
|
let mut siblings = match parent {
|
|
Some(parent) => mutable_authored_children(world, parent),
|
|
None => mutable_level_object_roots(world),
|
|
};
|
|
siblings.retain(|entity| !exclude.contains(entity));
|
|
sort_siblings(world, &mut siblings, HierarchySort::Manual);
|
|
apply_sibling_order(world, parent, &siblings)
|
|
}
|
|
|
|
fn apply_sibling_order(
|
|
world: &mut World,
|
|
parent: Option<Entity>,
|
|
siblings: &[Entity],
|
|
) -> Vec<SiblingChange> {
|
|
let mut changes = Vec::new();
|
|
for (index, entity) in siblings.iter().enumerate() {
|
|
let old_parent = world
|
|
.get::<ChildOf>(*entity)
|
|
.map(|child_of| child_of.parent());
|
|
let old_index = sibling_index(world, *entity);
|
|
let new_index = index as i32;
|
|
let old_transform = world.get::<Transform>(*entity).copied();
|
|
let new_transform = if old_parent != parent {
|
|
world
|
|
.get::<GlobalTransform>(*entity)
|
|
.map(|global| {
|
|
match parent.and_then(|parent| world.get::<GlobalTransform>(parent)) {
|
|
Some(parent_global) => global.reparented_to(parent_global),
|
|
None => global.compute_transform(),
|
|
}
|
|
})
|
|
.or(old_transform)
|
|
} else {
|
|
old_transform
|
|
};
|
|
if old_parent != parent || old_index != new_index {
|
|
changes.push(SiblingChange {
|
|
entity: *entity,
|
|
old_parent,
|
|
new_parent: parent,
|
|
old_index,
|
|
new_index,
|
|
old_transform,
|
|
new_transform,
|
|
});
|
|
}
|
|
if let Ok(mut entity_mut) = world.get_entity_mut(*entity) {
|
|
if let Some(parent) = parent {
|
|
entity_mut.insert(ChildOf(parent));
|
|
} else {
|
|
entity_mut.remove::<ChildOf>();
|
|
}
|
|
entity_mut.insert(HierarchySiblingIndex(new_index));
|
|
if let Some(transform) = new_transform {
|
|
entity_mut.insert(transform);
|
|
}
|
|
}
|
|
}
|
|
changes
|
|
}
|
|
|
|
pub fn backfill_missing_editor_visibility(world: &mut World) {
|
|
let mut query =
|
|
world.query_filtered::<Entity, (With<LevelObject>, Without<EditorVisibility>)>();
|
|
let entities: Vec<Entity> = query.iter(world).collect();
|
|
for entity in entities {
|
|
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
|
entity_mut.insert(EditorVisibility::default());
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn backfill_missing_sibling_indices(world: &mut World) {
|
|
let mut roots = level_object_roots(world);
|
|
if roots
|
|
.iter()
|
|
.any(|entity| world.get::<HierarchySiblingIndex>(*entity).is_none())
|
|
{
|
|
sort_siblings(world, &mut roots, HierarchySort::Manual);
|
|
renumber_siblings(world, None, &roots);
|
|
}
|
|
for root in roots {
|
|
backfill_children(world, root);
|
|
}
|
|
}
|
|
|
|
fn backfill_children(world: &mut World, parent: Entity) {
|
|
let mut children = authored_children(world, parent);
|
|
if children
|
|
.iter()
|
|
.any(|entity| world.get::<HierarchySiblingIndex>(*entity).is_none())
|
|
{
|
|
sort_siblings(world, &mut children, HierarchySort::Manual);
|
|
renumber_siblings(world, Some(parent), &children);
|
|
}
|
|
for child in children {
|
|
backfill_children(world, child);
|
|
}
|
|
}
|
|
|
|
pub fn actor_kind_icon(kind: ActorKind) -> &'static str {
|
|
use egui_phosphor_icons::icons;
|
|
match kind {
|
|
ActorKind::Empty => icons::FOLDER.as_str(),
|
|
ActorKind::Brush => icons::CUBE.as_str(),
|
|
ActorKind::StaticMesh => icons::CUBE.as_str(),
|
|
ActorKind::ImportedModel => icons::CUBE_TRANSPARENT.as_str(),
|
|
ActorKind::Light => icons::LIGHTBULB.as_str(),
|
|
ActorKind::AudioSource => icons::SPEAKER_HIGH.as_str(),
|
|
ActorKind::AudioListener => icons::HEADPHONES.as_str(),
|
|
ActorKind::PrefabAnchor => icons::PACKAGE.as_str(),
|
|
ActorKind::PlayerSpawn => icons::USER_CIRCLE.as_str(),
|
|
ActorKind::WeaponSpawn => icons::CROSSHAIR.as_str(),
|
|
ActorKind::TriggerVolume => icons::BROADCAST.as_str(),
|
|
ActorKind::PostProcessVolume => icons::CAMERA.as_str(),
|
|
ActorKind::TeamSpawn => icons::FLAG.as_str(),
|
|
ActorKind::Objective => icons::TARGET.as_str(),
|
|
}
|
|
}
|
|
|
|
pub struct HierarchyLabel {
|
|
pub icon: &'static str,
|
|
pub text: String,
|
|
}
|
|
|
|
pub fn hierarchy_label_for_entity(
|
|
world: &World,
|
|
entity: Entity,
|
|
kind: HierarchyNodeKind,
|
|
) -> HierarchyLabel {
|
|
use egui_phosphor_icons::icons;
|
|
let icon = world
|
|
.get::<ActorKind>(entity)
|
|
.copied()
|
|
.map(actor_kind_icon)
|
|
.unwrap_or_else(|| match kind {
|
|
HierarchyNodeKind::Authored => icons::CUBE.as_str(),
|
|
HierarchyNodeKind::Generated => icons::STACK.as_str(),
|
|
HierarchyNodeKind::Runtime => icons::CPU.as_str(),
|
|
});
|
|
let mut text = entity_name(world, entity);
|
|
if let Some(light) = world.get::<LightDesc>(entity) {
|
|
if matches!(light.kind, AuthoringLightKind::Directional) {
|
|
text.push_str(" (sun)");
|
|
}
|
|
}
|
|
match kind {
|
|
HierarchyNodeKind::Authored => {}
|
|
HierarchyNodeKind::Generated => text.push_str(" (generated)"),
|
|
HierarchyNodeKind::Runtime => text.push_str(" (runtime)"),
|
|
}
|
|
HierarchyLabel { icon, text }
|
|
}
|
|
|
|
pub fn entity_matches_filter(world: &World, entity: Entity, filter_lower: &str) -> bool {
|
|
if filter_lower.is_empty() {
|
|
return true;
|
|
}
|
|
if entity_name(world, entity)
|
|
.to_lowercase()
|
|
.contains(filter_lower)
|
|
{
|
|
return true;
|
|
}
|
|
if let Some(kind) = world.get::<ActorKind>(entity) {
|
|
if format!("{kind:?}").to_lowercase().contains(filter_lower) {
|
|
return true;
|
|
}
|
|
}
|
|
let tags = [
|
|
world.get::<LightDesc>(entity).is_some().then_some("light"),
|
|
world
|
|
.get::<PlayerSpawn>(entity)
|
|
.is_some()
|
|
.then_some("spawn"),
|
|
world.get::<ModelRef>(entity).is_some().then_some("model"),
|
|
world
|
|
.get::<PhysicsBody>(entity)
|
|
.is_some()
|
|
.then_some("physics"),
|
|
world
|
|
.get::<shared::PostProcessVolumeDesc>(entity)
|
|
.is_some()
|
|
.then_some("postprocess volume"),
|
|
];
|
|
tags.into_iter()
|
|
.flatten()
|
|
.any(|tag| tag.contains(filter_lower))
|
|
}
|
|
|
|
pub fn editor_visibility(world: &World, entity: Entity) -> EditorVisibility {
|
|
world
|
|
.get::<EditorVisibility>(entity)
|
|
.copied()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn is_entity_locked(locked: &std::collections::HashSet<Entity>, entity: Entity) -> bool {
|
|
locked.contains(&entity)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use shared::ActorId;
|
|
|
|
#[test]
|
|
fn sort_by_name_is_stable_for_duplicate_names() {
|
|
let mut world = World::new();
|
|
let a = world.spawn((LevelObject, Name::new("Pillar"))).id();
|
|
let b = world.spawn((LevelObject, Name::new("Pillar"))).id();
|
|
let mut entities = vec![a, b];
|
|
|
|
sort_siblings(&world, &mut entities, HierarchySort::Name);
|
|
let first = entities.clone();
|
|
|
|
sort_siblings(&world, &mut entities, HierarchySort::Name);
|
|
assert_eq!(entities, first, "duplicate-name sort must be deterministic");
|
|
}
|
|
|
|
#[test]
|
|
fn reparent_preserves_world_transform_and_round_trips_history() {
|
|
let mut world = World::new();
|
|
let parent = world
|
|
.spawn((
|
|
LevelObject,
|
|
Transform::from_xyz(10.0, 0.0, 0.0),
|
|
GlobalTransform::from_xyz(10.0, 0.0, 0.0),
|
|
HierarchySiblingIndex(0),
|
|
))
|
|
.id();
|
|
let child = world
|
|
.spawn((
|
|
LevelObject,
|
|
Transform::from_xyz(12.0, 1.0, -3.0),
|
|
GlobalTransform::from_xyz(12.0, 1.0, -3.0),
|
|
HierarchySiblingIndex(1),
|
|
))
|
|
.id();
|
|
|
|
let changes = reorder_entities_under_parent(&mut world, &[child], Some(parent), 0);
|
|
let child_change = changes
|
|
.iter()
|
|
.find(|change| change.entity == child)
|
|
.expect("reparent should record the moved actor");
|
|
|
|
assert_eq!(world.get::<ChildOf>(child).unwrap().parent(), parent);
|
|
assert_eq!(
|
|
world.get::<Transform>(child).unwrap().translation,
|
|
Vec3::new(2.0, 1.0, -3.0)
|
|
);
|
|
|
|
apply_sibling_change(&mut world, child_change);
|
|
assert!(world.get::<ChildOf>(child).is_none());
|
|
assert_eq!(
|
|
world.get::<Transform>(child).unwrap().translation,
|
|
Vec3::new(12.0, 1.0, -3.0)
|
|
);
|
|
|
|
apply_sibling_change_new(&mut world, child_change);
|
|
assert_eq!(world.get::<ChildOf>(child).unwrap().parent(), parent);
|
|
assert_eq!(
|
|
world.get::<Transform>(child).unwrap().translation,
|
|
Vec3::new(2.0, 1.0, -3.0)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cycle_detection_rejects_descendant_parent() {
|
|
let mut world = World::new();
|
|
let parent = world.spawn(LevelObject).id();
|
|
let child = world.spawn((LevelObject, ChildOf(parent))).id();
|
|
|
|
assert!(would_create_cycle(&world, &[parent], Some(child)));
|
|
assert!(!would_create_cycle(&world, &[child], None));
|
|
}
|
|
|
|
#[test]
|
|
fn prefab_boundary_allows_local_and_same_layer_override_structure() {
|
|
let mut world = World::new();
|
|
let instance = world.spawn((LevelObject, PrefabInstance::default())).id();
|
|
let local_child = world.spawn((LevelObject, ChildOf(instance))).id();
|
|
let generated_child = world
|
|
.spawn((
|
|
LevelObject,
|
|
ActorId::new("generated-child"),
|
|
ChildOf(instance),
|
|
HydratedPrefabMember {
|
|
instance_root: instance,
|
|
},
|
|
))
|
|
.id();
|
|
let actor = world.spawn(LevelObject).id();
|
|
let nested_instance = world.spawn((LevelObject, PrefabInstance::default())).id();
|
|
|
|
assert_eq!(
|
|
hierarchy_drop_violation(&world, &[actor], Some(instance)),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
hierarchy_drop_violation(&world, &[nested_instance], Some(local_child)),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
hierarchy_drop_violation(&world, &[nested_instance], Some(generated_child)),
|
|
Some(HierarchyDropViolation::PrefabBoundary { root: instance })
|
|
);
|
|
assert_eq!(
|
|
hierarchy_drop_violation(&world, &[generated_child], None),
|
|
None
|
|
);
|
|
assert_eq!(hierarchy_drop_violation(&world, &[instance], None), None);
|
|
assert_eq!(hierarchy_drop_violation(&world, &[local_child], None), None);
|
|
}
|
|
}
|