1use std::collections::{BTreeMap, BTreeSet};
13
14use crate::types::{Grid, Id, Point, Rect};
15
16pub fn clash(a: Rect, b: Rect, gap: f64) -> bool {
24 a.x < b.x + b.width + gap
25 && b.x < a.x + a.width + gap
26 && a.y < b.y + b.height + gap
27 && b.y < a.y + a.height + gap
28}
29
30pub fn settled(
43 frames: &BTreeMap<Id, Rect>,
44 moving: &BTreeSet<Id>,
45 gap: f64,
46 grid: Grid,
47) -> BTreeMap<Id, Rect> {
48 let gap = if gap.is_finite() { gap.max(0.0) } else { 0.0 };
49 let mut answer = frames.clone();
50
51 relocate_group(&mut answer, moving, gap, grid);
56
57 for _ in 0..=frames.len() {
61 let Some((keeper, displaced)) = first_collision(&answer, moving, gap) else {
62 break;
63 };
64 let start = answer[&displaced];
65 let occupied: Vec<Rect> = answer
66 .iter()
67 .filter(|(id, _)| *id != &displaced)
68 .map(|(_, frame)| *frame)
69 .collect();
70 let next = nearest_clear(start, answer[&keeper], &occupied, gap, grid);
71 debug_assert_ne!(
72 next, start,
73 "a clashing frame must have somewhere to settle"
74 );
75 answer.insert(displaced, next);
76 }
77
78 debug_assert!(clear(&answer, gap), "layout settlement left an overlap");
79 answer
80}
81
82fn relocate_group(frames: &mut BTreeMap<Id, Rect>, moving: &BTreeSet<Id>, gap: f64, grid: Grid) {
85 let group: Vec<Rect> = frames
86 .iter()
87 .filter(|(id, _)| moving.contains(*id))
88 .map(|(_, frame)| *frame)
89 .collect();
90 let fixed: Vec<Rect> = frames
91 .iter()
92 .filter(|(id, _)| !moving.contains(*id))
93 .map(|(_, frame)| *frame)
94 .collect();
95 if group.is_empty() || !clear_frames(&group, gap) || groups_clear(&group, &fixed, gap) {
96 return;
97 }
98 let Some(offset) = nearest_group_translation(&group, &fixed, gap, grid) else {
99 return;
100 };
101 debug_assert!(offset.x != 0.0 || offset.y != 0.0);
102 for (id, frame) in frames.iter_mut() {
103 if moving.contains(id) {
104 *frame = translated(*frame, offset);
105 }
106 }
107}
108
109fn nearest_group_translation(
112 group: &[Rect],
113 fixed: &[Rect],
114 gap: f64,
115 grid: Grid,
116) -> Option<Point> {
117 let mut candidates = Vec::with_capacity(group.len() * fixed.len() * 4);
118 for frame in group {
119 for other in fixed {
120 candidates.extend([
121 Point::new(
122 snap_before(other.x - frame.width - gap, grid) - frame.x,
123 0.0,
124 ),
125 Point::new(snap_after(other.x + other.width + gap, grid) - frame.x, 0.0),
126 Point::new(
127 0.0,
128 snap_before(other.y - frame.height - gap, grid) - frame.y,
129 ),
130 Point::new(
131 0.0,
132 snap_after(other.y + other.height + gap, grid) - frame.y,
133 ),
134 ]);
135 }
136 }
137 candidates
138 .into_iter()
139 .filter(|offset| {
140 group.iter().all(|frame| {
141 fixed
142 .iter()
143 .all(|other| !clash(translated(*frame, *offset), *other, gap))
144 })
145 })
146 .min_by_key(|offset| translation_key(*offset))
147}
148
149fn translated(frame: Rect, offset: Point) -> Rect {
150 Rect::new(
151 frame.x + offset.x,
152 frame.y + offset.y,
153 frame.width,
154 frame.height,
155 )
156}
157
158fn groups_clear(group: &[Rect], fixed: &[Rect], gap: f64) -> bool {
159 group
160 .iter()
161 .all(|frame| fixed.iter().all(|other| !clash(*frame, *other, gap)))
162}
163
164fn clear_frames(frames: &[Rect], gap: f64) -> bool {
165 frames.iter().enumerate().all(|(index, frame)| {
166 frames[index + 1..]
167 .iter()
168 .all(|other| !clash(*frame, *other, gap))
169 })
170}
171
172fn first_collision(
175 frames: &BTreeMap<Id, Rect>,
176 moving: &BTreeSet<Id>,
177 gap: f64,
178) -> Option<(Id, Id)> {
179 let entries: Vec<_> = frames.iter().collect();
180 for (index, (a_id, a)) in entries.iter().enumerate() {
181 for (b_id, b) in &entries[index + 1..] {
182 if !clash(**a, **b, gap) {
183 continue;
184 }
185 return Some(match (moving.contains(*a_id), moving.contains(*b_id)) {
186 (true, false) => ((*b_id).clone(), (*a_id).clone()),
187 (false, true) => ((*a_id).clone(), (*b_id).clone()),
188 _ => ((*a_id).clone(), (*b_id).clone()),
192 });
193 }
194 }
195 None
196}
197
198fn nearest_clear(frame: Rect, keeper: Rect, occupied: &[Rect], gap: f64, grid: Grid) -> Rect {
200 let mut candidates = Vec::with_capacity(occupied.len() * 4);
201 for other in occupied {
202 candidates.extend([
203 Rect::new(
204 snap_before(other.x - frame.width - gap, grid),
205 grid.snap(frame.y),
206 frame.width,
207 frame.height,
208 ),
209 Rect::new(
210 snap_after(other.x + other.width + gap, grid),
211 grid.snap(frame.y),
212 frame.width,
213 frame.height,
214 ),
215 Rect::new(
216 grid.snap(frame.x),
217 snap_before(other.y - frame.height - gap, grid),
218 frame.width,
219 frame.height,
220 ),
221 Rect::new(
222 grid.snap(frame.x),
223 snap_after(other.y + other.height + gap, grid),
224 frame.width,
225 frame.height,
226 ),
227 ]);
228 }
229
230 let away_x = frame.center().x - keeper.center().x;
231 let away_y = frame.center().y - keeper.center().y;
232 candidates
233 .into_iter()
234 .filter(|candidate| occupied.iter().all(|other| !clash(*candidate, *other, gap)))
235 .min_by(|a, b| {
236 candidate_key(*a, frame, away_x, away_y)
237 .partial_cmp(&candidate_key(*b, frame, away_x, away_y))
238 .unwrap_or(std::cmp::Ordering::Equal)
239 })
240 .unwrap_or(frame)
244}
245
246fn candidate_key(
249 candidate: Rect,
250 start: Rect,
251 away_x: f64,
252 away_y: f64,
253) -> (Ordered, u8, u8, Ordered, Ordered) {
254 let dx = candidate.x - start.x;
255 let dy = candidate.y - start.y;
256 let dot = dx * away_x + dy * away_y;
257 let away_rank = if dot > 0.0 {
258 0
259 } else if dot == 0.0 {
260 1
261 } else {
262 2
263 };
264 let direction = if dx > 0.0 {
265 0 } else if dy > 0.0 {
267 1 } else if dx < 0.0 {
269 2 } else {
271 3 };
273 (
274 Ordered(dx.abs() + dy.abs()),
275 away_rank,
276 direction,
277 Ordered(candidate.x),
278 Ordered(candidate.y),
279 )
280}
281
282fn translation_key(offset: Point) -> (Ordered, u8, Ordered, Ordered) {
283 let direction = if offset.x > 0.0 {
284 0 } else if offset.y > 0.0 {
286 1 } else if offset.x < 0.0 {
288 2 } else {
290 3 };
292 (
293 Ordered(offset.x.abs() + offset.y.abs()),
294 direction,
295 Ordered(offset.x),
296 Ordered(offset.y),
297 )
298}
299
300#[derive(Clone, Copy, Debug, PartialEq)]
303struct Ordered(f64);
304
305impl Eq for Ordered {}
306
307impl PartialOrd for Ordered {
308 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
309 Some(self.cmp(other))
310 }
311}
312
313impl Ord for Ordered {
314 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
315 self.0.total_cmp(&other.0)
316 }
317}
318
319fn snap_before(value: f64, grid: Grid) -> f64 {
320 (value / grid.size()).floor() * grid.size()
321}
322
323fn snap_after(value: f64, grid: Grid) -> f64 {
324 (value / grid.size()).ceil() * grid.size()
325}
326
327fn clear(frames: &BTreeMap<Id, Rect>, gap: f64) -> bool {
328 clear_frames(&frames.values().copied().collect::<Vec<_>>(), gap)
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 const GRID: Grid = Grid::new(12.0);
336 const GAP: f64 = 12.0;
337
338 fn card(x: f64, y: f64) -> Rect {
339 Rect::new(x, y, 216.0, 48.0)
340 }
341
342 fn map(entries: &[(&str, Rect)]) -> BTreeMap<Id, Rect> {
343 entries
344 .iter()
345 .map(|(id, frame)| ((*id).into(), *frame))
346 .collect()
347 }
348
349 #[test]
350 fn a_gap_apart_is_clear_and_anything_closer_is_not() {
351 let a = card(0.0, 0.0);
352 assert!(!clash(a, Rect::new(228.0, 0.0, 216.0, 48.0), GAP));
353 assert!(clash(a, Rect::new(227.0, 0.0, 216.0, 48.0), GAP));
354 }
355
356 #[test]
357 fn separation_on_either_axis_is_enough() {
358 let a = card(0.0, 0.0);
359 assert!(!clash(a, Rect::new(0.0, 60.0, 216.0, 48.0), GAP));
360 assert!(!clash(a, Rect::new(-228.0, 0.0, 216.0, 48.0), GAP));
361 }
362
363 #[test]
364 fn a_clear_layout_is_returned_unchanged() {
365 let frames = map(&[("a", card(0.0, 0.0)), ("b", card(240.0, 0.0))]);
366 assert_eq!(settled(&frames, &BTreeSet::new(), GAP, GRID), frames);
367 }
368
369 #[test]
370 fn the_dragged_frame_moves_and_the_resting_frame_stays_anchored() {
371 let frames = map(&[
372 ("dragged", card(240.0, 0.0)),
373 ("resting", card(240.0, 0.0)),
374 ("unrelated", card(1200.0, 0.0)),
375 ]);
376 let answer = settled(&frames, &BTreeSet::from(["dragged".into()]), GAP, GRID);
377
378 assert_eq!(answer["dragged"], card(240.0, 60.0));
379 assert_eq!(answer["resting"], frames["resting"]);
380 assert_eq!(answer["unrelated"], frames["unrelated"]);
381 assert!(clear(&answer, GAP));
382 }
383
384 #[test]
385 fn a_dragged_group_moves_rigidly_while_its_obstacle_stays_anchored() {
386 let frames = map(&[
387 ("a", card(0.0, 0.0)),
388 ("b", card(0.0, 60.0)),
389 ("obstacle", card(0.0, 60.0)),
390 ]);
391 let moving = BTreeSet::from(["a".into(), "b".into()]);
392 let answer = settled(&frames, &moving, GAP, GRID);
393
394 let a_offset = Point::new(answer["a"].x - frames["a"].x, answer["a"].y - frames["a"].y);
395 let b_offset = Point::new(answer["b"].x - frames["b"].x, answer["b"].y - frames["b"].y);
396 assert_ne!(a_offset, Point::default());
397 assert_eq!(a_offset, b_offset);
398 assert_eq!(answer["obstacle"], frames["obstacle"]);
399 assert!(clear(&answer, GAP));
400 }
401
402 #[test]
403 fn several_overlaps_are_all_repaired_on_the_grid() {
404 let frames = map(&[
405 ("a", card(0.0, 0.0)),
406 ("b", card(0.0, 0.0)),
407 ("c", card(0.0, 0.0)),
408 ("d", card(0.0, 0.0)),
409 ]);
410 let answer = settled(&frames, &BTreeSet::from(["a".into()]), GAP, GRID);
411
412 assert!(clear(&answer, GAP));
413 for frame in answer.values() {
414 assert_eq!(GRID.snap(frame.x), frame.x);
415 assert_eq!(GRID.snap(frame.y), frame.y);
416 }
417 }
418
419 #[test]
420 fn a_grown_node_relocates_while_the_node_below_stays_anchored() {
421 let grown = Rect::new(0.0, 0.0, 216.0, 120.0);
422 let below = card(0.0, 60.0);
423 let frames = map(&[("grown", grown), ("below", below)]);
424 let answer = settled(&frames, &BTreeSet::from(["grown".into()]), GAP, GRID);
425
426 assert_ne!(answer["grown"], grown);
427 assert_eq!(answer["below"], below);
428 assert!(clear(&answer, GAP));
429 }
430}