1use crate::Gray;
2
3#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
5pub struct Shift {
6 pub x: i32,
7 pub y: i32,
8}
9
10impl Shift {
11 pub const ZERO: Self = Self { x: 0, y: 0 };
12
13 pub fn new(x: i32, y: i32) -> Self {
14 Self { x, y }
15 }
16
17 pub fn doubled(self) -> Self {
19 Self::new(self.x * 2, self.y * 2)
20 }
21
22 pub fn offset(self, x: i32, y: i32) -> Self {
23 Self::new(self.x + x, self.y + y)
24 }
25}
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub struct Rect {
30 pub x: usize,
31 pub y: usize,
32 pub width: usize,
33 pub height: usize,
34}
35
36impl Rect {
37 pub fn is_empty(self) -> bool {
38 self.width == 0 || self.height == 0
39 }
40}
41
42impl Gray {
43 pub fn shifted(&self, shift: Shift) -> Gray {
46 let (width, height) = (self.width(), self.height());
47 let mut moved = vec![0; width * height];
48
49 for y in 0..height {
50 let Some(source_y) = source(y, shift.y, height) else {
51 continue;
52 };
53
54 for x in 0..width {
55 if let Some(source_x) = source(x, shift.x, width) {
56 moved[y * width + x] = self.sample(source_x, source_y);
57 }
58 }
59 }
60
61 Gray::from_vec(moved, width, height)
62 }
63}
64
65pub fn common_crop(shifts: &[Shift], width: usize, height: usize) -> Rect {
68 let span = |extent: usize, of: fn(Shift) -> i32| {
71 let furthest = |pick: fn(i32, i32) -> i32| {
72 shifts
73 .iter()
74 .fold(0, |carried, &shift| pick(carried, of(shift)))
75 };
76
77 let start = furthest(i32::max).max(0) as usize;
78 let lost_from_the_far_edge = furthest(i32::min).unsigned_abs() as usize;
79
80 (
81 start,
82 extent
83 .saturating_sub(lost_from_the_far_edge)
84 .saturating_sub(start),
85 )
86 };
87
88 let (x, width) = span(width, |shift| shift.x);
89 let (y, height) = span(height, |shift| shift.y);
90
91 Rect {
92 x,
93 y,
94 width,
95 height,
96 }
97}
98
99fn source(destination: usize, shift: i32, extent: usize) -> Option<usize> {
101 let source = destination as i64 - shift as i64;
102
103 (0..extent as i64)
104 .contains(&source)
105 .then_some(source as usize)
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 fn counting(width: usize, height: usize) -> Gray {
113 Gray::from_vec((1..=(width * height) as u8).collect(), width, height)
114 }
115
116 #[test]
117 fn doubling_carries_a_shift_to_the_next_level_down() {
118 assert_eq!(Shift::new(-3, 5).doubled(), Shift::new(-6, 10));
119 assert_eq!(Shift::ZERO.doubled(), Shift::ZERO);
120 }
121
122 #[test]
123 fn offsetting_moves_by_the_candidate_step() {
124 assert_eq!(Shift::new(4, -2).offset(-1, 1), Shift::new(3, -1));
125 }
126
127 #[test]
128 fn shifting_by_nothing_leaves_the_image_alone() {
129 let image = counting(3, 2);
130
131 assert_eq!(image.shifted(Shift::ZERO).as_slice(), image.as_slice());
132 }
133
134 #[test]
135 fn shifting_pads_the_edge_it_moved_away_from() {
136 let shifted = counting(3, 2).shifted(Shift::new(1, 1));
137
138 assert_eq!(shifted.as_slice(), &[0, 0, 0, 0, 1, 2]);
139 }
140
141 #[test]
142 fn shifting_the_other_way_pads_the_far_edge() {
143 let shifted = counting(3, 2).shifted(Shift::new(-1, 0));
144
145 assert_eq!(shifted.as_slice(), &[2, 3, 0, 5, 6, 0]);
146 }
147
148 #[test]
150 fn shifting_further_than_the_image_leaves_nothing_behind() {
151 let shifted = counting(3, 2).shifted(Shift::new(9, -9));
152
153 assert_eq!(shifted.as_slice(), &[0; 6]);
154 }
155
156 #[test]
157 fn a_stack_that_never_moved_keeps_the_whole_frame() {
158 let crop = common_crop(&[Shift::ZERO, Shift::ZERO], 100, 80);
159
160 assert_eq!(
161 crop,
162 Rect {
163 x: 0,
164 y: 0,
165 width: 100,
166 height: 80
167 }
168 );
169 assert!(!crop.is_empty());
170 }
171
172 #[test]
173 fn a_positive_shift_trims_the_leading_edge() {
174 assert_eq!(
175 common_crop(&[Shift::ZERO, Shift::new(3, 2)], 100, 80),
176 Rect {
177 x: 3,
178 y: 2,
179 width: 97,
180 height: 78
181 }
182 );
183 }
184
185 #[test]
186 fn a_negative_shift_trims_the_trailing_edge() {
187 assert_eq!(
188 common_crop(&[Shift::ZERO, Shift::new(-4, -1)], 100, 80),
189 Rect {
190 x: 0,
191 y: 0,
192 width: 96,
193 height: 79
194 }
195 );
196 }
197
198 #[test]
199 fn shifts_in_both_directions_trim_both_edges() {
200 assert_eq!(
201 common_crop(
202 &[Shift::new(5, 0), Shift::new(-4, -1), Shift::new(2, 3)],
203 100,
204 80
205 ),
206 Rect {
207 x: 5,
208 y: 3,
209 width: 91,
210 height: 76
211 }
212 );
213 }
214
215 #[test]
216 fn a_stack_that_moved_further_than_it_is_wide_has_nothing_in_common() {
217 let crop = common_crop(&[Shift::new(-60, 0), Shift::new(60, 0)], 100, 80);
218
219 assert!(crop.is_empty(), "{crop:?}");
220 }
221}