//! 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::(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::(descendant) .map(|child_of| child_of.parent()); while let Some(entity) = current { if entity == ancestor { return true; } current = world .get::(entity) .map(|child_of| child_of.parent()); } false } pub fn would_create_cycle(world: &World, targets: &[Entity], new_parent: Option) -> 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 { let mut current = Some(entity); while let Some(candidate) = current { if world.get::(candidate).is_some() { return Some(candidate); } current = world .get::(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, ) -> Option { 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::(parent)) .map(|owner| HierarchyDropViolation::PrefabBoundary { root: owner.instance_root, }) } pub fn level_object_roots(world: &mut World) -> Vec { let mut query = world.query_filtered::<(Entity, Option<&ChildOf>), With>(); 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 { world .get::(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 { authored_children(world, parent) .into_iter() .filter(|entity| { world.get::(*entity).is_none() && world .get::(*entity) .is_none() }) .collect() } fn mutable_level_object_roots(world: &mut World) -> Vec { level_object_roots(world) .into_iter() .filter(|entity| { world.get::(*entity).is_none() && world .get::(*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::(entity).is_some() || world .get::(entity) .is_some() { 0 } else { 1 } } fn actor_kind_sort_key(world: &World, entity: Entity) -> u8 { world .get::(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::(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::(parent) .map(|child_of| child_of.parent()); } parts.reverse(); parts.join("/") } pub fn next_sibling_index(world: &mut World, parent: Option) -> 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, 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::(entity).copied(); SiblingChange { entity, old_parent: world .get::(entity) .map(|child_of| child_of.parent()), new_parent: world .get::(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::(); } 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::(); } 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, insert_index: i32, ) -> Vec { let mut changes = Vec::new(); let old_parents: std::collections::HashSet> = moving .iter() .map(|entity| { world .get::(*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, insert_index: i32, ) -> Vec { 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, exclude: &[Entity], ) -> Vec { 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, siblings: &[Entity], ) -> Vec { let mut changes = Vec::new(); for (index, entity) in siblings.iter().enumerate() { let old_parent = world .get::(*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::(*entity).copied(); let new_transform = if old_parent != parent { world .get::(*entity) .map(|global| { match parent.and_then(|parent| world.get::(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::(); } 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::, Without)>(); let entities: Vec = 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::(*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::(*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::(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::(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::(entity) { if format!("{kind:?}").to_lowercase().contains(filter_lower) { return true; } } let tags = [ world.get::(entity).is_some().then_some("light"), world .get::(entity) .is_some() .then_some("spawn"), world.get::(entity).is_some().then_some("model"), world .get::(entity) .is_some() .then_some("physics"), world .get::(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::(entity) .copied() .unwrap_or_default() } pub fn is_entity_locked(locked: &std::collections::HashSet, 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::(child).unwrap().parent(), parent); assert_eq!( world.get::(child).unwrap().translation, Vec3::new(2.0, 1.0, -3.0) ); apply_sibling_change(&mut world, child_change); assert!(world.get::(child).is_none()); assert_eq!( world.get::(child).unwrap().translation, Vec3::new(12.0, 1.0, -3.0) ); apply_sibling_change_new(&mut world, child_change); assert_eq!(world.get::(child).unwrap().parent(), parent); assert_eq!( world.get::(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); } }