nightshade-api 0.46.0

Procedural high level API for the nightshade game engine
Documentation
//! World-space extents and framing. A tool needs to know how big a thing is to
//! point a camera at it; these read the engine's bounding volumes and merge
//! them into one sphere.

use nightshade::prelude::*;
use serde::{Deserialize, Serialize};

/// A world-space bounding sphere: enough to frame a selection without carrying
/// an oriented box around.
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Bounds {
    pub center: Vec3,
    pub radius: f32,
}

/// The entity's world-space bounds, its mesh bounding volume placed by its
/// global transform. `None` when the entity has no bounding volume.
pub fn bounds(world: &World, entity: Entity) -> Option<Bounds> {
    let bounding_volume = world.core.get_bounding_volume(entity)?;
    let global = world.core.get_global_transform(entity)?;
    let transformed = bounding_volume.transform(&global.0);
    Some(Bounds {
        center: transformed.obb.center,
        radius: transformed.sphere_radius,
    })
}

/// The bounds enclosing every listed entity, or `None` when none of them has a
/// bounding volume.
pub fn bounds_of(world: &World, entities: &[Entity]) -> Option<Bounds> {
    let mut combined: Option<Bounds> = None;
    for &entity in entities {
        if let Some(next) = bounds(world, entity) {
            combined = Some(match combined {
                Some(current) => merge(current, next),
                None => next,
            });
        }
    }
    combined
}

/// Points the active orbit camera at the listed entities, gliding its focus and
/// distance so they fill the view. A no-op without an orbit camera or any
/// bounded entity.
pub fn frame_entities(world: &mut World, entities: &[Entity]) {
    let Some(bounds) = bounds_of(world, entities) else {
        return;
    };
    let Some(camera) = world.resources.active_camera else {
        return;
    };
    if let Some(orbit) = world.core.get_pan_orbit_camera_mut(camera) {
        orbit.target_focus = bounds.center;
        orbit.target_radius = (bounds.radius * 2.5).max(0.5);
    }
}

fn merge(a: Bounds, b: Bounds) -> Bounds {
    let offset = b.center - a.center;
    let distance = offset.magnitude();
    if distance + b.radius <= a.radius {
        return a;
    }
    if distance + a.radius <= b.radius {
        return b;
    }
    if distance < f32::EPSILON {
        return Bounds {
            center: a.center,
            radius: a.radius.max(b.radius),
        };
    }
    let radius = (a.radius + distance + b.radius) * 0.5;
    let center = a.center + offset * ((radius - a.radius) / distance);
    Bounds { center, radius }
}