1use crate::map_tiles::MapLike;
24
25pub const DEFAULT_COVERAGE_GRID: u32 = 64;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31enum AxisMode {
32 Start,
34 Center,
36 End,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum HAlign {
43 Left,
45 Center,
47 Right,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum VAlign {
54 Top,
56 Center,
58 Bottom,
60}
61
62impl HAlign {
63 #[must_use]
66 pub const fn offset(self, content: u32, available: u32) -> u32 {
67 let slack = available.saturating_sub(content);
68 match self {
69 Self::Left => 0,
70 Self::Center => slack / 2,
71 Self::Right => slack,
72 }
73 }
74}
75
76impl VAlign {
77 #[must_use]
80 pub const fn offset(self, content: u32, available: u32) -> u32 {
81 let slack = available.saturating_sub(content);
82 match self {
83 Self::Top => 0,
84 Self::Center => slack / 2,
85 Self::Bottom => slack,
86 }
87 }
88}
89
90const fn axis_origin(mode: AxisMode, content: u32, total: u32, margin: u32) -> u32 {
94 let slack = total.saturating_sub(content);
95 match mode {
96 AxisMode::Start => {
98 if margin < slack {
99 margin
100 } else {
101 slack
102 }
103 }
104 AxisMode::Center => slack / 2,
105 AxisMode::End => slack.saturating_sub(margin),
107 }
108}
109
110fn run_for_mode(col_free: &[bool], mode: AxisMode) -> (u32, u32) {
115 let n = u32::try_from(col_free.len()).unwrap_or(u32::MAX);
116 match mode {
117 AxisMode::Start => {
118 let mut w = 0;
119 while w < n && col_free.get(w as usize).copied().unwrap_or(false) {
120 w += 1;
121 }
122 (0, w)
123 }
124 AxisMode::End => {
125 let mut w = 0;
126 while w < n && col_free.get((n - 1 - w) as usize).copied().unwrap_or(false) {
127 w += 1;
128 }
129 (n - w, w)
130 }
131 AxisMode::Center => {
132 for w in (1..=n).rev() {
133 let c0 = (n - w) / 2;
134 if (c0..c0 + w).all(|c| col_free.get(c as usize).copied().unwrap_or(false)) {
135 return (c0, w);
136 }
137 }
138 (n / 2, 0)
139 }
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
147pub enum PlacementSlot {
148 TopLeft,
150 TopCenter,
152 TopRight,
154 MiddleLeft,
156 Center,
158 MiddleRight,
160 BottomLeft,
162 BottomCenter,
164 BottomRight,
166}
167
168impl PlacementSlot {
169 pub const ALL: [Self; 9] = [
172 Self::TopLeft,
173 Self::TopCenter,
174 Self::TopRight,
175 Self::MiddleLeft,
176 Self::Center,
177 Self::MiddleRight,
178 Self::BottomLeft,
179 Self::BottomCenter,
180 Self::BottomRight,
181 ];
182
183 #[must_use]
187 pub const fn as_str(self) -> &'static str {
188 match self {
189 Self::TopLeft => "top_left",
190 Self::TopCenter => "top_center",
191 Self::TopRight => "top_right",
192 Self::MiddleLeft => "middle_left",
193 Self::Center => "center",
194 Self::MiddleRight => "middle_right",
195 Self::BottomLeft => "bottom_left",
196 Self::BottomCenter => "bottom_center",
197 Self::BottomRight => "bottom_right",
198 }
199 }
200
201 #[must_use]
205 pub const fn cell(self) -> (u8, u8) {
206 match self {
207 Self::TopLeft => (0, 0),
208 Self::TopCenter => (1, 0),
209 Self::TopRight => (2, 0),
210 Self::MiddleLeft => (0, 1),
211 Self::Center => (1, 1),
212 Self::MiddleRight => (2, 1),
213 Self::BottomLeft => (0, 2),
214 Self::BottomCenter => (1, 2),
215 Self::BottomRight => (2, 2),
216 }
217 }
218
219 #[must_use]
221 const fn horizontal(self) -> AxisMode {
222 match self.cell().0 {
223 0 => AxisMode::Start,
224 1 => AxisMode::Center,
225 _ => AxisMode::End,
226 }
227 }
228
229 #[must_use]
231 const fn vertical(self) -> AxisMode {
232 match self.cell().1 {
233 0 => AxisMode::Start,
234 1 => AxisMode::Center,
235 _ => AxisMode::End,
236 }
237 }
238
239 #[must_use]
244 pub const fn default_alignment(self) -> (HAlign, VAlign) {
245 let (h, v) = self.cell();
246 let ha = match h {
247 0 => HAlign::Left,
248 1 => HAlign::Center,
249 _ => HAlign::Right,
250 };
251 let va = match v {
252 0 => VAlign::Top,
253 1 => VAlign::Center,
254 _ => VAlign::Bottom,
255 };
256 (ha, va)
257 }
258
259 #[must_use]
266 pub const fn anchored_origin(
267 self,
268 content_w: u32,
269 content_h: u32,
270 img_w: u32,
271 img_h: u32,
272 margin: u32,
273 ) -> (u32, u32) {
274 let x = axis_origin(self.horizontal(), content_w, img_w, margin);
275 let y = axis_origin(self.vertical(), content_h, img_h, margin);
276 (x, y)
277 }
278
279 #[must_use]
286 pub fn slots_form_rectangle(slots: &[Self]) -> bool {
287 let mut cells: Vec<(u8, u8)> = slots.iter().map(|s| s.cell()).collect();
288 cells.sort_unstable();
289 cells.dedup();
290 if cells.len() <= 1 {
291 return true;
292 }
293 let c_min = cells.iter().map(|&(c, _)| c).min().unwrap_or(0);
294 let c_max = cells.iter().map(|&(c, _)| c).max().unwrap_or(0);
295 let r_min = cells.iter().map(|&(_, r)| r).min().unwrap_or(0);
296 let r_max = cells.iter().map(|&(_, r)| r).max().unwrap_or(0);
297 let width = u32::from(c_max - c_min) + 1;
298 let height = u32::from(r_max - r_min) + 1;
299 u32::try_from(cells.len()).unwrap_or(u32::MAX) == width * height
302 }
303
304 #[must_use]
307 pub fn neighbours(self) -> Vec<Self> {
308 let (h, v) = self.cell();
309 Self::ALL
310 .into_iter()
311 .filter(|other| {
312 let (oh, ov) = other.cell();
313 let dh = (i16::from(h) - i16::from(oh)).abs();
314 let dv = (i16::from(v) - i16::from(ov)).abs();
315 dh + dv == 1
316 })
317 .collect()
318 }
319}
320
321impl std::fmt::Display for PlacementSlot {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 f.write_str(self.as_str())
324 }
325}
326
327#[derive(Debug, Clone, thiserror::Error)]
330#[error("`{0}` is not a valid placement slot name")]
331pub struct ParsePlacementSlotError(String);
332
333impl std::str::FromStr for PlacementSlot {
334 type Err = ParsePlacementSlotError;
335
336 fn from_str(s: &str) -> Result<Self, Self::Err> {
337 Ok(match s {
338 "top_left" => Self::TopLeft,
339 "top_center" => Self::TopCenter,
340 "top_right" => Self::TopRight,
341 "middle_left" => Self::MiddleLeft,
342 "center" => Self::Center,
343 "middle_right" => Self::MiddleRight,
344 "bottom_left" => Self::BottomLeft,
345 "bottom_center" => Self::BottomCenter,
346 "bottom_right" => Self::BottomRight,
347 other => return Err(ParsePlacementSlotError(other.to_owned())),
348 })
349 }
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub struct PixelRect {
355 pub x: u32,
357 pub y: u32,
359 pub width: u32,
361 pub height: u32,
363}
364
365#[derive(Debug, Clone, PartialEq)]
367pub struct PlacementSlotInfo {
368 pub slot: PlacementSlot,
370 pub available: bool,
373 pub free_rect: Option<PixelRect>,
378 pub free_size: (u32, u32),
381 pub occupied_fraction: f32,
384 pub connected_neighbours: Vec<PlacementSlot>,
387}
388
389#[derive(Debug, Clone)]
395pub struct OccupancyGrid {
396 cols: u32,
398 rows: u32,
400 cell_size: u32,
402 img_width: u32,
404 img_height: u32,
406 occupied: Vec<bool>,
408}
409
410impl OccupancyGrid {
411 fn grid_params(img_width: u32, img_height: u32, grid_resolution: u32) -> (u32, u32, u32) {
415 if img_width == 0 || img_height == 0 {
416 return (0, 0, 1);
417 }
418 let resolution = grid_resolution.max(1);
419 let longer = img_width.max(img_height);
420 let cell_size = longer.div_ceil(resolution).max(1);
421 let cols = img_width.div_ceil(cell_size).max(1);
422 let rows = img_height.div_ceil(cell_size).max(1);
423 (cols, rows, cell_size)
424 }
425
426 #[must_use]
434 pub fn from_map<M: MapLike + ?Sized>(map: &M, grid_resolution: u32) -> Self {
435 let rgba = map.image().to_rgba8();
436 let (img_width, img_height) = rgba.dimensions();
437 let (cols, rows, cell_size) = Self::grid_params(img_width, img_height, grid_resolution);
438 let mut occupied = vec![false; (cols * rows) as usize];
439 if cols > 0 && rows > 0 {
440 for (x, y, pixel) in rgba.enumerate_pixels() {
441 if pixel[3] != 0 {
442 let col = (x / cell_size).min(cols - 1);
443 let row = (y / cell_size).min(rows - 1);
444 if let Some(cell) = occupied.get_mut((row * cols + col) as usize) {
445 *cell = true;
446 }
447 }
448 }
449 }
450 Self {
451 cols,
452 rows,
453 cell_size,
454 img_width,
455 img_height,
456 occupied,
457 }
458 }
459
460 fn is_free(&self, row: u32, col: u32) -> bool {
463 !self
464 .occupied
465 .get((row * self.cols + col) as usize)
466 .copied()
467 .unwrap_or(true)
468 }
469
470 fn cell_rect_to_pixels(&self, r0: u32, r1: u32, c0: u32, c1: u32) -> PixelRect {
473 let x = c0 * self.cell_size;
474 let y = r0 * self.cell_size;
475 let width = (c1 * self.cell_size).min(self.img_width).saturating_sub(x);
476 let height = (r1 * self.cell_size).min(self.img_height).saturating_sub(y);
477 PixelRect {
478 x,
479 y,
480 width,
481 height,
482 }
483 }
484
485 const fn col_band(&self, h3: u32) -> (u32, u32) {
491 let edge = self.cols / 3;
492 match h3 {
493 0 => (0, edge),
494 1 => (edge, self.cols - edge),
495 _ => (self.cols - edge, self.cols),
496 }
497 }
498
499 const fn row_band(&self, v3: u32) -> (u32, u32) {
503 let edge = self.rows / 3;
504 match v3 {
505 0 => (0, edge),
506 1 => (edge, self.rows - edge),
507 _ => (self.rows - edge, self.rows),
508 }
509 }
510
511 fn best_anchored_rect(
520 c_lo: u32,
521 c_hi: u32,
522 r_lo: u32,
523 r_hi: u32,
524 hmode: AxisMode,
525 vmode: AxisMode,
526 free: impl Fn(u32, u32) -> bool,
527 ) -> Option<(u32, u32, u32, u32)> {
528 if c_lo >= c_hi || r_lo >= r_hi {
529 return None;
530 }
531 let band_rows = r_hi - r_lo;
532 let mut best: Option<(u32, u32, u32, u32, u32)> = None;
533 for h in 1..=band_rows {
534 let (r0, r1) = match vmode {
535 AxisMode::Start => (r_lo, r_lo + h),
536 AxisMode::End => (r_hi - h, r_hi),
537 AxisMode::Center => {
538 let r0 = r_lo + (band_rows - h) / 2;
539 (r0, r0 + h)
540 }
541 };
542 let col_free: Vec<bool> = (c_lo..c_hi).map(|c| (r0..r1).all(|r| free(r, c))).collect();
543 let (off, w) = run_for_mode(&col_free, hmode);
544 if w == 0 {
545 continue;
546 }
547 let area = w * h;
548 if best.is_none_or(|b| area > b.0) {
549 best = Some((area, r0, r1, c_lo + off, c_lo + off + w));
550 }
551 }
552 best.map(|(_, r0, r1, c0, c1)| (r0, r1, c0, c1))
553 }
554
555 fn largest_free_rect_in(
561 &self,
562 c_lo: u32,
563 c_hi: u32,
564 r_lo: u32,
565 r_hi: u32,
566 hmode: AxisMode,
567 vmode: AxisMode,
568 ) -> Option<(u32, u32, u32, u32)> {
569 Self::best_anchored_rect(c_lo, c_hi, r_lo, r_hi, hmode, vmode, |r, c| {
570 self.is_free(r, c)
571 })
572 }
573
574 fn largest_free_rect(&self, hmode: AxisMode, vmode: AxisMode) -> Option<(u32, u32, u32, u32)> {
581 self.largest_free_rect_in(0, self.cols, 0, self.rows, hmode, vmode)
582 }
583
584 fn anchor_cell(&self, anchor: PlacementSlot) -> (u32, u32) {
586 let col = match anchor.horizontal() {
587 AxisMode::Start => 0,
588 AxisMode::Center => self.img_width / 2 / self.cell_size,
589 AxisMode::End => self.img_width.saturating_sub(1) / self.cell_size,
590 }
591 .min(self.cols.saturating_sub(1));
592 let row = match anchor.vertical() {
593 AxisMode::Start => 0,
594 AxisMode::Center => self.img_height / 2 / self.cell_size,
595 AxisMode::End => self.img_height.saturating_sub(1) / self.cell_size,
596 }
597 .min(self.rows.saturating_sub(1));
598 (row, col)
599 }
600
601 fn local_occupied_fraction(&self, anchor: PlacementSlot) -> f32 {
604 if self.cols == 0 || self.rows == 0 {
605 return 0f32;
606 }
607 let (hi, vi) = anchor.cell();
608 let (c0, c1) = self.col_band(u32::from(hi));
609 let (r0, r1) = self.row_band(u32::from(vi));
610 let mut total = 0u32;
611 let mut occ = 0u32;
612 for r in r0..r1 {
613 for c in c0..c1 {
614 total += 1;
615 if !self.is_free(r, c) {
616 occ += 1;
617 }
618 }
619 }
620 if total == 0 {
621 0f32
622 } else {
623 f32::from(u16::try_from(occ).unwrap_or(u16::MAX))
624 / f32::from(u16::try_from(total).unwrap_or(u16::MAX))
625 }
626 }
627
628 fn free_components(&self) -> Vec<i32> {
631 let mut comp = vec![-1i32; (self.cols * self.rows) as usize];
632 let mut next = 0i32;
633 for start_row in 0..self.rows {
634 for start_col in 0..self.cols {
635 if !self.is_free(start_row, start_col) {
636 continue;
637 }
638 let start_idx = (start_row * self.cols + start_col) as usize;
639 if comp.get(start_idx).copied().unwrap_or(0) != -1 {
640 continue;
641 }
642 if let Some(cell) = comp.get_mut(start_idx) {
643 *cell = next;
644 }
645 let mut stack = vec![(start_row, start_col)];
646 while let Some((r, c)) = stack.pop() {
647 let mut neighbours: Vec<(u32, u32)> = Vec::with_capacity(4);
648 if r > 0 {
649 neighbours.push((r - 1, c));
650 }
651 if r + 1 < self.rows {
652 neighbours.push((r + 1, c));
653 }
654 if c > 0 {
655 neighbours.push((r, c - 1));
656 }
657 if c + 1 < self.cols {
658 neighbours.push((r, c + 1));
659 }
660 for (rr, cc) in neighbours {
661 let i = (rr * self.cols + cc) as usize;
662 if self.is_free(rr, cc) && comp.get(i).copied().unwrap_or(0) == -1 {
663 if let Some(cell) = comp.get_mut(i) {
664 *cell = next;
665 }
666 stack.push((rr, cc));
667 }
668 }
669 }
670 next += 1;
671 }
672 }
673 comp
674 }
675
676 fn anchor_component(&self, anchor: PlacementSlot, comp: &[i32]) -> Option<i32> {
679 if self.cols == 0 || self.rows == 0 {
680 return None;
681 }
682 let (row, col) = self.anchor_cell(anchor);
683 let id = comp
684 .get((row * self.cols + col) as usize)
685 .copied()
686 .unwrap_or(-1);
687 if id < 0 { None } else { Some(id) }
688 }
689
690 fn connected_neighbours(&self, anchor: PlacementSlot, comp: &[i32]) -> Vec<PlacementSlot> {
693 let Some(my) = self.anchor_component(anchor, comp) else {
694 return Vec::new();
695 };
696 anchor
697 .neighbours()
698 .into_iter()
699 .filter(|&nb| self.anchor_component(nb, comp) == Some(my))
700 .collect()
701 }
702
703 #[must_use]
705 pub fn evaluate_slots(&self) -> Vec<PlacementSlotInfo> {
706 let components = self.free_components();
707 PlacementSlot::ALL
708 .into_iter()
709 .map(|anchor| {
710 let (h3, v3) = anchor.cell();
711 let (c_lo, c_hi) = self.col_band(u32::from(h3));
712 let (r_lo, r_hi) = self.row_band(u32::from(v3));
713 let free = self.largest_free_rect_in(
714 c_lo,
715 c_hi,
716 r_lo,
717 r_hi,
718 anchor.horizontal(),
719 anchor.vertical(),
720 );
721 let free_rect =
722 free.map(|(r0, r1, c0, c1)| self.cell_rect_to_pixels(r0, r1, c0, c1));
723 let free_size = free_rect.map_or((0, 0), |r| (r.width, r.height));
724 let available = free_size.0 > 0 && free_size.1 > 0;
725 PlacementSlotInfo {
726 slot: anchor,
727 available,
728 free_rect,
729 free_size,
730 occupied_fraction: self.local_occupied_fraction(anchor),
731 connected_neighbours: self.connected_neighbours(anchor, &components),
732 }
733 })
734 .collect()
735 }
736
737 #[must_use]
750 pub fn spanned_region(&self, anchor: PlacementSlot) -> Option<(Vec<PlacementSlot>, PixelRect)> {
751 let components = self.free_components();
752 let my = self.anchor_component(anchor, &components)?;
753 let slots: Vec<PlacementSlot> = PlacementSlot::ALL
754 .into_iter()
755 .filter(|&slot| self.anchor_component(slot, &components) == Some(my))
756 .collect();
757 let (r0, r1, c0, c1) = self.largest_free_rect(anchor.horizontal(), anchor.vertical())?;
758 Some((slots, self.cell_rect_to_pixels(r0, r1, c0, c1)))
759 }
760
761 #[must_use]
774 pub fn subset_rect(&self, slots: &[PlacementSlot]) -> Option<PixelRect> {
775 if self.cols == 0 || self.rows == 0 || slots.is_empty() {
776 return None;
777 }
778 let mut allowed = vec![false; (self.cols * self.rows) as usize];
781 let (mut c_lo, mut c_hi, mut r_lo, mut r_hi) = (self.cols, 0u32, self.rows, 0u32);
782 let (mut has_left, mut has_right, mut has_top, mut has_bottom) =
783 (false, false, false, false);
784 for &s in slots {
785 let (h3, v3) = s.cell();
786 match h3 {
787 0 => has_left = true,
788 2 => has_right = true,
789 _ => {}
790 }
791 match v3 {
792 0 => has_top = true,
793 2 => has_bottom = true,
794 _ => {}
795 }
796 let (cl, ch) = self.col_band(u32::from(h3));
797 let (rl, rh) = self.row_band(u32::from(v3));
798 c_lo = c_lo.min(cl);
799 c_hi = c_hi.max(ch);
800 r_lo = r_lo.min(rl);
801 r_hi = r_hi.max(rh);
802 for r in rl..rh {
803 for c in cl..ch {
804 if let Some(cell) = allowed.get_mut((r * self.cols + c) as usize) {
805 *cell = true;
806 }
807 }
808 }
809 }
810 if c_lo >= c_hi || r_lo >= r_hi {
811 return None;
812 }
813 let free_allowed = |r: u32, c: u32| {
814 self.is_free(r, c)
815 && allowed
816 .get((r * self.cols + c) as usize)
817 .copied()
818 .unwrap_or(false)
819 };
820 let hmode = match (has_left, has_right) {
824 (true, false) => AxisMode::Start,
825 (false, true) => AxisMode::End,
826 _ => AxisMode::Center,
827 };
828 let vmode = match (has_top, has_bottom) {
829 (true, false) => AxisMode::Start,
830 (false, true) => AxisMode::End,
831 _ => AxisMode::Center,
832 };
833 Self::best_anchored_rect(c_lo, c_hi, r_lo, r_hi, hmode, vmode, free_allowed)
834 .map(|(r0, r1, c0, c1)| self.cell_rect_to_pixels(r0, r1, c0, c1))
835 }
836}
837
838#[cfg(test)]
839mod test {
840 use super::*;
841
842 fn grid_from_cells(cols: u32, rows: u32, cell_size: u32, occupied: Vec<bool>) -> OccupancyGrid {
846 assert_eq!(occupied.len(), (cols * rows) as usize);
847 OccupancyGrid {
848 cols,
849 rows,
850 cell_size,
851 img_width: cols * cell_size,
852 img_height: rows * cell_size,
853 occupied,
854 }
855 }
856
857 fn slot(
858 slots: &[PlacementSlotInfo],
859 anchor: PlacementSlot,
860 ) -> Result<PlacementSlotInfo, String> {
861 slots
862 .iter()
863 .find(|s| s.slot == anchor)
864 .cloned()
865 .ok_or_else(|| format!("anchor {anchor:?} should be evaluated"))
866 }
867
868 #[test]
869 fn empty_grid_confines_each_slot_to_its_third() -> Result<(), Box<dyn std::error::Error>> {
870 let grid = grid_from_cells(8, 8, 10, vec![false; 64]);
873 let slots = grid.evaluate_slots();
874 assert_eq!(slots.len(), 9);
875 for s in &slots {
876 assert!(s.available, "{:?} should be available", s.slot);
877 assert!(s.occupied_fraction.abs() < f32::EPSILON);
878 }
879 let size = |a| slot(&slots, a).map(|s| s.free_size);
882 assert_eq!(size(PlacementSlot::TopLeft)?, (20, 20));
883 assert_eq!(size(PlacementSlot::TopCenter)?, (40, 20));
884 assert_eq!(size(PlacementSlot::TopRight)?, (20, 20));
885 assert_eq!(size(PlacementSlot::MiddleLeft)?, (20, 40));
886 assert_eq!(size(PlacementSlot::Center)?, (40, 40));
887 assert_eq!(size(PlacementSlot::BottomRight)?, (20, 20));
888 Ok(())
889 }
890
891 #[test]
892 fn empty_grid_slot_rects_tile_without_overlap() -> Result<(), Box<dyn std::error::Error>> {
893 let grid = grid_from_cells(8, 8, 10, vec![false; 64]);
896 let rects: Vec<PixelRect> = grid
897 .evaluate_slots()
898 .into_iter()
899 .filter_map(|s| s.free_rect)
900 .collect();
901 assert_eq!(rects.len(), 9);
902 let overlaps = |a: &PixelRect, b: &PixelRect| {
903 a.x < b.x + b.width
904 && b.x < a.x + a.width
905 && a.y < b.y + b.height
906 && b.y < a.y + a.height
907 };
908 for (i, a) in rects.iter().enumerate() {
909 for b in rects.iter().skip(i + 1) {
910 assert!(!overlaps(a, b), "{a:?} overlaps {b:?}");
911 }
912 }
913 let total: u32 = rects.iter().map(|r| r.width * r.height).sum();
914 assert_eq!(total, 80 * 80, "the nine thirds tile the whole image");
915 Ok(())
916 }
917
918 #[test]
919 fn full_grid_no_slot_available() {
920 let grid = grid_from_cells(8, 8, 10, vec![true; 64]);
921 let slots = grid.evaluate_slots();
922 for s in &slots {
923 assert!(!s.available, "{:?} should not be available", s.slot);
924 assert_eq!(s.free_rect, None);
925 assert_eq!(s.free_size, (0, 0));
926 assert!(s.connected_neighbours.is_empty());
927 }
928 }
929
930 #[test]
931 fn central_vertical_stripe_frees_the_sides() -> Result<(), Box<dyn std::error::Error>> {
932 let occupied: Vec<bool> = (0..64u32).map(|i| matches!(i % 8, 3 | 4)).collect();
934 let grid = grid_from_cells(8, 8, 10, occupied);
935 let slots = grid.evaluate_slots();
936 assert!(slot(&slots, PlacementSlot::MiddleLeft)?.available);
937 assert!(slot(&slots, PlacementSlot::MiddleRight)?.available);
938 assert!(!slot(&slots, PlacementSlot::Center)?.available);
940 assert_eq!(slot(&slots, PlacementSlot::MiddleLeft)?.free_size.0, 20);
942 assert_eq!(slot(&slots, PlacementSlot::MiddleRight)?.free_size.0, 20);
943 Ok(())
944 }
945
946 #[test]
947 fn horizontal_mirror_swaps_left_and_right() -> Result<(), Box<dyn std::error::Error>> {
948 let occupied: Vec<bool> = (0..64u32).map(|i| i == 9).collect();
950 let grid = grid_from_cells(8, 8, 10, occupied);
951 let slots = grid.evaluate_slots();
952 let top_left = slot(&slots, PlacementSlot::TopLeft)?.free_size;
953 assert_ne!(top_left, (0, 0), "the top-left third still has free space");
954
955 let mirrored: Vec<bool> = (0..64u32).map(|i| i == 14).collect();
959 let mirrored_grid = grid_from_cells(8, 8, 10, mirrored);
960 let mirrored_slots = mirrored_grid.evaluate_slots();
961 let top_right = slot(&mirrored_slots, PlacementSlot::TopRight)?.free_size;
962
963 assert_eq!(top_left, top_right);
964 Ok(())
965 }
966
967 #[test]
968 fn adjacency_links_free_neighbours_across_free_band() -> Result<(), Box<dyn std::error::Error>>
969 {
970 let grid = grid_from_cells(9, 9, 10, vec![false; 81]);
972 let slots = grid.evaluate_slots();
973 let center = slot(&slots, PlacementSlot::Center)?;
974 let mut neighbours = center.connected_neighbours.clone();
975 neighbours.sort_by_key(|a| format!("{a:?}"));
976 let mut expected = PlacementSlot::Center.neighbours();
977 expected.sort_by_key(|a| format!("{a:?}"));
978 assert_eq!(neighbours, expected);
979 Ok(())
980 }
981
982 #[test]
983 fn adjacency_broken_by_occupied_band() -> Result<(), Box<dyn std::error::Error>> {
984 let occupied: Vec<bool> = (0..81u32).map(|i| i % 9 == 4).collect();
987 let grid = grid_from_cells(9, 9, 10, occupied);
988 let slots = grid.evaluate_slots();
989 let top_left = slot(&slots, PlacementSlot::TopLeft)?;
992 assert!(
993 !top_left
994 .connected_neighbours
995 .contains(&PlacementSlot::TopCenter)
996 );
997 assert!(
999 top_left
1000 .connected_neighbours
1001 .contains(&PlacementSlot::MiddleLeft)
1002 );
1003 Ok(())
1004 }
1005
1006 #[test]
1007 fn slots_form_rectangle_accepts_solid_blocks_only() {
1008 use PlacementSlot::{
1009 BottomCenter, BottomLeft, BottomRight, Center, MiddleLeft, TopCenter, TopLeft, TopRight,
1010 };
1011 assert!(PlacementSlot::slots_form_rectangle(&[]));
1013 assert!(PlacementSlot::slots_form_rectangle(&[TopLeft]));
1014 assert!(PlacementSlot::slots_form_rectangle(&[
1016 TopLeft, TopCenter, TopRight
1017 ]));
1018 assert!(PlacementSlot::slots_form_rectangle(&[TopLeft, MiddleLeft]));
1019 assert!(PlacementSlot::slots_form_rectangle(&[
1020 TopLeft, TopCenter, MiddleLeft, Center
1021 ]));
1022 assert!(PlacementSlot::slots_form_rectangle(&PlacementSlot::ALL));
1024 assert!(PlacementSlot::slots_form_rectangle(&[
1025 TopLeft, MiddleLeft, BottomLeft
1026 ]));
1027 assert!(PlacementSlot::slots_form_rectangle(&[TopLeft, TopLeft]));
1029 assert!(!PlacementSlot::slots_form_rectangle(&[
1031 TopLeft,
1032 BottomRight
1033 ]));
1034 assert!(!PlacementSlot::slots_form_rectangle(&[
1035 TopLeft, TopCenter, MiddleLeft
1036 ]));
1037 assert!(!PlacementSlot::slots_form_rectangle(&[TopLeft, BottomLeft]));
1038 assert!(!PlacementSlot::slots_form_rectangle(&[
1039 TopLeft,
1040 BottomCenter
1041 ]));
1042 }
1043
1044 #[test]
1045 fn neighbours_are_orthogonal_only() {
1046 assert_eq!(
1047 PlacementSlot::TopLeft.neighbours(),
1048 vec![PlacementSlot::TopCenter, PlacementSlot::MiddleLeft]
1049 );
1050 let center = PlacementSlot::Center.neighbours();
1051 assert_eq!(center.len(), 4);
1052 assert!(!center.contains(&PlacementSlot::TopLeft));
1053 }
1054
1055 #[test]
1056 fn as_str_and_from_str_round_trip() {
1057 for slot in PlacementSlot::ALL {
1058 assert_eq!(slot.as_str().parse::<PlacementSlot>().ok(), Some(slot));
1059 assert_eq!(slot.to_string(), slot.as_str());
1061 }
1062 assert_eq!("nonsense".parse::<PlacementSlot>().ok(), None);
1063 }
1064
1065 #[test]
1066 fn default_alignment_matches_slot_outward() {
1067 assert_eq!(
1068 PlacementSlot::TopLeft.default_alignment(),
1069 (HAlign::Left, VAlign::Top)
1070 );
1071 assert_eq!(
1072 PlacementSlot::TopCenter.default_alignment(),
1073 (HAlign::Center, VAlign::Top)
1074 );
1075 assert_eq!(
1076 PlacementSlot::MiddleRight.default_alignment(),
1077 (HAlign::Right, VAlign::Center)
1078 );
1079 assert_eq!(
1080 PlacementSlot::Center.default_alignment(),
1081 (HAlign::Center, VAlign::Center)
1082 );
1083 assert_eq!(
1084 PlacementSlot::BottomRight.default_alignment(),
1085 (HAlign::Right, VAlign::Bottom)
1086 );
1087 }
1088
1089 #[test]
1090 fn align_offset_clamps_and_centres() {
1091 assert_eq!(HAlign::Left.offset(20, 100), 0);
1093 assert_eq!(HAlign::Center.offset(20, 100), 40);
1094 assert_eq!(HAlign::Right.offset(20, 100), 80);
1095 assert_eq!(VAlign::Bottom.offset(30, 100), 70);
1096 assert_eq!(HAlign::Right.offset(120, 100), 0);
1098 assert_eq!(VAlign::Center.offset(120, 100), 0);
1099 }
1100
1101 #[test]
1102 fn anchored_origin_hugs_edges_and_centres() {
1103 assert_eq!(
1105 PlacementSlot::TopLeft.anchored_origin(40, 20, 200, 100, 8),
1106 (8, 8)
1107 );
1108 assert_eq!(
1109 PlacementSlot::TopRight.anchored_origin(40, 20, 200, 100, 8),
1110 (200 - 40 - 8, 8)
1111 );
1112 assert_eq!(
1113 PlacementSlot::BottomRight.anchored_origin(40, 20, 200, 100, 8),
1114 (200 - 40 - 8, 100 - 20 - 8)
1115 );
1116 assert_eq!(
1117 PlacementSlot::Center.anchored_origin(40, 20, 200, 100, 8),
1118 ((200 - 40) / 2, (100 - 20) / 2)
1119 );
1120 assert_eq!(
1121 PlacementSlot::TopCenter.anchored_origin(40, 20, 200, 100, 8),
1122 ((200 - 40) / 2, 8)
1123 );
1124 assert_eq!(
1126 PlacementSlot::BottomRight.anchored_origin(400, 200, 200, 100, 8),
1127 (0, 0)
1128 );
1129 }
1130
1131 #[test]
1132 fn spanned_region_covers_whole_free_bottom_edge() -> Result<(), Box<dyn std::error::Error>> {
1133 let occupied: Vec<bool> = (0..27u32).map(|i| i / 9 != 2).collect();
1135 let grid = grid_from_cells(9, 3, 10, occupied);
1136 let (mut slots, rect) = grid
1137 .spanned_region(PlacementSlot::BottomCenter)
1138 .ok_or("bottom_center anchor is free, must span")?;
1139 slots.sort_by_key(|a| format!("{a:?}"));
1140 let mut expected = vec![
1141 PlacementSlot::BottomLeft,
1142 PlacementSlot::BottomCenter,
1143 PlacementSlot::BottomRight,
1144 ];
1145 expected.sort_by_key(|a| format!("{a:?}"));
1146 assert_eq!(slots, expected, "all three bottom slots are reserved");
1147 assert_eq!(rect.width, 90);
1149 assert_eq!(rect.height, 10);
1150 Ok(())
1151 }
1152
1153 #[test]
1154 fn spanned_region_none_when_anchor_covered() {
1155 let occupied: Vec<bool> = (0..27u32).map(|i| i / 9 == 2).collect();
1157 let grid = grid_from_cells(9, 3, 10, occupied);
1158 assert_eq!(grid.spanned_region(PlacementSlot::BottomCenter), None);
1159 }
1160
1161 #[test]
1162 fn spanned_region_stops_at_occupied_band() -> Result<(), Box<dyn std::error::Error>> {
1163 let occupied: Vec<bool> = (0..27u32)
1166 .map(|i| {
1167 let (row, col) = (i / 9, i % 9);
1168 !(row == 2 && col < 2)
1169 })
1170 .collect();
1171 let grid = grid_from_cells(9, 3, 10, occupied);
1172 let (slots, _rect) = grid
1173 .spanned_region(PlacementSlot::BottomLeft)
1174 .ok_or("bottom_left anchor is free")?;
1175 assert!(slots.contains(&PlacementSlot::BottomLeft));
1176 assert!(!slots.contains(&PlacementSlot::BottomRight));
1177 Ok(())
1178 }
1179
1180 #[test]
1181 fn centre_third_absorbs_remainder_keeping_edges_equal() -> Result<(), Box<dyn std::error::Error>>
1182 {
1183 let grid = grid_from_cells(11, 11, 10, vec![false; 121]);
1187 let slots = grid.evaluate_slots();
1188 let size = |a| slot(&slots, a).map(|s| s.free_size);
1189 assert_eq!(size(PlacementSlot::TopLeft)?, (30, 30));
1190 assert_eq!(size(PlacementSlot::TopRight)?, (30, 30));
1191 assert_eq!(size(PlacementSlot::BottomLeft)?, (30, 30));
1192 assert_eq!(size(PlacementSlot::BottomRight)?, (30, 30));
1193 assert_eq!(size(PlacementSlot::Center)?, (50, 50));
1194 Ok(())
1195 }
1196
1197 #[test]
1198 fn spanned_region_uses_minimum_perpendicular_extent() -> Result<(), Box<dyn std::error::Error>>
1199 {
1200 let occupied: Vec<bool> = (0..54u32)
1206 .map(|i| {
1207 let (row, col) = (i / 9, i % 9);
1208 let free = if col < 3 { row < 4 } else { row < 2 };
1209 !free
1210 })
1211 .collect();
1212 let grid = grid_from_cells(9, 6, 10, occupied);
1213 let (_slots, rect) = grid
1214 .spanned_region(PlacementSlot::TopLeft)
1215 .ok_or("top_left anchor is free")?;
1216 assert_eq!(rect.width, 90, "the span grows across the full width");
1217 assert_eq!(
1218 rect.height, 20,
1219 "the span is clipped to the shallower thirds"
1220 );
1221 Ok(())
1222 }
1223
1224 #[test]
1225 fn subset_rect_spans_two_adjacent_slots() -> Result<(), Box<dyn std::error::Error>> {
1226 let grid = grid_from_cells(9, 9, 10, vec![false; 81]);
1228 let rect = grid
1230 .subset_rect(&[PlacementSlot::TopLeft, PlacementSlot::TopCenter])
1231 .ok_or("the two free top slots have a combined rect")?;
1232 assert_eq!(
1233 (rect.x, rect.y, rect.width, rect.height),
1234 (0, 0, 60, 30),
1235 "the combined rect fills both thirds"
1236 );
1237 Ok(())
1238 }
1239
1240 #[test]
1241 fn subset_rect_confined_to_chosen_slots_only() -> Result<(), Box<dyn std::error::Error>> {
1242 let grid = grid_from_cells(9, 9, 10, vec![false; 81]);
1246 let rect = grid
1247 .subset_rect(&[
1248 PlacementSlot::TopLeft,
1249 PlacementSlot::TopCenter,
1250 PlacementSlot::MiddleLeft,
1251 ])
1252 .ok_or("the L-shaped union has a free rect")?;
1253 assert_eq!((rect.x, rect.y, rect.width, rect.height), (0, 0, 60, 30));
1256 Ok(())
1257 }
1258
1259 #[test]
1260 fn subset_rect_none_when_union_fully_occupied() -> Result<(), Box<dyn std::error::Error>> {
1261 let occupied: Vec<bool> = (0..81u32)
1263 .map(|i| {
1264 let (row, col) = (i / 9, i % 9);
1265 row < 3 && col < 6
1266 })
1267 .collect();
1268 let grid = grid_from_cells(9, 9, 10, occupied);
1269 assert!(
1270 grid.subset_rect(&[PlacementSlot::TopLeft, PlacementSlot::TopCenter])
1271 .is_none(),
1272 "a fully covered union has no combined rect"
1273 );
1274 Ok(())
1275 }
1276
1277 #[test]
1278 fn subset_rect_hugs_the_edge_the_group_touches() -> Result<(), Box<dyn std::error::Error>> {
1279 let occupied: Vec<bool> = (0..81u32)
1283 .map(|i| {
1284 let (row, col) = (i / 9, i % 9);
1285 col == 3 && row >= 6
1286 })
1287 .collect();
1288 let grid = grid_from_cells(9, 9, 10, occupied);
1289 let rect = grid
1290 .subset_rect(&[PlacementSlot::BottomCenter, PlacementSlot::BottomRight])
1291 .ok_or("the bottom centre+right union has a free rect")?;
1292 assert_eq!(
1293 (rect.x, rect.y, rect.width, rect.height),
1294 (40, 60, 50, 30),
1295 "the rect hugs the right edge instead of centring"
1296 );
1297 assert_eq!(rect.x + rect.width, 90);
1299 Ok(())
1300 }
1301
1302 mod with_map {
1303 use super::*;
1304 use crate::map_tiles::Map;
1305 use image::GenericImageView as _;
1306 use sl_types::map::{GridCoordinates, GridRectangle, ZoomLevel};
1307
1308 fn test_rectangle() -> GridRectangle {
1309 GridRectangle::new(
1311 GridCoordinates::new(1000, 1000),
1312 GridCoordinates::new(1003, 1003),
1313 )
1314 }
1315
1316 #[test]
1317 fn blank_dimensions_match_zoom_times_regions() -> Result<(), Box<dyn std::error::Error>> {
1318 let zoom = ZoomLevel::try_new(4)?;
1319 let map = Map::blank(test_rectangle(), zoom);
1320 let expected = u32::from(zoom.pixels_per_region()) * 4;
1322 assert_eq!(map.dimensions(), (expected, expected));
1323 assert_eq!(map.dimensions(), (128, 128));
1324 Ok(())
1325 }
1326
1327 #[test]
1328 fn blank_fit_matches_real_render_sizing() -> Result<(), Box<dyn std::error::Error>> {
1329 let rect = test_rectangle();
1330 let map = Map::blank_fit(rect.clone(), 200, 200)?;
1331 let zoom = ZoomLevel::max_zoom_level_to_fit_regions_into_output_image(4, 4, 200, 200)?;
1332 let expected = u32::from(zoom.pixels_per_region()) * 4;
1333 assert_eq!(map.dimensions(), (expected, expected));
1334 Ok(())
1335 }
1336
1337 #[test]
1338 fn stamped_rectangle_blocks_its_corner() -> Result<(), Box<dyn std::error::Error>> {
1339 let mut map = Map::blank(test_rectangle(), ZoomLevel::try_new(4)?);
1340 imageproc::drawing::draw_filled_rect_mut(
1342 map.image_mut(),
1343 imageproc::rect::Rect::at(0, 0).of_size(40, 40),
1344 image::Rgba([255, 0, 0, 255]),
1345 );
1346 let grid = OccupancyGrid::from_map(&map, DEFAULT_COVERAGE_GRID);
1347 let slots = grid.evaluate_slots();
1348 assert!(!slot(&slots, PlacementSlot::TopLeft)?.available);
1350 assert!(slot(&slots, PlacementSlot::TopLeft)?.occupied_fraction > 0f32);
1351 let bottom_right = slot(&slots, PlacementSlot::BottomRight)?;
1355 assert!(bottom_right.available);
1356 assert!(bottom_right.free_size.0 >= 40);
1357 assert!(bottom_right.free_size.1 >= 40);
1358 Ok(())
1359 }
1360
1361 #[test]
1362 fn diagonal_route_blocks_centre_but_not_off_diagonal_corner()
1363 -> Result<(), Box<dyn std::error::Error>> {
1364 let mut map = Map::blank(test_rectangle(), ZoomLevel::try_new(4)?);
1365 map.draw_pixel_waypoint_route(
1366 &[(10f32, 10f32), (64f32, 64f32), (118f32, 118f32)],
1367 image::Rgba([0, 0, 255, 255]),
1368 )?;
1369 let grid = OccupancyGrid::from_map(&map, DEFAULT_COVERAGE_GRID);
1370 let slots = grid.evaluate_slots();
1371 assert!(!slot(&slots, PlacementSlot::Center)?.available);
1373 assert!(slot(&slots, PlacementSlot::TopRight)?.available);
1375 Ok(())
1376 }
1377 }
1378}