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
9const 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
31pub(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 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}