use bevy::prelude::*;
use bevy_kana::Position;
use super::constants::MIN_VISIBLE_DEPTH;
pub(crate) struct CameraBasis {
pub position: Position,
pub right: Vec3,
pub up: Vec3,
pub forward: Vec3,
}
impl CameraBasis {
pub(crate) fn from_global_transform(global: &GlobalTransform) -> Self {
let rot = global.rotation();
Self {
position: Position(global.translation()),
right: rot * Vec3::X,
up: rot * Vec3::Y,
forward: rot * Vec3::NEG_Z,
}
}
}
pub(crate) struct ProjectionParams {
pub half_extent_x: f32,
pub half_extent_y: f32,
pub is_ortho: bool,
}
impl ProjectionParams {
pub(crate) fn from_projection(projection: &Projection, viewport_aspect: f32) -> Option<Self> {
let is_ortho = matches!(projection, Projection::Orthographic(_));
let (half_extent_x, half_extent_y) = match projection {
Projection::Perspective(p) => {
let half_tan_vfov = (p.fov * 0.5).tan();
(half_tan_vfov * viewport_aspect, half_tan_vfov)
},
Projection::Orthographic(o) => (o.area.width() * 0.5, o.area.height() * 0.5),
Projection::Custom(_) => return None,
};
Some(Self {
half_extent_x,
half_extent_y,
is_ortho,
})
}
}
pub(crate) fn project_point(
point: Vec3,
camera: &CameraBasis,
is_ortho: bool,
) -> Option<(f32, f32, f32)> {
let relative = point - *camera.position;
let depth = relative.dot(camera.forward);
if !is_ortho && depth <= MIN_VISIBLE_DEPTH {
return None;
}
let x = relative.dot(camera.right);
let y = relative.dot(camera.up);
let (norm_x, norm_y) = if is_ortho {
(x, y)
} else {
(x / depth, y / depth)
};
Some((norm_x, norm_y, depth))
}
pub(crate) fn projection_aspect_ratio(
projection: &Projection,
viewport_size: Option<Vec2>,
) -> Option<f32> {
match projection {
Projection::Perspective(p) => Some(viewport_size.map_or(p.aspect_ratio, |s| s.x / s.y)),
Projection::Orthographic(o) => {
let area = o.area;
if area.height().abs() < f32::EPSILON {
return None;
}
Some(area.width() / area.height())
},
Projection::Custom(_) => None,
}
}
#[derive(Debug, Clone)]
pub(crate) struct PointDepths {
pub min_x: f32,
pub max_x: f32,
pub min_y: f32,
pub max_y: f32,
#[cfg(feature = "fit_overlay")]
pub sum: f32,
#[cfg(feature = "fit_overlay")]
pub count: usize,
}
#[derive(Debug, Clone)]
pub(crate) struct ScreenSpaceBounds {
pub left_margin: f32,
pub right_margin: f32,
pub top_margin: f32,
pub bottom_margin: f32,
pub min_norm_x: f32,
pub max_norm_x: f32,
pub min_norm_y: f32,
pub max_norm_y: f32,
pub half_extent_x: f32,
pub half_extent_y: f32,
}
impl ScreenSpaceBounds {
#[allow(
clippy::similar_names,
reason = "min/max pairs per axis follow a consistent naming pattern"
)]
pub(crate) fn from_points(
points: &[Vec3],
camera_global: &GlobalTransform,
projection: &Projection,
viewport_aspect: f32,
) -> Option<(Self, PointDepths)> {
let ProjectionParams {
half_extent_x,
half_extent_y,
is_ortho,
} = ProjectionParams::from_projection(projection, viewport_aspect)?;
let camera_basis = CameraBasis::from_global_transform(camera_global);
let mut min_norm_x = f32::INFINITY;
let mut max_norm_x = f32::NEG_INFINITY;
let mut min_norm_y = f32::INFINITY;
let mut max_norm_y = f32::NEG_INFINITY;
let mut min_x = 0.0_f32;
let mut max_x = 0.0_f32;
let mut min_y = 0.0_f32;
let mut max_y = 0.0_f32;
#[cfg(feature = "fit_overlay")]
let mut sum = 0.0_f32;
for point in points {
let (norm_x, norm_y, depth) = project_point(*point, &camera_basis, is_ortho)?;
#[cfg(feature = "fit_overlay")]
{
sum += depth;
}
if norm_x < min_norm_x {
min_norm_x = norm_x;
min_x = depth;
}
if norm_x > max_norm_x {
max_norm_x = norm_x;
max_x = depth;
}
if norm_y < min_norm_y {
min_norm_y = norm_y;
min_y = depth;
}
if norm_y > max_norm_y {
max_norm_y = norm_y;
max_y = depth;
}
}
let left_margin = min_norm_x - (-half_extent_x);
let right_margin = half_extent_x - max_norm_x;
let bottom_margin = min_norm_y - (-half_extent_y);
let top_margin = half_extent_y - max_norm_y;
let bounds = Self {
left_margin,
right_margin,
top_margin,
bottom_margin,
min_norm_x,
max_norm_x,
min_norm_y,
max_norm_y,
half_extent_x,
half_extent_y,
};
let depths = PointDepths {
min_x,
max_x,
min_y,
max_y,
#[cfg(feature = "fit_overlay")]
sum,
#[cfg(feature = "fit_overlay")]
count: points.len(),
};
Some((bounds, depths))
}
pub(crate) const fn center(&self) -> (f32, f32) {
let center_x = (self.min_norm_x + self.max_norm_x) * 0.5;
let center_y = (self.min_norm_y + self.max_norm_y) * 0.5;
(center_x, center_y)
}
}
pub(crate) fn extract_mesh_vertices(
entity: Entity,
children_query: &Query<&Children>,
mesh_query: &Query<&Mesh3d>,
global_transform_query: &Query<&GlobalTransform>,
meshes: &Assets<Mesh>,
) -> Option<(Vec<Vec3>, Vec3)> {
let mesh_entities: Vec<Entity> = std::iter::once(entity)
.chain(children_query.iter_descendants(entity))
.filter(|e| mesh_query.get(*e).is_ok())
.collect();
if mesh_entities.is_empty() {
return None;
}
let mut all_vertices = Vec::new();
for mesh_entity in &mesh_entities {
let Ok(mesh3d) = mesh_query.get(*mesh_entity) else {
continue;
};
let Some(mesh) = meshes.get(&mesh3d.0) else {
continue;
};
let Ok(global_transform) = global_transform_query.get(*mesh_entity) else {
continue;
};
let Some(positions) = mesh
.attribute(Mesh::ATTRIBUTE_POSITION)
.and_then(|a| a.as_float3())
else {
continue;
};
all_vertices.extend(
positions
.iter()
.map(|pos| global_transform.transform_point(Vec3::from_array(*pos))),
);
}
if all_vertices.is_empty() {
return None;
}
let geometric_center = global_transform_query
.get(entity)
.map_or(Vec3::ZERO, GlobalTransform::translation);
Some((all_vertices, geometric_center))
}