grixy/transform.rs
1//! Transformation operations for grids.
2//!
3//! [`GridConvertExt`] is automatically implemented for all types that implement reading, or in the
4//! case of [`blend`][GridConvertExt::blend], reading _and_ writing from a grid, and provides
5//! additional methods for lazily transforming grids (leaving the original grid unchanged, and
6//! without any allocations).
7//!
8//! Operations include:
9//!
10//! - [`blend`](GridConvertExt::blend): Creates a blended version of the grid, applying a blend function when setting elements.
11//! - [`copied`](GridConvertExt::copied): Creates a grid that copies all of its elements.
12//! - [`flatten`](GridConvertExt::flatten): Collects the elements of the grid into a new buffer.
13//! - [`map`](GridConvertExt::map): Creates a grid that applies a mapping function to its elements.
14//! - [`scale`](GridConvertExt::scale): Creates a scaled version of the grid.
15//! - [`view`](GridConvertExt::view): Creates a view of the grid over a specified rectangular region.
16//!
17//! ## Chaining transformations
18//!
19//! Methods on [`GridConvertExt`] can be chained together to create complex transformations, as
20//! they consume the grid and return a new one. This allows for a functional style of programming
21//! where each transformation is applied in sequence, without modifying the original grid:
22//!
23//! ```rust
24//! use grixy::prelude::*;
25//!
26//! let grid = GridBuf::new_filled(3, 3, 1)
27//! .copied()
28//! .map(|x| x * 2)
29//! .view(Rect::from_ltwh(0, 0, 2, 2))
30//! .scale(2);
31//!
32//! assert_eq!(grid.get(Pos::new(1, 1)), Some(2));
33//! ```
34//!
35//! ## Sharing a grid
36//!
37//! To share the original grid, you can use `Rc` or `Arc` to wrap it:
38//!
39//! ```rust
40//! // Or alloc::rc::Rc;
41//! use std::rc::Rc;
42//! use grixy::prelude::*;
43//!
44//! let rc = Rc::new(GridBuf::new_filled(3, 3, 1));
45//!
46//! let rf = Rc::clone(&rc);
47//! let chained = rf
48//! .copied()
49//! .map(|x| x * 2)
50//! .view(Rect::from_ltwh(0, 0, 2, 2))
51//! .scale(2);
52//! assert_eq!(chained.get(Pos::new(1, 1)), Some(2));
53//!
54//! // Original grid is still accessible
55//! assert_eq!(rc.get(Pos::new(1, 1)), Some(&1));
56//! ```
57
58use core::marker::PhantomData;
59
60#[cfg(feature = "buffer")]
61use crate::ops::{ExactSizeGrid, layout};
62use crate::{
63 core::Rect,
64 ops::{GridRead, GridWrite},
65};
66
67mod blended;
68pub use blended::Blended;
69
70mod copied;
71pub use copied::Copied;
72
73mod mapped;
74pub use mapped::Mapped;
75
76mod scaled;
77pub use scaled::Scaled;
78
79mod viewed;
80pub use viewed::Viewed;
81
82/// Extension trait for converting grids into different forms.
83pub trait GridConvertExt: GridRead {
84 /// Creates a grid that copies all of its elements.
85 ///
86 /// This is useful when you have a `GridRead<&T>`, but need a `GridRead<T>`.
87 ///
88 /// ## Examples
89 ///
90 /// ```rust
91 /// use grixy::prelude::*;
92 ///
93 /// // By default, `GridBuf` returns references to its elements (similar to `Vec`).
94 /// let grid = GridBuf::new_filled(3, 3, 1);
95 /// assert_eq!(grid.get(Pos::new(1, 1)), Some(&1));
96 ///
97 /// // We can create a `GridRead` that returns owned copies of the elements.
98 /// let copied = grid.copied();
99 /// assert_eq!(copied.get(Pos::new(1, 1)), Some(1));
100 /// ```
101 fn copied<'a, T>(self) -> Copied<T, Self>
102 where
103 Self: Sized + GridRead<Element<'a> = &'a T> + 'a,
104 T: Copy + 'a,
105 {
106 Copied {
107 source: self,
108 _element: PhantomData,
109 }
110 }
111
112 /// Creates a grid that applies a mapping function to its elements.
113 ///
114 /// This is useful when you want to transform the elements of a grid lazily.
115 ///
116 /// ## Examples
117 ///
118 /// ```rust
119 /// use grixy::prelude::*;
120 ///
121 /// let grid = GridBuf::new_filled(3, 3, 1);
122 /// let mapped = grid.map(|&x| x * 2);
123 /// assert_eq!(mapped.get(Pos::new(1, 1)), Some(2));
124 /// ```
125 fn map<F, T>(self, map_fn: F) -> Mapped<F, Self, T>
126 where
127 Self: Sized,
128 F: Fn(Self::Element<'_>) -> T,
129 {
130 Mapped {
131 source: self,
132 map_fn,
133 _element: PhantomData,
134 }
135 }
136
137 /// Creates a view of the grid over a specified rectangular region.
138 ///
139 /// The view is a lightweight wrapper that allows access to a subset of the grid's elements.
140 ///
141 /// ## Examples
142 ///
143 /// ```rust
144 /// use grixy::prelude::*;
145 ///
146 /// let grid = GridBuf::new_filled(3, 3, 1);
147 /// let view = grid.view(Rect::from_ltwh(0, 0, 2, 2));
148 /// assert_eq!(view.get(Pos::new(1, 1)), Some(&1));
149 /// assert_eq!(view.get(Pos::new(2, 2)), None);
150 /// ```
151 fn view(self, bounds: Rect) -> Viewed<Self>
152 where
153 Self: Sized,
154 {
155 Viewed {
156 source: self,
157 bounds,
158 }
159 }
160
161 /// Creates a scaled version of the grid.
162 ///
163 /// The `scale` factor determines how many cells in the original grid correspond to one cell
164 /// in the scaled grid. For example, a scale factor of 2 means that each cell in the scaled grid
165 /// corresponds to a 2x2 block of cells in the original grid.
166 ///
167 /// ## Examples
168 ///
169 /// ```rust
170 /// use grixy::prelude::*;
171 ///
172 /// let grid = GridBuf::new_filled(2, 2, 1);
173 /// let scaled = grid.scale(2);
174 /// assert_eq!(scaled.get(Pos::new(0, 0)), Some(&1));
175 /// assert_eq!(scaled.get(Pos::new(1, 1)), Some(&1));
176 /// assert_eq!(scaled.get(Pos::new(2, 2)), Some(&1));
177 /// assert_eq!(scaled.get(Pos::new(3, 3)), Some(&1));
178 /// assert_eq!(scaled.get(Pos::new(4, 4)), None);
179 /// ```
180 fn scale(self, factor: usize) -> Scaled<Self>
181 where
182 Self: Sized,
183 {
184 Scaled {
185 source: self,
186 scale: factor,
187 }
188 }
189
190 /// Collects the elements of the grid into a new buffer.
191 ///
192 /// This method is only available when the `buffer` feature is enabled.
193 ///
194 /// ## Examples
195 ///
196 /// ```rust
197 /// use grixy::prelude::*;
198 ///
199 /// let grid = GridBuf::new_filled(3, 3, 1);
200 /// let collected = grid.copied().flatten::<Vec<_>, RowMajor>();
201 /// assert_eq!(collected.get(Pos::new(1, 1)), Some(&1));
202 /// assert_eq!(collected.get(Pos::new(3, 3)), None);
203 /// ```
204 #[cfg(feature = "buffer")]
205 fn flatten<'a, B, L>(&'a self) -> crate::buf::GridBuf<Self::Element<'a>, B, L>
206 where
207 B: FromIterator<Self::Element<'a>> + AsRef<[Self::Element<'a>]>,
208 L: layout::LinearLayout,
209 Self: Sized + ExactSizeGrid,
210 Self::Element<'a>: Copy,
211 {
212 use crate::core::Rect;
213
214 let iter = self.iter_rect(Rect::from_ltwh(0, 0, self.width(), self.height()));
215 let elem = iter.collect::<B>();
216 crate::buf::GridBuf::from_buffer(elem, self.width())
217 }
218
219 /// Creates a blended version of this grid, applying a blend function when setting elements.
220 ///
221 /// This is useful for operations like blending colors or combining values.
222 ///
223 /// ## Examples
224 ///
225 /// ```rust
226 /// use grixy::prelude::*;
227 ///
228 /// let mut grid = GridBuf::new_filled(3, 3, 1);
229 /// let blend_fn = |current: &i32, new: i32| current + new;
230 /// let mut blended = grid.blend(blend_fn);
231 ///
232 /// blended.set(Pos::new(1, 1), 5).unwrap();
233 /// assert_eq!(blended.get(Pos::new(1, 1)), Some(&6));
234 /// ```
235 fn blend<F>(&mut self, blend_fn: F) -> Blended<'_, Self, F>
236 where
237 Self: Sized + GridWrite,
238 F: Fn(
239 <Self as GridRead>::Element<'_>,
240 <Self as GridWrite>::Element,
241 ) -> <Self as GridWrite>::Element,
242 {
243 Blended {
244 source: self,
245 blend_fn,
246 }
247 }
248}
249
250impl<T> GridConvertExt for T where T: GridRead {}
251
252// The whole module exercises `GridBuf` (requires `buffer`) and `alloc::{rc, sync}` (requires
253// `alloc`), so gate it accordingly rather than assuming `--all-features` like the rest of the
254// crate's tests happen to (this is what let a `--no-default-features`/partial-feature build
255// silently fail to compile its own test suite).
256#[cfg(all(test, feature = "buffer", feature = "alloc"))]
257mod tests {
258 extern crate alloc;
259
260 use crate::{
261 buf::GridBuf,
262 core::{Pos, Rect},
263 ops::{GridBase as _, layout::RowMajor},
264 };
265 use alloc::{vec, vec::Vec};
266 use ixy::HasSize as _;
267
268 use super::*;
269
270 #[test]
271 fn grid_copied_size() {
272 let grid = GridBuf::<u8, _, _>::new(10, 10).copied();
273 let (size, _) = grid.size_hint();
274 assert_eq!(size.width(), 10);
275 assert_eq!(size.height(), 10);
276 }
277
278 #[test]
279 fn grid_copied_get() {
280 let grid = GridBuf::new_filled(3, 3, 1);
281 let copied = grid.copied();
282 assert_eq!(copied.get(Pos::new(1, 1)), Some(1));
283 assert_eq!(copied.get(Pos::new(3, 3)), None);
284 }
285
286 #[test]
287 fn grid_copied_iter_rect() {
288 let grid = GridBuf::new_filled(3, 3, 1);
289 let copied = grid.copied();
290 let elements: Vec<_> = copied.iter_rect(Rect::from_ltwh(0, 0, 2, 2)).collect();
291 assert_eq!(elements, vec![1, 1, 1, 1]);
292 }
293
294 #[test]
295 fn grid_mapped_size() {
296 let grid = GridBuf::<u8, _, _>::new(10, 10);
297 let mapped = grid.map(|x| x * 2);
298 let (size, _) = mapped.size_hint();
299 assert_eq!(size.width(), 10);
300 assert_eq!(size.height(), 10);
301 }
302
303 #[test]
304 fn grid_mapped_get() {
305 let grid = GridBuf::new_filled(3, 3, 1);
306 let mapped = grid.map(|x| x * 2);
307 assert_eq!(mapped.get(Pos::new(1, 1)), Some(2));
308 assert_eq!(mapped.get(Pos::new(3, 3)), None);
309 }
310
311 #[test]
312 fn grid_mapped_iter_rect() {
313 let grid = GridBuf::new_filled(3, 3, 1);
314 let mapped = grid.map(|x| x * 2);
315 let elements: Vec<_> = mapped.iter_rect(Rect::from_ltwh(0, 0, 2, 2)).collect();
316 assert_eq!(elements, vec![2, 2, 2, 2]);
317 }
318
319 #[test]
320 fn grid_view_size() {
321 let grid = GridBuf::<u8, _, _>::new(10, 10);
322 let view = grid.view(Rect::from_ltwh(0, 0, 5, 5));
323 let (size, _) = view.size_hint();
324 assert_eq!(size.width(), 5);
325 assert_eq!(size.height(), 5);
326 }
327
328 #[test]
329 fn grid_view_get() {
330 let grid = GridBuf::new_filled(3, 3, 1);
331 let view = grid.view(Rect::from_ltwh(0, 0, 2, 2));
332 assert_eq!(view.get(Pos::new(1, 1)), Some(&1));
333 assert_eq!(view.get(Pos::new(2, 2)), None);
334 }
335
336 #[test]
337 fn grid_view_iter_rect() {
338 let grid = GridBuf::new_filled(3, 3, 1);
339 let view = grid.view(Rect::from_ltwh(0, 0, 2, 2));
340 let elements: Vec<_> = view.iter_rect(Rect::from_ltwh(0, 0, 2, 2)).collect();
341 assert_eq!(elements, &[&1, &1, &1, &1]);
342 }
343
344 #[test]
345 fn grid_view_get_with_offset_bounds() {
346 // Regression test for https://github.com/crates-lurey-io/grixy/issues/16: `Viewed::get`
347 // used to double-offset `pos` (and underflow-panic near the origin) whenever
348 // `bounds.top_left() != Pos::ORIGIN`. Every other `view` test above only exercises
349 // origin-aligned bounds, where subtracting or adding the (zero) offset is indistinguishable.
350 #[rustfmt::skip]
351 let grid = GridBuf::<_, _, RowMajor>::from_buffer(vec![
352 1, 2, 3,
353 4, 5, 6,
354 7, 8, 9,
355 ], 3);
356 let view = grid.view(Rect::from_ltwh(1, 1, 2, 2));
357
358 // View-local (0, 0) is the view's own top-left, i.e. source (1, 1) = 5. Used to panic
359 // with "attempt to subtract with overflow".
360 assert_eq!(view.get(Pos::new(0, 0)), Some(&5));
361 // View-local (1, 1) is the view's own bottom-right, i.e. source (2, 2) = 9.
362 assert_eq!(view.get(Pos::new(1, 1)), Some(&9));
363 // Out of the view's own 2x2 bounds, even though it's a valid position in `grid`.
364 assert_eq!(view.get(Pos::new(2, 2)), None);
365 }
366
367 #[test]
368 fn grid_view_iter_rect_with_offset_bounds() {
369 #[rustfmt::skip]
370 let grid = GridBuf::<_, _, RowMajor>::from_buffer(vec![
371 1, 2, 3,
372 4, 5, 6,
373 7, 8, 9,
374 ], 3);
375 let view = grid.view(Rect::from_ltwh(1, 1, 2, 2));
376
377 // Used to underflow-panic translating (0, 0, 2, 2) by `bounds.top_left()` in the wrong
378 // direction.
379 let elements: Vec<_> = view.iter_rect(Rect::from_ltwh(0, 0, 2, 2)).collect();
380 assert_eq!(elements, &[&5, &6, &8, &9]);
381 }
382
383 #[test]
384 fn grid_view_iter_rect_clamps_oversized_query_to_view_bounds() {
385 #[rustfmt::skip]
386 let grid = GridBuf::<_, _, RowMajor>::from_buffer(vec![
387 1, 2, 3,
388 4, 5, 6,
389 7, 8, 9,
390 ], 3);
391 let view = grid.view(Rect::from_ltwh(1, 1, 2, 2));
392
393 // A query larger than the view's own 2x2 size must not leak cells from outside `bounds`.
394 let elements: Vec<_> = view.iter_rect(Rect::from_ltwh(0, 0, 10, 10)).collect();
395 assert_eq!(elements, &[&5, &6, &8, &9]);
396 }
397
398 #[test]
399 fn grid_scaled_size() {
400 let grid = GridBuf::<u8, _, _>::new(10, 10);
401 let scaled = grid.scale(2);
402 let (size, _) = scaled.size_hint();
403 assert_eq!(size.width(), 20);
404 assert_eq!(size.height(), 20);
405 }
406
407 #[test]
408 fn grid_scaled_get() {
409 let grid = GridBuf::<_, _, RowMajor>::from_buffer(vec![1, 2, 3, 4], 2);
410 let scaled = grid.scale(2);
411 assert_eq!(scaled.get(Pos::new(1, 1)), Some(&1));
412 assert_eq!(scaled.get(Pos::new(2, 2)), Some(&4));
413 assert_eq!(scaled.get(Pos::new(3, 3)), Some(&4));
414 assert_eq!(scaled.get(Pos::new(4, 4)), None);
415 }
416
417 #[test]
418 fn grid_scaled_iter_rect() {
419 let grid = GridBuf::<_, _, RowMajor>::from_buffer(vec![1, 2, 3, 4], 2);
420 let scaled = grid.scale(2);
421 let elements: Vec<_> = scaled.iter_rect(Rect::from_ltwh(0, 0, 4, 4)).collect();
422
423 #[rustfmt::skip]
424 assert_eq!(elements, &[
425 &1, &1, &2, &2,
426 &1, &1, &2, &2,
427 &3, &3, &4, &4,
428 &3, &3, &4, &4,
429 ]);
430 }
431
432 #[test]
433 fn grid_blended_size() {
434 let mut grid = GridBuf::<u8, _, _>::new(10, 10);
435 let mut blended = grid.blend(|current, new| current + new);
436 blended.set(Pos::new(1, 1), 5).unwrap();
437 let (size, _) = blended.size_hint();
438 assert_eq!(size.width(), 10);
439 assert_eq!(size.height(), 10);
440 }
441
442 #[test]
443 fn grid_write_blended_set() {
444 let mut grid = GridBuf::new_filled(3, 3, 0);
445 let mut blended = grid.blend(|current, new| current + new);
446 blended.set(Pos::new(1, 1), 5).unwrap();
447 assert_eq!(blended.get(Pos::new(1, 1)), Some(&5));
448 blended.set(Pos::new(1, 1), 3).unwrap();
449 assert_eq!(blended.get(Pos::new(1, 1)), Some(&8));
450 }
451
452 #[test]
453 fn grid_write_blended_iter_rect() {
454 let mut grid = GridBuf::new_filled(3, 3, 0);
455 let mut blended = grid.blend(|current, new| current + new);
456 blended.set(Pos::new(1, 1), 5).unwrap();
457 blended.set(Pos::new(2, 2), 3).unwrap();
458 let elements: Vec<_> = blended.iter_rect(Rect::from_ltwh(0, 0, 3, 3)).collect();
459 assert_eq!(elements, vec![&0, &0, &0, &0, &5, &0, &0, &0, &3]);
460 }
461
462 #[test]
463 fn grid_chained_operations() {
464 let grid = GridBuf::new_filled(3, 3, 1)
465 .copied()
466 .map(|x| x * 2)
467 .view(Rect::from_ltwh(0, 0, 2, 2))
468 .scale(2);
469
470 assert_eq!(grid.get(Pos::new(1, 1)), Some(2));
471 }
472
473 #[test]
474 fn grid_rc() {
475 use alloc::rc::Rc;
476
477 let rc = Rc::new(GridBuf::new_filled(3, 3, 1));
478 let rf = Rc::clone(&rc);
479 let chained = rf
480 .copied()
481 .map(|x| x * 2)
482 .view(Rect::from_ltwh(0, 0, 2, 2))
483 .scale(2);
484 assert_eq!(chained.get(Pos::new(1, 1)), Some(2));
485 }
486
487 #[test]
488 fn grid_arc() {
489 use alloc::sync::Arc;
490
491 let arc = Arc::new(GridBuf::new_filled(3, 3, 1));
492 let af = Arc::clone(&arc);
493 let chained = af
494 .copied()
495 .map(|x| x * 2)
496 .view(Rect::from_ltwh(0, 0, 2, 2))
497 .scale(2);
498 assert_eq!(chained.get(Pos::new(1, 1)), Some(2));
499 }
500}