Blacksite/crates/editor/src/ui/hierarchy_ops.rs
Rbanh 0553a85220
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
Build production-ready editor authoring workflows
2026-07-11 12:41:04 -04:00

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);
}
}