use crate::canvas::definition_drawing::reach_with_photo_m;
use crate::canvas::rotated_rect::RotatedRect;
use egui::{Pos2, Rect, Vec2, pos2, vec2};
use gled_model::patching::definition::{FixtureDefinition, Pixel};
use gled_model::patching::fixture::UNITS_PER_METRE;
const MIN_SCENE: f32 = 50.0;
const MAX_SCENE: f32 = 4000.0;
const SCENE_MARGIN: f32 = 1.5;
pub(crate) fn scene_side(definition: &FixtureDefinition) -> f32 {
(2.0 * SCENE_MARGIN * reach_with_photo_m(definition) * UNITS_PER_METRE)
.clamp(MIN_SCENE, MAX_SCENE)
}
pub(crate) fn outgrows(definition: &FixtureDefinition, side: f32) -> bool {
2.0 * reach_with_photo_m(definition) * UNITS_PER_METRE > 0.8 * side && side < MAX_SCENE
}
pub(crate) fn origin(side: f32) -> Pos2 {
pos2(side / 2.0, side / 2.0)
}
pub(crate) fn to_scene(origin: Pos2, position_m: [f32; 3]) -> Pos2 {
origin + vec2(position_m[0], position_m[1]) * UNITS_PER_METRE
}
pub(crate) fn to_metres(origin: Pos2, scene: Pos2) -> [f32; 2] {
let offset = (scene - origin) / UNITS_PER_METRE;
[offset.x, offset.y]
}
pub(crate) fn pixel_shape(
definition: &FixtureDefinition,
pixel: usize,
origin: Pos2,
min_half: f32,
) -> Option<RotatedRect> {
let pixel = definition.pixels.get(pixel)?;
let size_m = definition.model(pixel.model).map(|model| model.size_m);
Some(pixel_shape_of(pixel, size_m, origin, min_half))
}
pub(crate) fn pixel_shape_of(
pixel: &Pixel,
size_m: Option<[f32; 2]>,
origin: Pos2,
min_half: f32,
) -> RotatedRect {
let size = size_m.map_or(Vec2::ZERO, |size| vec2(size[0], size[1]) * UNITS_PER_METRE);
RotatedRect {
centre: to_scene(origin, pixel.position_m),
half: vec2((size.x / 2.0).max(min_half), (size.y / 2.0).max(min_half)),
angle: pixel.rotation,
}
}
pub(crate) fn pixel_under(
definition: &FixtureDefinition,
origin: Pos2,
point: Pos2,
min_half: f32,
) -> Option<usize> {
(0..definition.pixels.len()).rev().find(|pixel| {
pixel_shape(definition, *pixel, origin, min_half).is_some_and(|shape| shape.contains(point))
})
}
pub(crate) fn covered_pixels(
definition: &FixtureDefinition,
origin: Pos2,
band: Rect,
) -> Vec<usize> {
definition
.pixels
.iter()
.enumerate()
.filter(|(_, pixel)| band.contains(to_scene(origin, pixel.position_m)))
.map(|(index, _)| index)
.collect()
}
pub(crate) fn selection_bbox(
definition: &FixtureDefinition,
origin: Pos2,
selection: impl IntoIterator<Item = usize>,
) -> Option<Rect> {
let corners: Vec<Pos2> = selection
.into_iter()
.filter_map(|pixel| pixel_shape(definition, pixel, origin, 0.0))
.flat_map(|shape| shape.corners())
.collect();
(!corners.is_empty()).then(|| Rect::from_points(&corners))
}
pub(crate) fn label_font_px(size_px: f32) -> Option<f32> {
(size_px >= 14.0).then(|| (size_px * 0.45).clamp(9.0, 16.0))
}
#[cfg(test)]
mod tests {
use super::*;
use gled_model::patching::definition::{EmittedColor, Model, Photo};
fn definition() -> FixtureDefinition {
let pixel = |x_m: f32| Pixel {
name: "p".to_string(),
model: 0,
position_m: [x_m, 0.0, 0.0],
rotation: 0.0,
channels: vec![EmittedColor::Red, EmittedColor::Green, EmittedColor::Blue],
};
FixtureDefinition {
size_m: [0.3, 0.02, 0.02],
models: vec![Model::circle("led", 0.02)],
pixels: vec![pixel(-0.1), pixel(0.0), pixel(0.005)],
..Default::default()
}
}
#[test]
fn metres_and_scene_units_round_trip() {
let origin = origin(100.0);
let scene = to_scene(origin, [0.12, -0.05, 0.0]);
assert_eq!(scene, pos2(62.0, 45.0));
let [x, y] = to_metres(origin, scene);
assert!((x - 0.12).abs() < 1e-5 && (y + 0.05).abs() < 1e-5);
}
#[test]
fn the_last_drawn_of_two_overlapping_pixels_is_hit() {
let origin = origin(100.0);
assert_eq!(pixel_under(&definition(), origin, origin, 0.0), Some(2));
assert_eq!(
pixel_under(&definition(), origin, pos2(40.0, 50.0), 0.0),
Some(0)
);
assert_eq!(
pixel_under(&definition(), origin, pos2(90.0, 50.0), 0.0),
None
);
}
#[test]
fn a_band_covers_the_pixels_whose_centres_it_holds() {
let origin = origin(100.0);
let band = Rect::from_min_max(pos2(35.0, 40.0), pos2(45.0, 60.0));
assert_eq!(covered_pixels(&definition(), origin, band), vec![0]);
}
#[test]
fn the_selection_box_spans_the_selected_drawings() {
let origin = origin(100.0);
let bbox = selection_bbox(&definition(), origin, [0, 1]).expect("two pixels");
let expected = Rect::from_min_max(pos2(39.0, 49.0), pos2(51.0, 51.0));
assert!((bbox.min - expected.min).length() < 1e-4, "{bbox:?}");
assert!((bbox.max - expected.max).length() < 1e-4, "{bbox:?}");
}
#[test]
fn the_scene_leaves_room_around_the_fixture() {
assert!((scene_side(&definition()) - MIN_SCENE).abs() < 1e-3);
assert!(!outgrows(&definition(), MIN_SCENE));
assert!(outgrows(&definition(), 30.0));
}
#[test]
fn the_scene_makes_room_for_the_photo() {
let mut definition = definition();
definition.photo = Some(Photo {
file: "lamp.png".into(),
size_m: [1.0, 1.0],
offset_m: [0.5, 0.0],
opacity: 0.1,
});
assert!((scene_side(&definition) - 300.0).abs() < 1e-3);
}
#[test]
fn tiny_pixels_get_no_number() {
assert_eq!(label_font_px(10.0), None);
assert_eq!(label_font_px(20.0), Some(9.0));
}
}