use concinnity_core::components::Sprite;
use concinnity_core::ecs::World;
use concinnity_core::ecs::asset_id::AssetId;
use crate::editor::hud_ids::{Family, family_base, hud_ids};
use crate::editor::theme;
pub(crate) const MAX_RINGS: usize = 32;
hud_ids! {
base: family_base(Family::Highlight);
sprites: [ring_id[MAX_RINGS]];
}
const BORDER_W: f32 = 2.0;
const PAD_PX: f32 = 3.0;
const MEMBER_TINT: [f32; 4] = [0.20, 0.30, 0.45, 1.0];
pub(crate) fn outline_sprites() -> Vec<(AssetId, Sprite)> {
ids()
.sprites
.iter()
.map(|&id| {
let ring = Sprite {
tint: [0.0, 0.0, 0.0, 0.0],
border_width: BORDER_W,
border_color: theme::ACCENT_TINT,
visible: false,
..Default::default()
};
(id, ring)
})
.collect()
}
pub(crate) fn screen_rect(
view: &[[f32; 4]; 4],
fov_y_radians: f32,
viewport: [f32; 2],
bb_min: [f32; 3],
bb_max: [f32; 3],
) -> Option<[f32; 4]> {
let (vw, vh) = (viewport[0], viewport[1]);
if vw <= 0.0 || vh <= 0.0 {
return None;
}
for i in 0..3 {
if !bb_min[i].is_finite() || !bb_max[i].is_finite() {
return None;
}
}
let tan_half = (fov_y_radians * 0.5).tan();
if tan_half <= 0.0 || !tan_half.is_finite() {
return None;
}
let aspect = vw / vh;
let (mut x0, mut y0) = (f32::INFINITY, f32::INFINITY);
let (mut x1, mut y1) = (f32::NEG_INFINITY, f32::NEG_INFINITY);
for corner in 0..8 {
let c = [
if corner & 1 == 0 {
bb_min[0]
} else {
bb_max[0]
},
if corner & 2 == 0 {
bb_min[1]
} else {
bb_max[1]
},
if corner & 4 == 0 {
bb_min[2]
} else {
bb_max[2]
},
];
let v = [
view[0][0] * c[0] + view[1][0] * c[1] + view[2][0] * c[2] + view[3][0],
view[0][1] * c[0] + view[1][1] * c[1] + view[2][1] * c[2] + view[3][1],
view[0][2] * c[0] + view[1][2] * c[1] + view[2][2] * c[2] + view[3][2],
];
let depth = -v[2];
if depth <= 1e-4 {
return None;
}
let px = (v[0] / (depth * tan_half * aspect) + 1.0) * 0.5 * vw;
let py = (1.0 - v[1] / (depth * tan_half)) * 0.5 * vh;
x0 = x0.min(px);
y0 = y0.min(py);
x1 = x1.max(px);
y1 = y1.max(py);
}
let (x0, y0) = ((x0 - PAD_PX).max(0.0), (y0 - PAD_PX).max(0.0));
let (x1, y1) = ((x1 + PAD_PX).min(vw), (y1 + PAD_PX).min(vh));
if x1 <= x0 || y1 <= y0 {
return None;
}
Some([x0, y0, x1 - x0, y1 - y0])
}
pub(crate) fn place_all(world: &mut World, rects: &[([f32; 4], bool)]) {
for (i, &id) in ids().sprites.iter().enumerate() {
let Some(s) = sprite_mut(world, id) else {
continue;
};
match rects.get(i) {
Some((rect, active)) => {
s.x = rect[0];
s.y = rect[1];
s.width = rect[2];
s.height = rect[3];
s.border_color = if *active {
theme::ACCENT_TINT
} else {
MEMBER_TINT
};
s.visible = true;
}
None => s.visible = false,
}
}
}
pub(crate) fn hide(world: &mut World) {
place_all(world, &[]);
}
fn sprite_mut(world: &mut World, id: AssetId) -> Option<&mut Sprite> {
world.get_mut_by_id::<Sprite>(id)
}
#[cfg(test)]
mod tests {
use super::*;
use concinnity_core::gfx::camera::view_matrix;
const VP: [f32; 2] = [1280.0, 720.0];
const FOV: f32 = core::f32::consts::FRAC_PI_2;
#[test]
fn centered_box_projects_to_a_centered_rect() {
let view = view_matrix([0.0; 3], 0.0, 0.0);
let r = screen_rect(&view, FOV, VP, [-1.0, -1.0, -6.0], [1.0, 1.0, -4.0]).unwrap();
let (cx, cy) = (r[0] + r[2] * 0.5, r[1] + r[3] * 0.5);
assert!((cx - 640.0).abs() < 1.0, "{cx}");
assert!((cy - 360.0).abs() < 1.0, "{cy}");
assert!((r[3] * 0.5 - (90.0 + PAD_PX)).abs() < 1.0, "{}", r[3]);
}
#[test]
fn offscreen_and_behind_boxes_yield_no_rect() {
let view = view_matrix([0.0; 3], 0.0, 0.0);
assert_eq!(
screen_rect(&view, FOV, VP, [-1.0, -1.0, 4.0], [1.0, 1.0, 6.0]),
None
);
assert_eq!(
screen_rect(&view, FOV, VP, [50.0, -1.0, -6.0], [52.0, 1.0, -4.0]),
None
);
assert_eq!(
screen_rect(&view, FOV, VP, [f32::NAN; 3], [f32::NAN; 3]),
None
);
assert_eq!(
screen_rect(&view, FOV, [0.0, 720.0], [-1.0; 3], [1.0; 3]),
None
);
}
#[test]
fn edge_crossing_box_clamps_to_the_viewport() {
let view = view_matrix([0.0; 3], 0.0, 0.0);
let r = screen_rect(&view, FOV, VP, [-50.0, -1.0, -6.0], [0.0, 1.0, -4.0]).unwrap();
assert_eq!(r[0], 0.0);
assert!(r[2] <= VP[0]);
}
}