use bevy::camera::primitives::Aabb;
use bevy::diagnostic::FrameCount;
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
use bevy::render::view::screenshot::{Screenshot, ScreenshotCaptured};
use bevy::sprite::Sprite;
use bevy::window::PrimaryWindow;
use chrono::{DateTime, Local};
use crossbeam_channel::{Receiver, Sender};
use crate::buffer::{CapturedScreenshot, ImageValidation, ScreenshotBufferManager};
use crate::save::SaveTaskSender;
#[derive(Clone, Debug)]
pub struct CropRegion {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl CropRegion {
pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn clamp_to_bounds(self, image_width: u32, image_height: u32) -> Option<Self> {
if self.x >= image_width || self.y >= image_height {
return None;
}
let clamped_width = (self.width).min(image_width.saturating_sub(self.x));
let clamped_height = (self.height).min(image_height.saturating_sub(self.y));
if clamped_width == 0 || clamped_height == 0 {
return None;
}
Some(Self {
x: self.x,
y: self.y,
width: clamped_width,
height: clamped_height,
})
}
}
#[derive(Clone, Debug)]
pub struct EntityScreenshotSettings {
pub padding: u32,
pub fallback_size: (u32, u32),
}
impl Default for EntityScreenshotSettings {
fn default() -> Self {
Self {
padding: 20,
fallback_size: (64, 64),
}
}
}
impl EntityScreenshotSettings {
pub fn with_padding(mut self, padding: u32) -> Self {
self.padding = padding;
self
}
pub fn with_fallback_size(mut self, width: u32, height: u32) -> Self {
self.fallback_size = (width, height);
self
}
}
#[derive(Resource, Default)]
pub struct RenderReadiness {
pub frames_rendered: u32,
pub is_ready: bool,
}
impl RenderReadiness {
pub const MIN_FRAMES_FOR_READY: u32 = 3;
pub fn ready(&self) -> bool {
self.is_ready && self.frames_rendered >= Self::MIN_FRAMES_FOR_READY
}
}
#[derive(Resource, Default)]
pub struct ShutdownState {
pub is_shutting_down: bool,
}
#[derive(Message, Clone)]
pub struct ScreenshotRequest {
pub key: String,
pub description: String,
pub timestamp: DateTime<Local>,
pub crop_region: Option<CropRegion>,
}
impl ScreenshotRequest {
pub fn new(key: impl Into<String>, description: impl Into<String>) -> Self {
Self {
key: key.into(),
description: description.into(),
timestamp: Local::now(),
crop_region: None,
}
}
pub fn with_crop(
key: impl Into<String>,
description: impl Into<String>,
crop_region: CropRegion,
) -> Self {
Self {
key: key.into(),
description: description.into(),
timestamp: Local::now(),
crop_region: Some(crop_region),
}
}
}
#[derive(Resource)]
pub struct ScreenshotRequestSender(pub Sender<ScreenshotRequest>);
#[derive(Resource)]
pub struct ScreenshotRequestReceiver(pub Receiver<ScreenshotRequest>);
#[derive(SystemParam)]
pub struct ScreenshotTrigger<'w> {
sender: Res<'w, ScreenshotRequestSender>,
}
impl ScreenshotTrigger<'_> {
pub fn capture(&self, description: impl Into<String>) {
self.capture_with_key("default", description);
}
pub fn capture_with_key(&self, key: impl Into<String>, description: impl Into<String>) {
let request = ScreenshotRequest::new(key, description);
let _ = self.sender.0.try_send(request);
}
}
#[derive(Component)]
pub struct PendingScreenshot {
pub key: String,
pub description: String,
pub timestamp: DateTime<Local>,
pub frame_number: u32,
pub crop_region: Option<CropRegion>,
}
pub fn process_screenshot_requests(
mut commands: Commands,
receiver: Res<ScreenshotRequestReceiver>,
windows: Query<Entity, With<PrimaryWindow>>,
render_readiness: Res<RenderReadiness>,
shutdown_state: Res<ShutdownState>,
frame_count: Res<FrameCount>,
) {
if shutdown_state.is_shutting_down {
while receiver.0.try_recv().is_ok() {}
return;
}
if !render_readiness.ready() {
let mut dropped_count = 0;
while receiver.0.try_recv().is_ok() {
dropped_count += 1;
}
if dropped_count > 0 {
bevy::log::warn!(
"Dropped {} screenshot request(s): render system not ready yet. \
Wait at least {} frames after startup before taking screenshots. \
(Current frame: {})",
dropped_count,
RenderReadiness::MIN_FRAMES_FOR_READY,
render_readiness.frames_rendered
);
}
return;
}
let current_frame = frame_count.0;
while let Ok(request) = receiver.0.try_recv() {
let Ok(_window_entity) = windows.single() else {
bevy::log::warn!("No primary window found for screenshot");
continue;
};
commands.spawn((
Screenshot::primary_window(),
PendingScreenshot {
key: request.key,
description: request.description,
timestamp: request.timestamp,
frame_number: current_frame,
crop_region: request.crop_region,
},
));
}
}
pub fn update_render_readiness(mut readiness: ResMut<RenderReadiness>) {
readiness.frames_rendered = readiness.frames_rendered.saturating_add(1);
if readiness.frames_rendered >= RenderReadiness::MIN_FRAMES_FOR_READY {
readiness.is_ready = true;
}
}
pub fn detect_shutdown(
mut shutdown_state: ResMut<ShutdownState>,
mut exit_events: MessageReader<AppExit>,
) {
for _ in exit_events.read() {
if !shutdown_state.is_shutting_down {
bevy::log::info!("Screenshot system: shutdown detected, stopping capture");
shutdown_state.is_shutting_down = true;
}
}
}
pub fn handle_screenshot_captured(
trigger: On<ScreenshotCaptured>,
mut commands: Commands,
pending_query: Query<&PendingScreenshot>,
mut buffer_manager: ResMut<ScreenshotBufferManager>,
save_sender: Res<SaveTaskSender>,
shutdown_state: Res<ShutdownState>,
) {
let entity = trigger.entity;
let captured = trigger.event();
if shutdown_state.is_shutting_down {
commands.entity(entity).despawn();
bevy::log::debug!("Screenshot discarded: application is shutting down");
return;
}
let Ok(pending) = pending_query.get(entity) else {
bevy::log::warn!("Screenshot captured but no pending metadata found");
return;
};
let screenshot = CapturedScreenshot {
image: captured.image.clone(),
timestamp: pending.timestamp,
description: pending.description.clone(),
key: pending.key.clone(),
frame_number: pending.frame_number,
crop_region: pending.crop_region.clone(),
};
match screenshot.validate() {
ImageValidation::Valid => {
buffer_manager.push(screenshot.clone());
let config = buffer_manager.config();
let should_save = config
.keys
.get(&pending.key)
.and_then(|k| k.auto_save)
.unwrap_or(config.auto_save);
if should_save {
let _ = save_sender.0.try_send(screenshot);
}
bevy::log::debug!(
"Screenshot captured: key='{}', description='{}'",
pending.key,
pending.description
);
}
ImageValidation::ZeroDimensions { width, height } => {
bevy::log::warn!(
"Screenshot discarded (key='{}', description='{}'): \
image has zero dimensions ({}x{}). \
This may indicate the screenshot was captured before the render system was ready.",
pending.key,
pending.description,
width,
height
);
}
ImageValidation::EmptyData => {
bevy::log::warn!(
"Screenshot discarded (key='{}', description='{}'): \
image has empty data. \
This may indicate the screenshot was captured during shutdown or before render was ready.",
pending.key,
pending.description
);
}
ImageValidation::DataSizeMismatch {
expected,
actual,
width,
height,
} => {
bevy::log::warn!(
"Screenshot discarded (key='{}', description='{}'): \
image data size mismatch (expected {} bytes for {}x{}, got {} bytes). \
The image may be corrupted.",
pending.key,
pending.description,
expected,
width,
height,
actual
);
}
}
commands.entity(entity).despawn();
}
#[derive(SystemParam)]
pub struct EntityScreenshotTrigger<'w, 's> {
sender: Res<'w, ScreenshotRequestSender>,
camera_2d_query: Query<'w, 's, (&'static Camera, &'static GlobalTransform), With<Camera2d>>,
camera_3d_query: Query<'w, 's, (&'static Camera, &'static GlobalTransform), With<Camera3d>>,
entity_query: Query<'w, 's, (&'static GlobalTransform, Option<&'static Sprite>, Option<&'static Aabb>)>,
window_query: Query<'w, 's, &'static Window, With<PrimaryWindow>>,
}
impl EntityScreenshotTrigger<'_, '_> {
pub fn capture(&self, entity: Entity) {
self.capture_with_key(entity, "default", "", EntityScreenshotSettings::default());
}
pub fn capture_key(&self, entity: Entity, key: impl Into<String>) {
self.capture_with_key(entity, key, "", EntityScreenshotSettings::default());
}
pub fn capture_key_desc(
&self,
entity: Entity,
key: impl Into<String>,
description: impl Into<String>,
) {
self.capture_with_key(entity, key, description, EntityScreenshotSettings::default());
}
pub fn capture_with_key(
&self,
entity: Entity,
key: impl Into<String>,
description: impl Into<String>,
settings: EntityScreenshotSettings,
) {
let key = key.into();
let description = description.into();
let Ok((entity_transform, sprite, aabb)) = self.entity_query.get(entity) else {
bevy::log::warn!(
"Entity screenshot failed: entity {:?} not found or missing required components",
entity
);
return;
};
let scale_factor = self
.window_query
.single()
.map(|w| w.scale_factor())
.unwrap_or(1.0);
if let Ok((camera, camera_transform)) = self.camera_2d_query.single() {
self.capture_with_camera_2d(
camera,
camera_transform,
entity_transform,
sprite,
aabb,
scale_factor,
&key,
&description,
&settings,
);
return;
}
if let Ok((camera, camera_transform)) = self.camera_3d_query.single() {
self.capture_with_camera_3d(
camera,
camera_transform,
entity_transform,
aabb,
scale_factor,
&key,
&description,
&settings,
);
return;
}
bevy::log::warn!("Entity screenshot failed: no 2D or 3D camera found");
}
fn capture_with_camera_2d(
&self,
camera: &Camera,
camera_transform: &GlobalTransform,
entity_transform: &GlobalTransform,
sprite: Option<&Sprite>,
aabb: Option<&Aabb>,
scale_factor: f32,
key: &str,
description: &str,
settings: &EntityScreenshotSettings,
) {
let entity_size = self.get_entity_size_2d(sprite, aabb, settings);
let entity_pos = entity_transform.translation();
let viewport_center = match camera.world_to_viewport(camera_transform, entity_pos) {
Ok(pos) => pos,
Err(_) => {
bevy::log::warn!(
"Entity screenshot (2D): entity is outside camera viewport, capturing with fallback"
);
let request = ScreenshotRequest::new(key, description);
let _ = self.sender.0.try_send(request);
return;
}
};
let crop_region = self.calculate_crop_region(
viewport_center,
entity_size,
scale_factor,
settings.padding,
);
let request = ScreenshotRequest::with_crop(key, description, crop_region);
let _ = self.sender.0.try_send(request);
}
fn capture_with_camera_3d(
&self,
camera: &Camera,
camera_transform: &GlobalTransform,
entity_transform: &GlobalTransform,
aabb: Option<&Aabb>,
scale_factor: f32,
key: &str,
description: &str,
settings: &EntityScreenshotSettings,
) {
let entity_pos = entity_transform.translation();
let screen_bounds = if let Some(aabb) = aabb {
self.project_aabb_to_screen(
camera,
camera_transform,
entity_transform,
aabb,
)
} else {
None
};
match screen_bounds {
Some((min_screen, max_screen)) => {
let center = Vec2::new(
(min_screen.x + max_screen.x) / 2.0,
(min_screen.y + max_screen.y) / 2.0,
);
let size = Vec2::new(
max_screen.x - min_screen.x,
max_screen.y - min_screen.y,
);
let crop_region = self.calculate_crop_region(
center,
size,
scale_factor,
settings.padding,
);
let request = ScreenshotRequest::with_crop(key, description, crop_region);
let _ = self.sender.0.try_send(request);
}
None => {
match camera.world_to_viewport(camera_transform, entity_pos) {
Ok(viewport_center) => {
let fallback_size = Vec2::new(
settings.fallback_size.0 as f32,
settings.fallback_size.1 as f32,
);
let crop_region = self.calculate_crop_region(
viewport_center,
fallback_size,
scale_factor,
settings.padding,
);
let request = ScreenshotRequest::with_crop(key, description, crop_region);
let _ = self.sender.0.try_send(request);
}
Err(_) => {
bevy::log::warn!(
"Entity screenshot (3D): entity is outside camera viewport, capturing full window"
);
let request = ScreenshotRequest::new(key, description);
let _ = self.sender.0.try_send(request);
}
}
}
}
}
fn project_aabb_to_screen(
&self,
camera: &Camera,
camera_transform: &GlobalTransform,
entity_transform: &GlobalTransform,
aabb: &Aabb,
) -> Option<(Vec2, Vec2)> {
let half_extents = aabb.half_extents;
let center = aabb.center;
let corners_local = [
Vec3::new(center.x - half_extents.x, center.y - half_extents.y, center.z - half_extents.z),
Vec3::new(center.x + half_extents.x, center.y - half_extents.y, center.z - half_extents.z),
Vec3::new(center.x - half_extents.x, center.y + half_extents.y, center.z - half_extents.z),
Vec3::new(center.x + half_extents.x, center.y + half_extents.y, center.z - half_extents.z),
Vec3::new(center.x - half_extents.x, center.y - half_extents.y, center.z + half_extents.z),
Vec3::new(center.x + half_extents.x, center.y - half_extents.y, center.z + half_extents.z),
Vec3::new(center.x - half_extents.x, center.y + half_extents.y, center.z + half_extents.z),
Vec3::new(center.x + half_extents.x, center.y + half_extents.y, center.z + half_extents.z),
];
let mut min_screen = Vec2::new(f32::MAX, f32::MAX);
let mut max_screen = Vec2::new(f32::MIN, f32::MIN);
let mut any_visible = false;
for corner_local in corners_local {
let corner_world = entity_transform.transform_point(corner_local);
if let Ok(screen_pos) = camera.world_to_viewport(camera_transform, corner_world) {
any_visible = true;
min_screen.x = min_screen.x.min(screen_pos.x);
min_screen.y = min_screen.y.min(screen_pos.y);
max_screen.x = max_screen.x.max(screen_pos.x);
max_screen.y = max_screen.y.max(screen_pos.y);
}
}
if any_visible {
Some((min_screen, max_screen))
} else {
None
}
}
fn calculate_crop_region(
&self,
viewport_center: Vec2,
entity_size: Vec2,
scale_factor: f32,
padding: u32,
) -> CropRegion {
let screen_center = viewport_center * scale_factor;
let physical_entity_size = entity_size * scale_factor;
let physical_padding = padding as f32 * scale_factor;
let half_width = physical_entity_size.x / 2.0 + physical_padding;
let half_height = physical_entity_size.y / 2.0 + physical_padding;
let crop_x = (screen_center.x - half_width).max(0.0) as u32;
let crop_y = (screen_center.y - half_height).max(0.0) as u32;
let crop_width = (half_width * 2.0) as u32;
let crop_height = (half_height * 2.0) as u32;
CropRegion::new(crop_x, crop_y, crop_width, crop_height)
}
fn get_entity_size_2d(
&self,
sprite: Option<&Sprite>,
aabb: Option<&Aabb>,
settings: &EntityScreenshotSettings,
) -> Vec2 {
if let Some(sprite) = sprite {
if let Some(custom_size) = sprite.custom_size {
return custom_size;
}
}
if let Some(aabb) = aabb {
let half_extents = aabb.half_extents;
return Vec2::new(half_extents.x * 2.0, half_extents.y * 2.0);
}
Vec2::new(settings.fallback_size.0 as f32, settings.fallback_size.1 as f32)
}
}
#[cfg(test)]
mod tests {
use super::*;
mod crop_region {
use super::*;
#[test]
fn test_new() {
let region = CropRegion::new(10, 20, 100, 200);
assert_eq!(region.x, 10);
assert_eq!(region.y, 20);
assert_eq!(region.width, 100);
assert_eq!(region.height, 200);
}
#[test]
fn test_clamp_to_bounds_fully_inside() {
let region = CropRegion::new(10, 10, 50, 50);
let clamped = region.clamp_to_bounds(100, 100);
assert!(clamped.is_some());
let c = clamped.unwrap();
assert_eq!(c.x, 10);
assert_eq!(c.y, 10);
assert_eq!(c.width, 50);
assert_eq!(c.height, 50);
}
#[test]
fn test_clamp_to_bounds_extends_right() {
let region = CropRegion::new(80, 10, 50, 50);
let clamped = region.clamp_to_bounds(100, 100);
assert!(clamped.is_some());
let c = clamped.unwrap();
assert_eq!(c.x, 80);
assert_eq!(c.y, 10);
assert_eq!(c.width, 20); assert_eq!(c.height, 50);
}
#[test]
fn test_clamp_to_bounds_extends_bottom() {
let region = CropRegion::new(10, 80, 50, 50);
let clamped = region.clamp_to_bounds(100, 100);
assert!(clamped.is_some());
let c = clamped.unwrap();
assert_eq!(c.x, 10);
assert_eq!(c.y, 80);
assert_eq!(c.width, 50);
assert_eq!(c.height, 20); }
#[test]
fn test_clamp_to_bounds_extends_both() {
let region = CropRegion::new(80, 80, 50, 50);
let clamped = region.clamp_to_bounds(100, 100);
assert!(clamped.is_some());
let c = clamped.unwrap();
assert_eq!(c.x, 80);
assert_eq!(c.y, 80);
assert_eq!(c.width, 20);
assert_eq!(c.height, 20);
}
#[test]
fn test_clamp_to_bounds_completely_outside_x() {
let region = CropRegion::new(100, 10, 50, 50);
let clamped = region.clamp_to_bounds(100, 100);
assert!(clamped.is_none());
}
#[test]
fn test_clamp_to_bounds_completely_outside_y() {
let region = CropRegion::new(10, 100, 50, 50);
let clamped = region.clamp_to_bounds(100, 100);
assert!(clamped.is_none());
}
#[test]
fn test_clamp_to_bounds_zero_width_after_clamp() {
let region = CropRegion::new(100, 10, 50, 50);
let clamped = region.clamp_to_bounds(100, 100);
assert!(clamped.is_none());
}
#[test]
fn test_clamp_to_bounds_zero_height_after_clamp() {
let region = CropRegion::new(10, 100, 50, 50);
let clamped = region.clamp_to_bounds(100, 100);
assert!(clamped.is_none());
}
#[test]
fn test_clamp_to_bounds_at_origin() {
let region = CropRegion::new(0, 0, 50, 50);
let clamped = region.clamp_to_bounds(100, 100);
assert!(clamped.is_some());
let c = clamped.unwrap();
assert_eq!(c.x, 0);
assert_eq!(c.y, 0);
assert_eq!(c.width, 50);
assert_eq!(c.height, 50);
}
#[test]
fn test_clamp_to_bounds_exact_fit() {
let region = CropRegion::new(0, 0, 100, 100);
let clamped = region.clamp_to_bounds(100, 100);
assert!(clamped.is_some());
let c = clamped.unwrap();
assert_eq!(c.x, 0);
assert_eq!(c.y, 0);
assert_eq!(c.width, 100);
assert_eq!(c.height, 100);
}
}
mod entity_screenshot_settings {
use super::*;
#[test]
fn test_default_values() {
let settings = EntityScreenshotSettings::default();
assert_eq!(settings.padding, 20);
assert_eq!(settings.fallback_size, (64, 64));
}
#[test]
fn test_with_padding() {
let settings = EntityScreenshotSettings::default().with_padding(50);
assert_eq!(settings.padding, 50);
assert_eq!(settings.fallback_size, (64, 64)); }
#[test]
fn test_with_fallback_size() {
let settings = EntityScreenshotSettings::default().with_fallback_size(128, 256);
assert_eq!(settings.padding, 20); assert_eq!(settings.fallback_size, (128, 256));
}
#[test]
fn test_builder_chain() {
let settings = EntityScreenshotSettings::default()
.with_padding(100)
.with_fallback_size(200, 300);
assert_eq!(settings.padding, 100);
assert_eq!(settings.fallback_size, (200, 300));
}
}
mod screenshot_request {
use super::*;
#[test]
fn test_new_without_crop() {
let request = ScreenshotRequest::new("test_key", "test_desc");
assert_eq!(request.key, "test_key");
assert_eq!(request.description, "test_desc");
assert!(request.crop_region.is_none());
}
#[test]
fn test_with_crop() {
let crop = CropRegion::new(10, 20, 100, 200);
let request = ScreenshotRequest::with_crop("test_key", "test_desc", crop);
assert_eq!(request.key, "test_key");
assert_eq!(request.description, "test_desc");
assert!(request.crop_region.is_some());
let c = request.crop_region.unwrap();
assert_eq!(c.x, 10);
assert_eq!(c.y, 20);
assert_eq!(c.width, 100);
assert_eq!(c.height, 200);
}
}
mod crop_region_3d {
use super::*;
fn crop_region_from_bounds(center: Vec2, size: Vec2, scale_factor: f32, padding: u32) -> CropRegion {
let screen_center = center * scale_factor;
let physical_size = size * scale_factor;
let physical_padding = padding as f32 * scale_factor;
let half_width = physical_size.x / 2.0 + physical_padding;
let half_height = physical_size.y / 2.0 + physical_padding;
let crop_x = (screen_center.x - half_width).max(0.0) as u32;
let crop_y = (screen_center.y - half_height).max(0.0) as u32;
let crop_width = (half_width * 2.0) as u32;
let crop_height = (half_height * 2.0) as u32;
CropRegion::new(crop_x, crop_y, crop_width, crop_height)
}
#[test]
fn test_crop_region_centered_no_padding() {
let center = Vec2::new(400.0, 300.0);
let size = Vec2::new(100.0, 100.0);
let region = crop_region_from_bounds(center, size, 1.0, 0);
assert_eq!(region.x, 350); assert_eq!(region.y, 250); assert_eq!(region.width, 100);
assert_eq!(region.height, 100);
}
#[test]
fn test_crop_region_with_padding() {
let center = Vec2::new(400.0, 300.0);
let size = Vec2::new(100.0, 100.0);
let region = crop_region_from_bounds(center, size, 1.0, 20);
assert_eq!(region.x, 330); assert_eq!(region.y, 230); assert_eq!(region.width, 140); assert_eq!(region.height, 140);
}
#[test]
fn test_crop_region_with_hidpi_scale() {
let center = Vec2::new(400.0, 300.0);
let size = Vec2::new(100.0, 100.0);
let region = crop_region_from_bounds(center, size, 2.0, 20);
assert_eq!(region.x, 660); assert_eq!(region.y, 460); assert_eq!(region.width, 280); assert_eq!(region.height, 280);
}
#[test]
fn test_crop_region_near_edge_clamped() {
let center = Vec2::new(30.0, 30.0);
let size = Vec2::new(100.0, 100.0);
let region = crop_region_from_bounds(center, size, 1.0, 10);
assert_eq!(region.x, 0);
assert_eq!(region.y, 0);
assert_eq!(region.width, 120); assert_eq!(region.height, 120);
}
#[test]
fn test_crop_region_asymmetric_size() {
let center = Vec2::new(400.0, 300.0);
let size = Vec2::new(80.0, 120.0); let region = crop_region_from_bounds(center, size, 1.0, 10);
assert_eq!(region.x, 350); assert_eq!(region.y, 230); assert_eq!(region.width, 100); assert_eq!(region.height, 140); }
#[test]
fn test_crop_region_small_entity_large_padding() {
let center = Vec2::new(400.0, 300.0);
let size = Vec2::new(10.0, 10.0);
let region = crop_region_from_bounds(center, size, 1.0, 100);
assert_eq!(region.x, 295); assert_eq!(region.y, 195); assert_eq!(region.width, 210); assert_eq!(region.height, 210);
}
#[test]
fn test_crop_region_fractional_scale() {
let center = Vec2::new(400.0, 300.0);
let size = Vec2::new(100.0, 100.0);
let region = crop_region_from_bounds(center, size, 1.5, 20);
assert_eq!(region.x, 495); assert_eq!(region.y, 345); assert_eq!(region.width, 210); assert_eq!(region.height, 210);
}
}
mod aabb_corners {
use super::*;
#[test]
fn test_unit_aabb_corners() {
let _aabb = Aabb {
center: Vec3A::ZERO,
half_extents: Vec3A::ONE,
};
let corners = [
Vec3::new(-1.0, -1.0, -1.0),
Vec3::new(1.0, -1.0, -1.0),
Vec3::new(-1.0, 1.0, -1.0),
Vec3::new(1.0, 1.0, -1.0),
Vec3::new(-1.0, -1.0, 1.0),
Vec3::new(1.0, -1.0, 1.0),
Vec3::new(-1.0, 1.0, 1.0),
Vec3::new(1.0, 1.0, 1.0),
];
for corner in corners {
let dist = corner.length();
assert!((dist - 3.0_f32.sqrt()).abs() < 0.001);
}
}
#[test]
fn test_offset_aabb_corners() {
let aabb = Aabb {
center: Vec3A::new(10.0, 20.0, 30.0),
half_extents: Vec3A::new(1.0, 2.0, 3.0),
};
let min_corner = Vec3::new(
aabb.center.x - aabb.half_extents.x,
aabb.center.y - aabb.half_extents.y,
aabb.center.z - aabb.half_extents.z,
);
assert_eq!(min_corner, Vec3::new(9.0, 18.0, 27.0));
let max_corner = Vec3::new(
aabb.center.x + aabb.half_extents.x,
aabb.center.y + aabb.half_extents.y,
aabb.center.z + aabb.half_extents.z,
);
assert_eq!(max_corner, Vec3::new(11.0, 22.0, 33.0));
}
#[test]
fn test_screen_bounds_from_corners() {
let screen_points = vec![
Vec2::new(100.0, 150.0),
Vec2::new(200.0, 150.0),
Vec2::new(100.0, 250.0),
Vec2::new(200.0, 250.0),
Vec2::new(120.0, 170.0),
Vec2::new(180.0, 170.0),
Vec2::new(120.0, 230.0),
Vec2::new(180.0, 230.0),
];
let mut min_screen = Vec2::new(f32::MAX, f32::MAX);
let mut max_screen = Vec2::new(f32::MIN, f32::MIN);
for pt in &screen_points {
min_screen.x = min_screen.x.min(pt.x);
min_screen.y = min_screen.y.min(pt.y);
max_screen.x = max_screen.x.max(pt.x);
max_screen.y = max_screen.y.max(pt.y);
}
assert_eq!(min_screen, Vec2::new(100.0, 150.0));
assert_eq!(max_screen, Vec2::new(200.0, 250.0));
let center = Vec2::new(
(min_screen.x + max_screen.x) / 2.0,
(min_screen.y + max_screen.y) / 2.0,
);
let size = Vec2::new(
max_screen.x - min_screen.x,
max_screen.y - min_screen.y,
);
assert_eq!(center, Vec2::new(150.0, 200.0));
assert_eq!(size, Vec2::new(100.0, 100.0));
}
#[test]
fn test_partial_visibility() {
let projected_points: Vec<Option<Vec2>> = vec![
Some(Vec2::new(100.0, 150.0)),
Some(Vec2::new(200.0, 150.0)),
None, None, Some(Vec2::new(120.0, 170.0)),
Some(Vec2::new(180.0, 170.0)),
None, None, ];
let mut min_screen = Vec2::new(f32::MAX, f32::MAX);
let mut max_screen = Vec2::new(f32::MIN, f32::MIN);
let mut any_visible = false;
for pt_opt in &projected_points {
if let Some(pt) = pt_opt {
any_visible = true;
min_screen.x = min_screen.x.min(pt.x);
min_screen.y = min_screen.y.min(pt.y);
max_screen.x = max_screen.x.max(pt.x);
max_screen.y = max_screen.y.max(pt.y);
}
}
assert!(any_visible);
assert_eq!(min_screen, Vec2::new(100.0, 150.0));
assert_eq!(max_screen, Vec2::new(200.0, 170.0));
}
#[test]
fn test_no_visibility() {
let projected_points: Vec<Option<Vec2>> = vec![
None, None, None, None, None, None, None, None,
];
let mut any_visible = false;
for pt_opt in &projected_points {
if pt_opt.is_some() {
any_visible = true;
}
}
assert!(!any_visible);
}
}
}