Skip to main content

polytope_playground/
hud.rs

1use std::fmt::Write as _;
2
3use loam::app::egui;
4use loam::math::Plane4;
5use loam::text::{TextDraw, TextPass};
6
7const SIZE_PT: f32 = 16.0;
8
9// loam-text has no mip chain, so bake at 4x and only ever minify.
10const BAKE_PX: f32 = 4.0 * SIZE_PT;
11
12const INSET_PT: f32 = 16.0;
13
14const COLOR: [f32; 4] = [0.92, 0.96, 1.0, 1.0];
15
16const SHADOW_COLOR: [f32; 4] = [0.0, 0.0, 0.0, 0.7];
17
18const SHADOW_OFFSET_PT: f32 = 1.0;
19
20const PLANE_OFF: &str = "..";
21
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub(crate) struct Readout {
24    pub(crate) slice: f32,
25    pub(crate) rate: f32,
26    pub(crate) bodies: usize,
27    pub(crate) planes: [bool; 6],
28    pub(crate) toybox: bool,
29}
30
31// loam-text lays out on advance widths only, so columns need padded formatting.
32pub(crate) fn write_readout(out: &mut String, readout: &Readout) {
33    out.clear();
34    let _ = writeln!(out, "{:<6} {:>+8.3}", "w", readout.slice);
35    if readout.toybox {
36        let _ = write!(out, "{:<6} {:>7}", "bodies", readout.bodies);
37        return;
38    }
39    let _ = writeln!(out, "{:<6} {:>7.2}x", "rate", readout.rate);
40    let _ = writeln!(out, "{:<6} {:>7}", "bodies", readout.bodies);
41    let _ = write!(out, "{:<6} ", "planes");
42    for (index, plane) in Plane4::ALL.into_iter().enumerate() {
43        if index > 0 {
44            out.push(' ');
45        }
46        out.push_str(if readout.planes[index] {
47            plane.label()
48        } else {
49            PLANE_OFF
50        });
51    }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq)]
55pub(crate) struct Seat {
56    pub(crate) origin_px: [f32; 2],
57    pub(crate) pixels_per_point: f32,
58}
59
60impl Default for Seat {
61    fn default() -> Self {
62        Self {
63            origin_px: [INSET_PT, INSET_PT],
64            pixels_per_point: 1.0,
65        }
66    }
67}
68
69impl Seat {
70    pub(crate) fn in_panel(free: egui::Rect, pixels_per_point: f32) -> Self {
71        Self {
72            origin_px: [
73                (free.left() + INSET_PT) * pixels_per_point,
74                (free.top() + INSET_PT) * pixels_per_point,
75            ],
76            pixels_per_point,
77        }
78    }
79
80    fn size_px(&self) -> f32 {
81        SIZE_PT * self.pixels_per_point
82    }
83
84    // Shadow first so the body paints over it.
85    fn draws(&self) -> [TextDraw; 2] {
86        let offset = SHADOW_OFFSET_PT * self.pixels_per_point;
87        [
88            TextDraw {
89                origin_px: [self.origin_px[0] + offset, self.origin_px[1] + offset],
90                size_px: self.size_px(),
91                color: SHADOW_COLOR,
92            },
93            TextDraw {
94                origin_px: self.origin_px,
95                size_px: self.size_px(),
96                color: COLOR,
97            },
98        ]
99    }
100}
101
102pub(crate) fn pass() -> TextPass {
103    TextPass::new("hud", font_bytes().unwrap_or_default(), BAKE_PX)
104}
105
106pub(crate) fn publish(pass: &TextPass, readout: Option<&Readout>, seat: Seat, lines: &mut String) {
107    match readout {
108        Some(readout) => {
109            write_readout(lines, readout);
110            pass.publish(lines, &seat.draws());
111        }
112        None => pass.publish("", &[]),
113    }
114}
115
116fn font_bytes() -> Option<Vec<u8>> {
117    let fonts = egui::FontDefinitions::default();
118    let chosen = fonts
119        .font_data
120        .iter()
121        .find(|(name, _)| name.starts_with("Hack"))
122        .or_else(|| fonts.font_data.iter().next())?;
123    Some(chosen.1.font.to_vec())
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    fn readout(value: f32, planes: [bool; 6]) -> Readout {
131        Readout {
132            slice: value,
133            rate: value,
134            bodies: 8,
135            planes,
136            toybox: false,
137        }
138    }
139
140    #[test]
141    fn every_extreme_float_stays_inside_the_fonts_own_glyphs() {
142        let mut out = String::new();
143        for value in [
144            0.0_f32,
145            -0.0,
146            f32::NAN,
147            f32::INFINITY,
148            f32::NEG_INFINITY,
149            f32::MIN,
150            f32::MAX,
151            f32::MIN_POSITIVE,
152            -1.0e-30,
153        ] {
154            write_readout(&mut out, &readout(value, [true; 6]));
155            assert!(
156                loam::text::is_renderable(&out),
157                "the readout for {value} carries characters loam-text drops: {out:?}"
158            );
159        }
160    }
161
162    #[test]
163    fn the_plane_strip_names_exactly_the_active_planes_in_place() {
164        let mut out = String::new();
165        let mut planes = [false; 6];
166        planes[2] = true;
167        planes[3] = true;
168        write_readout(&mut out, &readout(0.0, planes));
169        let strip = out.lines().last().expect("the planes line").to_string();
170        assert!(
171            strip.ends_with(".. .. xw yz .. .."),
172            "the strip was {strip:?}"
173        );
174
175        write_readout(&mut out, &readout(0.0, [false; 6]));
176        let off = out.lines().last().expect("the planes line");
177        assert_eq!(
178            off.chars().count(),
179            strip.chars().count(),
180            "the strip changes width with the active planes, so the column below it moves"
181        );
182    }
183
184    #[test]
185    fn the_seat_scales_its_whole_placement_by_pixels_per_point() {
186        let free = egui::Rect::from_min_max(egui::pos2(0.0, 24.0), egui::pos2(1280.0, 720.0));
187        let unit = Seat::in_panel(free, 1.0);
188        for ratio in [1.25_f32, 1.5, 2.0, 3.0] {
189            let scaled = Seat::in_panel(free, ratio);
190            for (at, held) in scaled.draws().iter().zip(unit.draws().iter()) {
191                assert!(
192                    (at.origin_px[0] - held.origin_px[0] * ratio).abs() < 1e-3
193                        && (at.origin_px[1] - held.origin_px[1] * ratio).abs() < 1e-3,
194                    "at {ratio}x the origin is {:?}, not {:?} scaled",
195                    at.origin_px,
196                    held.origin_px
197                );
198                assert!((at.size_px - held.size_px * ratio).abs() < 1e-3);
199                assert_eq!(at.color, held.color, "the scale changed a color");
200            }
201        }
202    }
203}