Skip to main content

nightshade_api/
bounds.rs

1//! World-space extents and framing. A tool needs to know how big a thing is to
2//! point a camera at it; these read the engine's bounding volumes and merge
3//! them into one sphere.
4
5use nightshade::prelude::*;
6use serde::{Deserialize, Serialize};
7
8/// A world-space bounding sphere: enough to frame a selection without carrying
9/// an oriented box around.
10#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
11pub struct Bounds {
12    pub center: Vec3,
13    pub radius: f32,
14}
15
16/// The entity's world-space bounds, its mesh bounding volume placed by its
17/// global transform. `None` when the entity has no bounding volume.
18pub fn bounds(world: &World, entity: Entity) -> Option<Bounds> {
19    let bounding_volume =
20        world.get::<nightshade::render::bounding_volume::BoundingVolume>(entity)?;
21    let global = world.get::<nightshade::ecs::transform::components::GlobalTransform>(entity)?;
22    let transformed = bounding_volume.transform(&global.0);
23    Some(Bounds {
24        center: transformed.obb.center,
25        radius: transformed.sphere_radius,
26    })
27}
28
29/// The bounds enclosing every listed entity, or `None` when none of them has a
30/// bounding volume.
31pub fn bounds_of(world: &World, entities: &[Entity]) -> Option<Bounds> {
32    let mut combined: Option<Bounds> = None;
33    for &entity in entities {
34        if let Some(next) = bounds(world, entity) {
35            combined = Some(match combined {
36                Some(current) => merge(current, next),
37                None => next,
38            });
39        }
40    }
41    combined
42}
43
44/// Points the active orbit camera at the listed entities, gliding its focus and
45/// distance so they fill the view. A no-op without an orbit camera or any
46/// bounded entity.
47pub fn frame_entities(world: &mut World, entities: &[Entity]) {
48    let Some(bounds) = bounds_of(world, entities) else {
49        return;
50    };
51    let Some(camera) = world
52        .res::<nightshade::ecs::camera::resources::ActiveCamera>()
53        .0
54    else {
55        return;
56    };
57    if let Some(orbit) =
58        world.get_mut::<nightshade::ecs::camera::components::PanOrbitCamera>(camera)
59    {
60        orbit.target_focus = bounds.center;
61        orbit.target_radius = (bounds.radius * 2.5).max(0.5);
62    }
63}
64
65fn merge(a: Bounds, b: Bounds) -> Bounds {
66    let offset = b.center - a.center;
67    let distance = offset.magnitude();
68    if distance + b.radius <= a.radius {
69        return a;
70    }
71    if distance + a.radius <= b.radius {
72        return b;
73    }
74    if distance < f32::EPSILON {
75        return Bounds {
76            center: a.center,
77            radius: a.radius.max(b.radius),
78        };
79    }
80    let radius = (a.radius + distance + b.radius) * 0.5;
81    let center = a.center + offset * ((radius - a.radius) / distance);
82    Bounds { center, radius }
83}