Skip to main content

kvas_engine/
hierarchy.rs

1//! Parent/child relations and world-space transforms.
2//!
3//! [`set_parent`] preserves the world pose; [`set_parent_local`] preserves the
4//! local transform. Both reject dead handles, cycles and invalid parent chains.
5//! Matrices use column vectors, +Y up and local -Z forward. Transform rotations
6//! are stored as Euler degrees. Directly inserting [`Parent`] bypasses validation;
7//! traversal still terminates on malformed cycles.
8
9use crate::components::*;
10use glam::Mat4;
11use kvas_ecs::{Entity, World};
12use std::collections::{HashMap, HashSet};
13
14/// Spawns an entity with [`Name`] and identity [`Transform`].
15/// Built-in component storages must already be registered by the host.
16pub fn spawn_named(world: &mut World, name: &str) -> Entity {
17    let e = world.spawn();
18    world.insert(e, Name(name.to_string()));
19    world.insert(e, Transform::default());
20    e
21}
22
23/// Entity name, or an empty string for stale or unnamed entities.
24pub fn name_of(world: &World, e: Entity) -> &str {
25    world.get::<Name>(e).map_or("", |n| n.0.as_str())
26}
27
28/// First exact name match in component storage order. Names need not be unique.
29pub fn find_by_name(world: &World, name: &str) -> Option<Entity> {
30    world.iter::<Name>().find(|(_, n)| n.0 == name).map(|(e, _)| e)
31}
32
33/// Living parent, or `None` for roots and dangling links.
34pub fn parent_of(world: &World, e: Entity) -> Option<Entity> {
35    world.get::<Parent>(e).map(|p| p.0).filter(|&p| world.is_alive(p))
36}
37
38/// Children in index order, including transient ones created by systems.
39pub fn children_all(world: &World, e: Entity) -> Vec<Entity> {
40    if !world.is_alive(e) {
41        return Vec::new();
42    }
43    let mut out: Vec<Entity> = world.iter::<Parent>().filter(|(_, p)| p.0 == e).map(|(c, _)| c).collect();
44    out.sort();
45    out
46}
47
48/// Children in index order, without transient system-made entities.
49pub fn children(world: &World, e: Entity) -> Vec<Entity> {
50    children_all(world, e).into_iter().filter(|&c| !world.has::<Transient>(c)).collect()
51}
52
53/// Non-transient entities without a living parent, in index order.
54pub fn roots(world: &World) -> Vec<Entity> {
55    world.iter_entities().filter(|&e| parent_of(world, e).is_none() && !world.has::<Transient>(e)).collect()
56}
57
58/// Whether `ancestor` occurs strictly above `of`. Traversal is cycle-safe.
59pub fn is_ancestor(world: &World, ancestor: Entity, of: Entity) -> bool {
60    if ancestor == of || !world.is_alive(ancestor) {
61        return false;
62    }
63    let mut seen = HashSet::from([of]);
64    let mut cur = parent_of(world, of);
65    loop {
66        match cur {
67            Some(p) if p == ancestor => return true,
68            Some(p) if seen.insert(p) => cur = parent_of(world, p),
69            Some(_) => return false,
70            None => return false,
71        }
72    }
73}
74
75/// Number of distinct living ancestors, excluding the entity itself.
76pub fn ancestors_count(world: &World, e: Entity) -> usize {
77    let mut n = 0;
78    let mut seen = HashSet::from([e]);
79    let mut cur = parent_of(world, e);
80    while let Some(p) = cur {
81        if !seen.insert(p) {
82            break;
83        }
84        n += 1;
85        cur = parent_of(world, p);
86    }
87    n
88}
89
90/// All descendants, including transient entities, each returned once.
91/// Children are visited deterministically; the root is excluded even on cycles.
92/// The parent map is built once, avoiding a world scan per visited node.
93pub fn descendants(world: &World, e: Entity) -> Vec<Entity> {
94    if !world.is_alive(e) {
95        return Vec::new();
96    }
97    let mut links: HashMap<Entity, Vec<Entity>> = HashMap::new();
98    for (child, p) in world.iter::<Parent>() {
99        links.entry(p.0).or_default().push(child);
100    }
101    for children in links.values_mut() {
102        children.sort();
103    }
104    let mut out = Vec::new();
105    let mut seen = HashSet::from([e]);
106    let mut stack = vec![e];
107    while let Some(cur) = stack.pop() {
108        for &c in links.get(&cur).into_iter().flatten() {
109            if seen.insert(c) {
110                out.push(c);
111                stack.push(c);
112            }
113        }
114    }
115    out
116}
117
118/// Destroys a subtree, including transient system entities. Stale roots are ignored.
119pub fn despawn_recursive(world: &mut World, e: Entity) {
120    for d in descendants(world, e) {
121        world.despawn(d);
122    }
123    world.despawn(e);
124}
125
126/// Computes the current world matrix directly from local transforms.
127/// Missing transforms contribute identity; stale handles return identity.
128/// Malformed cycles are truncated before revisiting an entity.
129pub fn global_matrix(world: &World, e: Entity) -> Mat4 {
130    let mut m = Mat4::IDENTITY;
131    let mut seen = HashSet::new();
132    let mut cur = Some(e);
133    while let Some(c) = cur {
134        if !world.is_alive(c) || !seen.insert(c) {
135            break;
136        }
137        m = world.get::<Transform>(c).map_or(Mat4::IDENTITY, Transform::matrix) * m;
138        cur = parent_of(world, c);
139    }
140    m
141}
142
143fn valid_parent(world: &World, child: Entity, parent: Option<Entity>) -> bool {
144    if !world.is_alive(child) {
145        return false;
146    }
147    let mut seen = HashSet::from([child]);
148    let mut cur = parent;
149    while let Some(e) = cur {
150        if !world.is_alive(e) || !seen.insert(e) {
151            return false;
152        }
153        cur = world.get::<Parent>(e).map(|p| p.0);
154    }
155    true
156}
157
158/// Reparents while keeping the world pose when it is representable as TRS.
159/// Returns `false` without mutation for dead handles, cycles, non-finite matrices
160/// or a singular parent. Shear introduced by a rotated non-uniformly scaled parent
161/// is rejected because [`Transform`] cannot represent it exactly.
162pub fn set_parent(world: &mut World, child: Entity, parent: Option<Entity>) -> bool {
163    if !valid_parent(world, child, parent) {
164        return false;
165    }
166    let world_m = global_matrix(world, child);
167    let parent_m = parent.map_or(Mat4::IDENTITY, |p| global_matrix(world, p));
168    if !world_m.is_finite() || !parent_m.is_finite() || parent_m.determinant() == 0.0 {
169        return false;
170    }
171    let local = parent_m.inverse() * world_m;
172    // Zero-scale local transforms cannot be decomposed into a finite rotation.
173    if !local.is_finite() || local.determinant() == 0.0 {
174        return false;
175    }
176    let Some(transform) = Transform::try_from_matrix(local) else {
177        return false;
178    };
179    set_parent_local(world, child, parent);
180    if let Some(t) = world.get_mut::<Transform>(child) {
181        *t = transform;
182    }
183    true
184}
185
186/// Reparents without changing the local transform. Accepts zero-scale parents.
187/// Returns `false` without mutation for stale handles or invalid/cyclic chains.
188pub fn set_parent_local(world: &mut World, child: Entity, parent: Option<Entity>) -> bool {
189    if !valid_parent(world, child, parent) {
190        return false;
191    }
192    match parent {
193        Some(p) => world.insert(child, Parent(p)),
194        None => {
195            world.remove::<Parent>(child);
196        }
197    }
198    true
199}
200
201/// Returns every exact name match in entity index order.
202pub fn find_all_by_name(world: &World, name: &str) -> Vec<Entity> {
203    let mut matches: Vec<_> = world.iter::<Name>().filter(|(_, n)| n.0 == name).map(|(e, _)| e).collect();
204    matches.sort();
205    matches
206}
207
208/// Finds a named entity inside a subtree, optionally including the root.
209pub fn find_descendant(world: &World, root: Entity, name: &str, include_root: bool) -> Option<Entity> {
210    if include_root && world.is_alive(root) && name_of(world, root) == name {
211        return Some(root);
212    }
213    descendants(world, root).into_iter().find(|&e| name_of(world, e) == name)
214}
215
216/// Effective mesh visibility inherited through the living parent chain.
217/// Missing visibility components default to visible; stale entities return false.
218/// Cycles are visited once, so directly inserted malformed links cannot hang rendering.
219pub fn is_visible(world: &World, entity: Entity) -> bool {
220    if !world.is_alive(entity) {
221        return false;
222    }
223    let mut current = Some(entity);
224    let mut seen = HashSet::new();
225    while let Some(e) = current {
226        if !seen.insert(e) {
227            break;
228        }
229        if world.get::<Visibility>(e).is_some_and(|v| !v.visible) {
230            return false;
231        }
232        current = parent_of(world, e);
233    }
234    true
235}
236
237/// Changes only local translation to place an entity at a world-space point.
238/// Rejects stale/missing transforms and singular/non-finite parent matrices.
239/// Dynamic bodies also need their `RigidBody.teleport` request set before the
240/// next physics step; [`crate::script::Ctx::teleport_of`] handles this for scripts.
241pub fn set_world_position(world: &mut World, entity: Entity, position: glam::Vec3) -> bool {
242    if !position.is_finite() || !world.has::<Transform>(entity) {
243        return false;
244    }
245    let parent = parent_of(world, entity).map_or(Mat4::IDENTITY, |parent| global_matrix(world, parent));
246    if !parent.is_finite() || parent.determinant() == 0.0 {
247        return false;
248    }
249    let local = parent.inverse().transform_point3(position);
250    if !local.is_finite() {
251        return false;
252    }
253    world.get_mut::<Transform>(entity).unwrap().position = local;
254    true
255}
256
257/// Refreshes [`GlobalTransform`] for every entity that has a [`Transform`].
258/// Healthy parent chains are resolved once and cached for this call, making a
259/// complete refresh linear in the entity count even for a long chain. Directly
260/// inserted cyclic links fall back to bounded per-entity evaluation.
261pub fn update_globals(world: &mut World) {
262    let entities = world.entities();
263    let mut resolved: HashMap<Entity, Mat4> = HashMap::with_capacity(entities.len());
264    let mut path = Vec::new();
265    let mut seen = HashSet::new();
266    for &entity in &entities {
267        if resolved.contains_key(&entity) {
268            continue;
269        }
270        path.clear();
271        seen.clear();
272        let mut current = Some(entity);
273        let mut base = Mat4::IDENTITY;
274        let mut cycle = false;
275        while let Some(e) = current {
276            if let Some(&matrix) = resolved.get(&e) {
277                base = matrix;
278                break;
279            }
280            if !seen.insert(e) {
281                cycle = true;
282                break;
283            }
284            path.push(e);
285            current = parent_of(world, e);
286        }
287        if cycle {
288            for &e in &path {
289                resolved.insert(e, global_matrix(world, e));
290            }
291        } else {
292            for &e in path.iter().rev() {
293                base *= world.get::<Transform>(e).map_or(Mat4::IDENTITY, Transform::matrix);
294                resolved.insert(e, base);
295            }
296        }
297    }
298    for entity in entities {
299        if world.has::<Transform>(entity) {
300            world.insert(entity, GlobalTransform(resolved[&entity]));
301        }
302    }
303}