1use crate::types::{Grid, Point, Rect};
8
9pub fn centred(centre: Point, width: f64, height: f64, grid: Grid) -> Rect {
12 grid.snap_rect(Rect::new(
13 centre.x - width / 2.0,
14 centre.y - height / 2.0,
15 width,
16 height,
17 ))
18}
19
20pub fn beside(at: Point, source: Rect, width: f64, height: f64, grid: Grid) -> Rect {
29 let centre = source.center();
30 let away = Point::new(at.x - centre.x, at.y - centre.y);
31 let frame = if away.x.abs() >= away.y.abs() {
32 Rect::new(
33 if away.x >= 0.0 { at.x } else { at.x - width },
34 at.y - height / 2.0,
35 width,
36 height,
37 )
38 } else {
39 Rect::new(
40 at.x - width / 2.0,
41 if away.y >= 0.0 { at.y } else { at.y - height },
42 width,
43 height,
44 )
45 };
46 grid.snap_rect(frame)
47}
48
49pub fn pushed_clear(frame: Rect, source: Rect, gap: f64, grid: Grid) -> Rect {
54 if !source.expanded(gap).intersects(frame) {
55 return frame;
56 }
57 let centre = frame.center();
58 let away = Point::new(centre.x - source.center().x, centre.y - source.center().y);
59 let mut frame = frame;
60 if away.x.abs() >= away.y.abs() {
61 frame.x = if away.x >= 0.0 {
62 source.x + source.width + gap
63 } else {
64 source.x - frame.width - gap
65 };
66 frame.y = grid.snap(centre.y - frame.height / 2.0);
67 } else {
68 frame.y = if away.y >= 0.0 {
69 source.y + source.height + gap
70 } else {
71 source.y - frame.height - gap
72 };
73 frame.x = grid.snap(centre.x - frame.width / 2.0);
74 }
75 frame
76}
77
78pub fn settled_below(frame: Rect, occupied: &[Rect], gap: f64, grid: Grid) -> Point {
86 let mut frame = frame;
87 for _ in 0..=occupied.len() {
88 let Some(below) = occupied
89 .iter()
90 .filter(|other| other.expanded(gap).intersects(frame))
91 .map(|other| other.y + other.height + gap)
92 .reduce(f64::max)
93 else {
94 break;
95 };
96 if below <= frame.y {
100 break;
101 }
102 frame.y = below;
103 }
104 grid.snap_point(Point::new(frame.x, frame.y))
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 const GRID: Grid = Grid::new(12.0);
112
113 #[test]
114 fn a_clear_spot_is_used_exactly() {
115 let frame = centred(Point::new(0.0, 0.0), 216.0, 48.0, GRID);
116 assert_eq!(
117 settled_below(frame, &[], 48.0, GRID),
118 Point::new(frame.x, frame.y)
119 );
120 }
121
122 #[test]
123 fn a_taken_spot_is_stepped_past_and_the_result_clears_everything() {
124 let taken = [
125 Rect::new(-108.0, -24.0, 216.0, 48.0),
126 Rect::new(-108.0, 96.0, 216.0, 48.0),
127 ];
128 let frame = centred(Point::new(0.0, 0.0), 216.0, 48.0, GRID);
129 let at = settled_below(frame, &taken, 48.0, GRID);
130 let placed = Rect::new(at.x, at.y, frame.width, frame.height);
131 assert!(taken.iter().all(|other| !other.intersects(placed)));
132 assert!(at.y > frame.y, "it should have moved down, not up");
133 }
134
135 #[test]
139 fn a_tall_obstacle_is_cleared_however_much_taller_than_the_new_item_it_is() {
140 let tall = [Rect::new(0.0, 0.0, 216.0, 1200.0)];
141 let frame = centred(Point::new(108.0, 24.0), 216.0, 48.0, GRID);
142 let at = settled_below(frame, &tall, 12.0, GRID);
143 let placed = Rect::new(at.x, at.y, frame.width, frame.height);
144
145 assert!(
146 !tall[0].intersects(placed),
147 "{placed:?} is still inside {:?}",
148 tall[0]
149 );
150 }
151
152 #[test]
153 fn a_drop_beside_its_source_is_pushed_off_the_source() {
154 let source = Rect::new(0.0, 0.0, 216.0, 48.0);
155 let gap = 48.0;
156 for towards in [
157 Point::new(240.0, 10.0),
158 Point::new(-240.0, 10.0),
159 Point::new(10.0, 240.0),
160 Point::new(10.0, -240.0),
161 ] {
162 let frame = centred(towards, 216.0, 48.0, GRID);
163 let pushed = pushed_clear(frame, source, gap, GRID);
164 assert!(
165 !source.expanded(gap - 1.0).intersects(pushed),
166 "a drop towards {towards:?} stayed on its source",
167 );
168 }
169 }
170
171 #[test]
172 fn a_drop_already_clear_of_its_source_is_left_where_it_is() {
173 let source = Rect::new(0.0, 0.0, 216.0, 48.0);
174 let frame = centred(Point::new(600.0, 600.0), 216.0, 48.0, GRID);
175 assert_eq!(pushed_clear(frame, source, 48.0, GRID), frame);
176 }
177
178 #[test]
181 fn a_new_item_grows_away_from_the_drop_rather_than_around_it() {
182 let source = Rect::new(0.0, 0.0, 216.0, 48.0);
183 let (width, height) = (216.0, 48.0);
184 for (at, edge) in [
185 (Point::new(600.0, 24.0), "left"),
186 (Point::new(-600.0, 24.0), "right"),
187 (Point::new(108.0, 600.0), "top"),
188 (Point::new(108.0, -600.0), "bottom"),
189 ] {
190 let frame = beside(at, source, width, height, GRID);
191 let touching = match edge {
192 "left" => frame.x,
193 "right" => frame.x + frame.width,
194 "top" => frame.y,
195 _ => frame.y + frame.height,
196 };
197 let along = if matches!(edge, "left" | "right") {
198 at.x
199 } else {
200 at.y
201 };
202 assert!(
203 (touching - along).abs() <= GRID.size() / 2.0,
204 "dropping at {at:?} put the {edge} edge at {touching}, not {along}"
205 );
206 assert!(
207 !source.intersects(frame),
208 "dropping at {at:?} landed on the card it came from"
209 );
210 }
211 }
212}