Skip to main content

oxigdal_core/buffer/
mask.rs

1//! Nodata and validity bitmask for raster regions.
2//!
3//! # Bit-packing convention
4//!
5//! Pixels are stored in row-major order: pixel `(x, y)` maps to bit index
6//! `i = y * width + x`.  Each bit index maps to word `data[i / 64]`, bit
7//! position `i % 64` (LSB = bit 0 = leftmost pixel in each group of 64).
8//!
9//! **Masking semantics**: a `1` bit means the pixel is **invalid / nodata**;
10//! a `0` bit means the pixel is **valid**.  This convention matches GDAL's
11//! nodata mask band where non-zero → valid data is the *inverse*, but
12//! follows the more common raster tradition where "masked out" = 1.
13//!
14//! # Tail-word invariant
15//!
16//! When `width * height` is not a multiple of 64 the high bits of the last
17//! word are always kept **zero**.  Every mutating method that could set high
18//! bits calls `Mask::clear_tail_bits` before returning.  This invariant
19//! makes `count_set`, `all_unset`, and `any_set` trivially correct without
20//! any per-call tail masking.
21
22use crate::error::{OxiGdalError, Result};
23
24/// A 2-D boolean bitmask for a raster region.
25///
26/// `Mask` uses bit-packed `u64` words (64 pixels per word) for memory
27/// efficiency.  See the [module documentation][self] for the storage and
28/// semantic conventions.
29///
30/// # Examples
31///
32/// ```
33/// use oxigdal_core::buffer::Mask;
34///
35/// let mut mask = Mask::new(10, 10);         // all valid (0)
36/// mask.set(3, 3, true);                      // mark (3,3) as nodata
37/// assert!(mask.get(3, 3));
38/// assert_eq!(mask.count_set(), 1);
39/// ```
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Mask {
42    /// Width of the raster region in pixels.
43    width: usize,
44    /// Height of the raster region in pixels.
45    height: usize,
46    /// Bit-packed words.  Bit `i % 64` of `data[i / 64]` holds pixel
47    /// `(i % width, i / width)` where `i = y * width + x`.
48    data: Vec<u64>,
49}
50
51// ─── Internal helpers ──────────────────────────────────────────────────────────
52
53impl Mask {
54    /// Returns the number of `u64` words needed for `pixel_count` pixels.
55    #[inline]
56    fn words_for(pixel_count: usize) -> usize {
57        pixel_count.saturating_add(63) / 64
58    }
59
60    /// Bit-index → (word index, bit position within word).
61    #[inline]
62    fn coords(i: usize) -> (usize, u32) {
63        (i / 64, (i % 64) as u32)
64    }
65
66    /// Clears the unused high bits of the last word so that the tail-word
67    /// invariant holds.  This is a no-op when `width * height` is an exact
68    /// multiple of 64.
69    fn clear_tail_bits(&mut self) {
70        let pixel_count = self.width * self.height;
71        let tail = pixel_count % 64;
72        if tail != 0 {
73            if let Some(last) = self.data.last_mut() {
74                // Keep only the lower `tail` bits.
75                *last &= (1u64 << tail).wrapping_sub(1);
76            }
77        }
78    }
79
80    /// Validates that `other` has the same dimensions as `self`.
81    #[inline]
82    fn check_dims(&self, other: &Mask) -> Result<()> {
83        if self.width != other.width || self.height != other.height {
84            Err(OxiGdalError::InvalidParameter {
85                parameter: "other",
86                message: format!(
87                    "Mask dimension mismatch: self={}×{}, other={}×{}",
88                    self.width, self.height, other.width, other.height
89                ),
90            })
91        } else {
92            Ok(())
93        }
94    }
95}
96
97// ─── Constructors ─────────────────────────────────────────────────────────────
98
99impl Mask {
100    /// Creates a new mask with all bits **unset** (all pixels valid).
101    ///
102    /// A mask with `width == 0` or `height == 0` is valid and has no pixels.
103    #[must_use]
104    pub fn new(width: usize, height: usize) -> Self {
105        let words = Self::words_for(width * height);
106        Self {
107            width,
108            height,
109            data: vec![0u64; words],
110        }
111    }
112
113    /// Creates a new mask with all bits **set** (all pixels invalid/nodata).
114    ///
115    /// The tail-word invariant is enforced — high bits of the last word are
116    /// always zero even after the fill.
117    #[must_use]
118    pub fn new_filled(width: usize, height: usize) -> Self {
119        let mut mask = Self {
120            width,
121            height,
122            data: vec![!0u64; Self::words_for(width * height)],
123        };
124        mask.clear_tail_bits();
125        mask
126    }
127
128    /// Constructs a mask from a `bool` slice.
129    ///
130    /// The slice is interpreted in row-major order: `values[y * width + x]`
131    /// is the mask value for pixel `(x, y)`.  `true` → bit set (nodata).
132    ///
133    /// # Errors
134    ///
135    /// Returns [`OxiGdalError::InvalidParameter`] if `values.len() !=
136    /// width * height`.
137    pub fn from_slice(width: usize, height: usize, values: &[bool]) -> Result<Self> {
138        let pixel_count = width * height;
139        if values.len() != pixel_count {
140            return Err(OxiGdalError::InvalidParameter {
141                parameter: "values",
142                message: format!(
143                    "Slice length {} does not match {}×{} = {} pixels",
144                    values.len(),
145                    width,
146                    height,
147                    pixel_count
148                ),
149            });
150        }
151        let mut mask = Self::new(width, height);
152        for (i, &v) in values.iter().enumerate() {
153            if v {
154                let (word, bit) = Self::coords(i);
155                mask.data[word] |= 1u64 << bit;
156            }
157        }
158        // No tail clearing needed: we only ever set individual bits; the
159        // constructor already initialised unused bits to 0.
160        Ok(mask)
161    }
162}
163
164// ─── Getters / basic accessors ────────────────────────────────────────────────
165
166impl Mask {
167    /// Width of the raster region in pixels.
168    #[must_use]
169    #[inline]
170    pub const fn width(&self) -> usize {
171        self.width
172    }
173
174    /// Height of the raster region in pixels.
175    #[must_use]
176    #[inline]
177    pub const fn height(&self) -> usize {
178        self.height
179    }
180
181    /// Total number of pixels (`width * height`).
182    #[must_use]
183    #[inline]
184    pub fn pixel_count(&self) -> usize {
185        self.width * self.height
186    }
187
188    /// Returns the mask bit for pixel `(x, y)`.
189    ///
190    /// `true` means the pixel is **invalid / nodata**.
191    ///
192    /// # Panics
193    ///
194    /// Panics in debug builds if `x >= width` or `y >= height`.  In release
195    /// builds the access is clamped silently.
196    #[must_use]
197    pub fn get(&self, x: usize, y: usize) -> bool {
198        debug_assert!(
199            x < self.width,
200            "x={} out of bounds (width={})",
201            x,
202            self.width
203        );
204        debug_assert!(
205            y < self.height,
206            "y={} out of bounds (height={})",
207            y,
208            self.height
209        );
210        let i = y * self.width + x;
211        let (word, bit) = Self::coords(i);
212        if word < self.data.len() {
213            (self.data[word] >> bit) & 1 == 1
214        } else {
215            false
216        }
217    }
218
219    /// Sets the mask bit for pixel `(x, y)`.
220    ///
221    /// `value = true` marks the pixel as **invalid / nodata**.
222    ///
223    /// # Panics
224    ///
225    /// Panics in debug builds if `x >= width` or `y >= height`.
226    pub fn set(&mut self, x: usize, y: usize, value: bool) {
227        debug_assert!(
228            x < self.width,
229            "x={} out of bounds (width={})",
230            x,
231            self.width
232        );
233        debug_assert!(
234            y < self.height,
235            "y={} out of bounds (height={})",
236            y,
237            self.height
238        );
239        let i = y * self.width + x;
240        let (word, bit) = Self::coords(i);
241        if word < self.data.len() {
242            if value {
243                self.data[word] |= 1u64 << bit;
244            } else {
245                self.data[word] &= !(1u64 << bit);
246            }
247        }
248    }
249}
250
251// ─── Fill operations ──────────────────────────────────────────────────────────
252
253impl Mask {
254    /// Sets all bits to `value`.
255    ///
256    /// The tail-word invariant is maintained.
257    pub fn fill(&mut self, value: bool) {
258        let fill_word = if value { !0u64 } else { 0u64 };
259        for w in &mut self.data {
260            *w = fill_word;
261        }
262        if value {
263            self.clear_tail_bits();
264        }
265    }
266
267    /// Sets all bits in the rectangle `[x, x+w) × [y, y+h)` to `value`.
268    ///
269    /// Coordinates that fall outside the mask bounds are silently clamped.
270    /// The tail-word invariant is maintained.
271    pub fn fill_rect(&mut self, x: usize, y: usize, w: usize, h: usize, value: bool) {
272        // Clamp rectangle to mask bounds.
273        let x_end = (x + w).min(self.width);
274        let y_end = (y + h).min(self.height);
275        for row in y..y_end {
276            for col in x..x_end {
277                self.set(col, row, value);
278            }
279        }
280        // `set` never touches tail bits beyond pixel bounds, so no extra
281        // clear_tail_bits call is required here.
282    }
283}
284
285// ─── Bitwise operations ───────────────────────────────────────────────────────
286
287impl Mask {
288    /// Applies bitwise AND with `other` in place.
289    ///
290    /// # Errors
291    ///
292    /// Returns [`OxiGdalError::InvalidParameter`] if dimensions differ.
293    pub fn and_assign(&mut self, other: &Mask) -> Result<()> {
294        self.check_dims(other)?;
295        for (a, b) in self.data.iter_mut().zip(other.data.iter()) {
296            *a &= b;
297        }
298        Ok(())
299    }
300
301    /// Applies bitwise OR with `other` in place.
302    ///
303    /// # Errors
304    ///
305    /// Returns [`OxiGdalError::InvalidParameter`] if dimensions differ.
306    pub fn or_assign(&mut self, other: &Mask) -> Result<()> {
307        self.check_dims(other)?;
308        for (a, b) in self.data.iter_mut().zip(other.data.iter()) {
309            *a |= b;
310        }
311        // OR can set tail bits only if `other` has tail bits set, but by our
312        // invariant `other`'s tail bits are 0, so no extra clear needed.
313        Ok(())
314    }
315
316    /// Applies bitwise NOT in place.
317    ///
318    /// The tail-word invariant is re-established after inversion.
319    pub fn not_in_place(&mut self) {
320        for w in &mut self.data {
321            *w = !*w;
322        }
323        self.clear_tail_bits();
324    }
325
326    /// Applies bitwise XOR with `other` in place.
327    ///
328    /// # Errors
329    ///
330    /// Returns [`OxiGdalError::InvalidParameter`] if dimensions differ.
331    pub fn xor_assign(&mut self, other: &Mask) -> Result<()> {
332        self.check_dims(other)?;
333        for (a, b) in self.data.iter_mut().zip(other.data.iter()) {
334            *a ^= b;
335        }
336        // Same argument as or_assign — tail stays zero.
337        Ok(())
338    }
339
340    /// Returns a new mask that is the bitwise AND of `self` and `other`.
341    ///
342    /// # Errors
343    ///
344    /// Returns [`OxiGdalError::InvalidParameter`] if dimensions differ.
345    pub fn and(&self, other: &Mask) -> Result<Mask> {
346        self.check_dims(other)?;
347        let data: Vec<u64> = self
348            .data
349            .iter()
350            .zip(other.data.iter())
351            .map(|(a, b)| a & b)
352            .collect();
353        Ok(Mask {
354            width: self.width,
355            height: self.height,
356            data,
357        })
358    }
359
360    /// Returns a new mask that is the bitwise OR of `self` and `other`.
361    ///
362    /// # Errors
363    ///
364    /// Returns [`OxiGdalError::InvalidParameter`] if dimensions differ.
365    pub fn or(&self, other: &Mask) -> Result<Mask> {
366        self.check_dims(other)?;
367        let data: Vec<u64> = self
368            .data
369            .iter()
370            .zip(other.data.iter())
371            .map(|(a, b)| a | b)
372            .collect();
373        Ok(Mask {
374            width: self.width,
375            height: self.height,
376            data,
377        })
378    }
379
380    /// Returns a new mask that is the bitwise NOT of `self`.
381    ///
382    /// The tail-word invariant is preserved.
383    #[must_use]
384    pub fn not(&self) -> Mask {
385        let mut result = Mask {
386            width: self.width,
387            height: self.height,
388            data: self.data.iter().map(|w| !w).collect(),
389        };
390        result.clear_tail_bits();
391        result
392    }
393}
394
395// ─── Statistics ───────────────────────────────────────────────────────────────
396
397impl Mask {
398    /// Number of bits that are set (pixels marked invalid/nodata).
399    #[must_use]
400    pub fn count_set(&self) -> usize {
401        self.data.iter().map(|w| w.count_ones() as usize).sum()
402    }
403
404    /// Number of bits that are unset (pixels that are valid).
405    #[must_use]
406    pub fn count_unset(&self) -> usize {
407        self.pixel_count() - self.count_set()
408    }
409
410    /// Returns `true` if **all** pixels are masked (all bits set).
411    #[must_use]
412    pub fn all_set(&self) -> bool {
413        self.count_set() == self.pixel_count()
414    }
415
416    /// Returns `true` if **no** pixels are masked (all bits unset).
417    #[must_use]
418    pub fn all_unset(&self) -> bool {
419        self.data.iter().all(|&w| w == 0)
420    }
421
422    /// Returns `true` if at least one pixel is masked.
423    #[must_use]
424    pub fn any_set(&self) -> bool {
425        self.data.iter().any(|&w| w != 0)
426    }
427}
428
429// ─── Iterator over set positions ─────────────────────────────────────────────
430
431impl Mask {
432    /// Returns an iterator over `(x, y)` coordinates of all **set** bits
433    /// (masked/nodata pixels).
434    ///
435    /// Iterates in row-major order.
436    pub fn set_positions(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
437        SetPositions {
438            mask: self,
439            word_idx: 0,
440            current_word: self.data.first().copied().unwrap_or(0),
441            pixel_base: 0,
442        }
443    }
444}
445
446/// Iterator over `(x, y)` positions of set bits.
447struct SetPositions<'a> {
448    mask: &'a Mask,
449    /// Index of the word currently being scanned.
450    word_idx: usize,
451    /// Copy of the current word with already-visited bits cleared.
452    current_word: u64,
453    /// Bit index of word `word_idx` bit 0.
454    pixel_base: usize,
455}
456
457impl<'a> Iterator for SetPositions<'a> {
458    type Item = (usize, usize);
459
460    fn next(&mut self) -> Option<Self::Item> {
461        loop {
462            if self.current_word != 0 {
463                // Find the lowest set bit.
464                let bit = self.current_word.trailing_zeros() as usize;
465                // Clear that bit.
466                self.current_word &= self.current_word - 1;
467                let i = self.pixel_base + bit;
468                if i < self.mask.pixel_count() {
469                    let x = i % self.mask.width;
470                    let y = i / self.mask.width;
471                    return Some((x, y));
472                }
473                // Bit was in the tail padding — skip.
474                continue;
475            }
476            // Advance to the next word.
477            self.word_idx += 1;
478            if self.word_idx >= self.mask.data.len() {
479                return None;
480            }
481            self.pixel_base = self.word_idx * 64;
482            self.current_word = self.mask.data[self.word_idx];
483        }
484    }
485}
486
487// ─── Nodata-based constructors ────────────────────────────────────────────────
488
489impl Mask {
490    /// Builds a nodata mask from an `f32` slice.
491    ///
492    /// A pixel is marked **invalid** if its value is NaN **or** exactly equals
493    /// `nodata` (bitwise-exact comparison after handling NaN).
494    ///
495    /// # Errors
496    ///
497    /// Returns [`OxiGdalError::InvalidParameter`] if `data.len() != width *
498    /// height`.
499    pub fn from_nodata_f32(data: &[f32], width: usize, height: usize, nodata: f32) -> Result<Self> {
500        let pixel_count = width * height;
501        if data.len() != pixel_count {
502            return Err(OxiGdalError::InvalidParameter {
503                parameter: "data",
504                message: format!(
505                    "Slice length {} ≠ {}×{} = {} pixels",
506                    data.len(),
507                    width,
508                    height,
509                    pixel_count
510                ),
511            });
512        }
513        let nodata_is_nan = nodata.is_nan();
514        let mut mask = Self::new(width, height);
515        for (i, &v) in data.iter().enumerate() {
516            let is_nodata = if nodata_is_nan {
517                v.is_nan()
518            } else {
519                v == nodata
520            };
521            if is_nodata {
522                let (word, bit) = Self::coords(i);
523                mask.data[word] |= 1u64 << bit;
524            }
525        }
526        Ok(mask)
527    }
528
529    /// Builds a nodata mask from an `f64` slice.
530    ///
531    /// A pixel is marked **invalid** if its value is NaN **or** exactly equals
532    /// `nodata`.
533    ///
534    /// # Errors
535    ///
536    /// Returns [`OxiGdalError::InvalidParameter`] if `data.len() != width *
537    /// height`.
538    pub fn from_nodata_f64(data: &[f64], width: usize, height: usize, nodata: f64) -> Result<Self> {
539        let pixel_count = width * height;
540        if data.len() != pixel_count {
541            return Err(OxiGdalError::InvalidParameter {
542                parameter: "data",
543                message: format!(
544                    "Slice length {} ≠ {}×{} = {} pixels",
545                    data.len(),
546                    width,
547                    height,
548                    pixel_count
549                ),
550            });
551        }
552        let nodata_is_nan = nodata.is_nan();
553        let mut mask = Self::new(width, height);
554        for (i, &v) in data.iter().enumerate() {
555            let is_nodata = if nodata_is_nan {
556                v.is_nan()
557            } else {
558                v == nodata
559            };
560            if is_nodata {
561                let (word, bit) = Self::coords(i);
562                mask.data[word] |= 1u64 << bit;
563            }
564        }
565        Ok(mask)
566    }
567
568    /// Builds a nodata mask from an `i32` slice.
569    ///
570    /// A pixel is marked **invalid** if its value exactly equals `nodata`.
571    ///
572    /// # Errors
573    ///
574    /// Returns [`OxiGdalError::InvalidParameter`] if `data.len() != width *
575    /// height`.
576    pub fn from_nodata_i32(data: &[i32], width: usize, height: usize, nodata: i32) -> Result<Self> {
577        let pixel_count = width * height;
578        if data.len() != pixel_count {
579            return Err(OxiGdalError::InvalidParameter {
580                parameter: "data",
581                message: format!(
582                    "Slice length {} ≠ {}×{} = {} pixels",
583                    data.len(),
584                    width,
585                    height,
586                    pixel_count
587                ),
588            });
589        }
590        let mut mask = Self::new(width, height);
591        for (i, &v) in data.iter().enumerate() {
592            if v == nodata {
593                let (word, bit) = Self::coords(i);
594                mask.data[word] |= 1u64 << bit;
595            }
596        }
597        Ok(mask)
598    }
599
600    /// Builds a nodata mask from a `u8` slice.
601    ///
602    /// A pixel is marked **invalid** if its value exactly equals `nodata`.
603    ///
604    /// # Errors
605    ///
606    /// Returns [`OxiGdalError::InvalidParameter`] if `data.len() != width *
607    /// height`.
608    pub fn from_nodata_u8(data: &[u8], width: usize, height: usize, nodata: u8) -> Result<Self> {
609        let pixel_count = width * height;
610        if data.len() != pixel_count {
611            return Err(OxiGdalError::InvalidParameter {
612                parameter: "data",
613                message: format!(
614                    "Slice length {} ≠ {}×{} = {} pixels",
615                    data.len(),
616                    width,
617                    height,
618                    pixel_count
619                ),
620            });
621        }
622        let mut mask = Self::new(width, height);
623        for (i, &v) in data.iter().enumerate() {
624            if v == nodata {
625                let (word, bit) = Self::coords(i);
626                mask.data[word] |= 1u64 << bit;
627            }
628        }
629        Ok(mask)
630    }
631}
632
633// ─── Interop ──────────────────────────────────────────────────────────────────
634
635impl Mask {
636    /// Converts the mask to a `Vec<bool>` in row-major order.
637    ///
638    /// The returned vector has length `pixel_count()`.  `true` at index
639    /// `y * width + x` means pixel `(x, y)` is **invalid / nodata**.
640    #[must_use]
641    pub fn to_bool_vec(&self) -> Vec<bool> {
642        let n = self.pixel_count();
643        let mut out = Vec::with_capacity(n);
644        for i in 0..n {
645            let (word, bit) = Self::coords(i);
646            out.push((self.data[word] >> bit) & 1 == 1);
647        }
648        out
649    }
650}
651
652// ─── Apply-to-data methods ────────────────────────────────────────────────────
653
654impl Mask {
655    /// Replaces every masked pixel in `data` with `nodata_value`.
656    ///
657    /// Operates on an `f32` slice of the same shape as the mask.
658    /// Pixels where the mask bit is **set** (invalid) are written with
659    /// `nodata_value`.
660    pub fn apply_to_f32(&self, data: &mut [f32], nodata_value: f32) {
661        let n = self.pixel_count().min(data.len());
662        for (i, elem) in data.iter_mut().enumerate().take(n) {
663            let (word, bit) = Self::coords(i);
664            if word < self.data.len() && (self.data[word] >> bit) & 1 == 1 {
665                *elem = nodata_value;
666            }
667        }
668    }
669
670    /// Replaces every masked pixel in `data` with `nodata_value`.
671    ///
672    /// Operates on an `f64` slice of the same shape as the mask.
673    pub fn apply_to_f64(&self, data: &mut [f64], nodata_value: f64) {
674        let n = self.pixel_count().min(data.len());
675        for (i, elem) in data.iter_mut().enumerate().take(n) {
676            let (word, bit) = Self::coords(i);
677            if word < self.data.len() && (self.data[word] >> bit) & 1 == 1 {
678                *elem = nodata_value;
679            }
680        }
681    }
682
683    /// Replaces every masked pixel in `data` with `nodata_value`.
684    ///
685    /// Operates on a `u8` slice of the same shape as the mask.
686    pub fn apply_to_u8(&self, data: &mut [u8], nodata_value: u8) {
687        let n = self.pixel_count().min(data.len());
688        for (i, elem) in data.iter_mut().enumerate().take(n) {
689            let (word, bit) = Self::coords(i);
690            if word < self.data.len() && (self.data[word] >> bit) & 1 == 1 {
691                *elem = nodata_value;
692            }
693        }
694    }
695
696    /// Replaces every masked pixel in `data` with `nodata_value`.
697    ///
698    /// Operates on an `i32` slice of the same shape as the mask.
699    pub fn apply_to_i32(&self, data: &mut [i32], nodata_value: i32) {
700        let n = self.pixel_count().min(data.len());
701        for (i, elem) in data.iter_mut().enumerate().take(n) {
702            let (word, bit) = Self::coords(i);
703            if word < self.data.len() && (self.data[word] >> bit) & 1 == 1 {
704                *elem = nodata_value;
705            }
706        }
707    }
708}
709
710// ─── Tests ────────────────────────────────────────────────────────────────────
711
712#[cfg(test)]
713mod tests {
714    #![allow(clippy::expect_used)]
715
716    use super::*;
717
718    // ── Basic construction ─────────────────────────────────────────────────
719
720    #[test]
721    fn test_mask_basic_set_get() {
722        let mut mask = Mask::new(8, 8);
723        // All zero initially.
724        for y in 0..8 {
725            for x in 0..8 {
726                assert!(!mask.get(x, y), "({x},{y}) should be unset");
727            }
728        }
729        mask.set(0, 0, true);
730        mask.set(7, 7, true);
731        mask.set(3, 5, true);
732        assert!(mask.get(0, 0));
733        assert!(mask.get(7, 7));
734        assert!(mask.get(3, 5));
735        assert!(!mask.get(1, 1));
736        assert_eq!(mask.count_set(), 3);
737    }
738
739    #[test]
740    fn test_mask_fill_true() {
741        let mut mask = Mask::new(10, 10);
742        mask.fill(true);
743        assert_eq!(mask.count_set(), 100);
744        assert_eq!(mask.count_unset(), 0);
745    }
746
747    #[test]
748    fn test_mask_fill_false() {
749        let mut mask = Mask::new_filled(5, 5);
750        mask.fill(false);
751        assert_eq!(mask.count_set(), 0);
752        assert!(mask.all_unset());
753    }
754
755    #[test]
756    fn test_mask_fill_rect() {
757        let mut mask = Mask::new(10, 10);
758        // Fill the 3×3 block starting at (2,2).
759        mask.fill_rect(2, 2, 3, 3, true);
760        assert_eq!(mask.count_set(), 9);
761        // Verify interior.
762        for dy in 0..3 {
763            for dx in 0..3 {
764                assert!(mask.get(2 + dx, 2 + dy));
765            }
766        }
767        // Verify boundary untouched.
768        assert!(!mask.get(1, 2));
769        assert!(!mask.get(5, 5));
770    }
771
772    // ── Bitwise operations ─────────────────────────────────────────────────
773
774    #[test]
775    fn test_mask_and_or_not() {
776        let mut a = Mask::new(4, 4); // all 0
777        a.set(0, 0, true);
778        a.set(1, 1, true);
779
780        let mut b = Mask::new(4, 4); // all 0
781        b.set(1, 1, true);
782        b.set(2, 2, true);
783
784        // AND: only (1,1) common.
785        let and = a.and(&b).expect("and should succeed");
786        assert_eq!(and.count_set(), 1);
787        assert!(and.get(1, 1));
788
789        // OR: (0,0),(1,1),(2,2) = 3 pixels.
790        let or = a.or(&b).expect("or should succeed");
791        assert_eq!(or.count_set(), 3);
792
793        // NOT of a: 16 - 2 = 14 set.
794        let not_a = a.not();
795        assert_eq!(not_a.count_set(), 14);
796        assert!(!not_a.get(0, 0));
797        assert!(!not_a.get(1, 1));
798        assert!(not_a.get(0, 1));
799    }
800
801    #[test]
802    fn test_mask_dimension_mismatch_err() {
803        let mut a = Mask::new(4, 4);
804        let b = Mask::new(4, 5);
805        assert!(a.and_assign(&b).is_err());
806        assert!(a.or_assign(&b).is_err());
807        assert!(a.xor_assign(&b).is_err());
808        assert!(a.and(&b).is_err());
809        assert!(a.or(&b).is_err());
810    }
811
812    // ── from_slice roundtrip ───────────────────────────────────────────────
813
814    #[test]
815    fn test_mask_from_slice_roundtrip() {
816        let values: Vec<bool> = (0..20usize).map(|i| i % 3 == 0).collect();
817        let mask = Mask::from_slice(4, 5, &values).expect("from_slice");
818        let back = mask.to_bool_vec();
819        assert_eq!(back, values);
820    }
821
822    #[test]
823    fn test_mask_from_slice_length_err() {
824        // Wrong length should error.
825        assert!(Mask::from_slice(4, 4, &[true; 10]).is_err());
826    }
827
828    // ── Nodata constructors ────────────────────────────────────────────────
829
830    #[test]
831    fn test_mask_from_nodata_f32() {
832        let nodata = -9999.0f32;
833        let data = vec![1.0f32, nodata, f32::NAN, 3.0, nodata];
834        let mask = Mask::from_nodata_f32(&data, 5, 1, nodata).expect("from_nodata_f32");
835        // index 1 and 4 = exact nodata, index 2 = NaN (only for NaN nodata)
836        // nodata is -9999 (not NaN), so NaN at index 2 is NOT marked.
837        assert!(!mask.get(0, 0));
838        assert!(mask.get(1, 0));
839        assert!(!mask.get(2, 0)); // NaN but nodata != NaN → not masked
840        assert!(!mask.get(3, 0));
841        assert!(mask.get(4, 0));
842        assert_eq!(mask.count_set(), 2);
843    }
844
845    #[test]
846    fn test_mask_from_nodata_f32_nan_nodata() {
847        // When nodata itself is NaN, NaN pixels are masked; non-NaN are not.
848        let data = vec![1.0f32, f32::NAN, 2.0, f32::NAN];
849        let mask = Mask::from_nodata_f32(&data, 4, 1, f32::NAN).expect("from_nodata_f32 NaN");
850        assert!(!mask.get(0, 0));
851        assert!(mask.get(1, 0));
852        assert!(!mask.get(2, 0));
853        assert!(mask.get(3, 0));
854    }
855
856    #[test]
857    fn test_mask_from_nodata_u8_zero() {
858        let data: Vec<u8> = vec![0, 1, 0, 5, 0];
859        let mask = Mask::from_nodata_u8(&data, 5, 1, 0).expect("from_nodata_u8");
860        assert!(mask.get(0, 0));
861        assert!(!mask.get(1, 0));
862        assert!(mask.get(2, 0));
863        assert!(!mask.get(3, 0));
864        assert!(mask.get(4, 0));
865        assert_eq!(mask.count_set(), 3);
866    }
867
868    // ── apply_to_* ─────────────────────────────────────────────────────────
869
870    #[test]
871    fn test_mask_apply_to_f32() {
872        let mut mask = Mask::new(4, 1);
873        mask.set(1, 0, true);
874        mask.set(3, 0, true);
875        let mut data = vec![1.0f32, 2.0, 3.0, 4.0];
876        mask.apply_to_f32(&mut data, -9999.0);
877        assert!((data[0] - 1.0).abs() < 1e-7);
878        assert!((data[1] - (-9999.0)).abs() < 1e-1);
879        assert!((data[2] - 3.0).abs() < 1e-7);
880        assert!((data[3] - (-9999.0)).abs() < 1e-1);
881    }
882
883    #[test]
884    fn test_mask_apply_to_u8() {
885        let mut mask = Mask::new(3, 1);
886        mask.set(0, 0, true);
887        mask.set(2, 0, true);
888        let mut data: Vec<u8> = vec![10, 20, 30];
889        mask.apply_to_u8(&mut data, 0);
890        assert_eq!(data, vec![0, 20, 0]);
891    }
892
893    // ── count_set / popcount ────────────────────────────────────────────────
894
895    #[test]
896    fn test_mask_count_set_checkerboard() {
897        // 8×8 checkerboard: even bit indices set.
898        let values: Vec<bool> = (0..64usize).map(|i| i % 2 == 0).collect();
899        let mask = Mask::from_slice(8, 8, &values).expect("from_slice");
900        assert_eq!(mask.count_set(), 32);
901        assert_eq!(mask.count_unset(), 32);
902    }
903
904    // ── set_positions iterator ─────────────────────────────────────────────
905
906    #[test]
907    fn test_mask_set_positions_iterator() {
908        let mut mask = Mask::new(5, 5);
909        let positions = [(1usize, 0usize), (4, 2), (0, 4)];
910        for &(x, y) in &positions {
911            mask.set(x, y, true);
912        }
913        let mut collected: Vec<(usize, usize)> = mask.set_positions().collect();
914        collected.sort_unstable();
915        let mut expected = positions.to_vec();
916        expected.sort_unstable();
917        assert_eq!(collected, expected);
918    }
919
920    // ── all_set / all_unset edge cases ─────────────────────────────────────
921
922    #[test]
923    fn test_mask_all_set_all_unset() {
924        let empty = Mask::new(0, 0);
925        // Zero-pixel mask: both are vacuously true.
926        assert!(empty.all_set());
927        assert!(empty.all_unset());
928
929        let zeros = Mask::new(10, 10);
930        assert!(zeros.all_unset());
931        assert!(!zeros.all_set());
932
933        let ones = Mask::new_filled(10, 10);
934        assert!(ones.all_set());
935        assert!(!ones.all_unset());
936    }
937
938    // ── Word-boundary correctness ──────────────────────────────────────────
939
940    #[test]
941    fn test_mask_large_width_crosses_word_boundary() {
942        // width=65 means pixel (64, 0) is in word 1, bit 0.
943        let mut mask = Mask::new(65, 1);
944        mask.set(63, 0, true); // last bit of word 0
945        mask.set(64, 0, true); // first bit of word 1
946        assert!(mask.get(63, 0));
947        assert!(mask.get(64, 0));
948        assert!(!mask.get(0, 0));
949        assert_eq!(mask.count_set(), 2);
950    }
951
952    // ── Single-pixel edge case ─────────────────────────────────────────────
953
954    #[test]
955    fn test_mask_single_pixel() {
956        let mut mask = Mask::new(1, 1);
957        assert!(!mask.get(0, 0));
958        assert!(mask.all_unset());
959        mask.set(0, 0, true);
960        assert!(mask.get(0, 0));
961        assert!(mask.all_set());
962        assert_eq!(mask.count_set(), 1);
963        mask.not_in_place();
964        assert!(!mask.get(0, 0));
965        assert!(mask.all_unset());
966    }
967
968    // ── XOR ────────────────────────────────────────────────────────────────
969
970    #[test]
971    fn test_mask_xor_self_is_zero() {
972        let mut mask = Mask::new(6, 6);
973        mask.fill_rect(0, 0, 3, 3, true);
974        let clone = mask.clone();
975        mask.xor_assign(&clone).expect("xor_assign");
976        assert!(mask.all_unset());
977    }
978
979    // ── new_filled tail-word invariant ─────────────────────────────────────
980
981    #[test]
982    fn test_mask_new_filled_all_set() {
983        // Non-multiple-of-64 dimensions.
984        let mask = Mask::new_filled(9, 7); // 63 pixels
985        assert_eq!(mask.pixel_count(), 63);
986        assert_eq!(mask.count_set(), 63);
987        assert!(mask.all_set());
988    }
989
990    #[test]
991    fn test_mask_not_in_place_tail_invariant() {
992        let mut mask = Mask::new(9, 7); // 63 pixels
993        mask.not_in_place(); // should give all-set
994        assert_eq!(mask.count_set(), 63);
995        mask.not_in_place(); // back to all-unset
996        assert_eq!(mask.count_set(), 0);
997    }
998
999    // ── or_assign idempotent ───────────────────────────────────────────────
1000
1001    #[test]
1002    fn test_mask_or_idempotent() {
1003        let mut a = Mask::new(4, 4);
1004        a.fill_rect(0, 0, 2, 2, true);
1005        let b = a.clone();
1006        a.or_assign(&b).expect("or_assign");
1007        assert_eq!(a.count_set(), 4);
1008    }
1009
1010    // ── to_bool_vec ─────────────────────────────────────────────────────────
1011
1012    #[test]
1013    fn test_mask_to_bool_vec_roundtrip() {
1014        let mask = Mask::new_filled(5, 3);
1015        let bv = mask.to_bool_vec();
1016        assert_eq!(bv.len(), 15);
1017        assert!(bv.iter().all(|&b| b));
1018    }
1019}