standard-plugin-sdk 0.1.1

Write Standard Code plugins in Rust: wasm components against standard:plugin@2.0.0
Documentation
//! Dirty-rectangle accounting: every write records the rectangle it
//! touched; a commit sends the merged set, so the viewer reads only what
//! changed.

use alloc::vec::Vec;

use crate::geometry::Rect;

/// Past this many rectangles the set collapses to their bounding box: the
/// viewer merges beyond 64 anyway, and a long list costs more to walk than
/// the extra cells cost to read.
pub(crate) const MAX_RECTS: usize = 16;

#[derive(Clone, Debug, Default)]
pub(crate) struct DirtyRects {
    rects: Vec<Rect>,
}

impl DirtyRects {
    /// Records `rect`, clipped to a `width` by `height` surface.
    pub(crate) fn add(&mut self, rect: Rect, width: u32, height: u32) {
        let Some(mut rect) = rect.clamp(width, height) else {
            return;
        };
        if self
            .rects
            .iter()
            .any(|existing| existing.contains_rect(&rect))
        {
            return;
        }
        // Absorb what the new rectangle covers or extends exactly (same
        // rows side by side, same columns stacked), until nothing changes.
        loop {
            let before = self.rects.len();
            self.rects.retain(|existing| {
                if rect.contains_rect(existing) {
                    return false;
                }
                let same_rows = existing.y == rect.y && existing.h == rect.h;
                let same_cols = existing.x == rect.x && existing.w == rect.w;
                let touch_x = existing.x <= rect.right() && rect.x <= existing.right();
                let touch_y = existing.y <= rect.bottom() && rect.y <= existing.bottom();
                if (same_rows && touch_x) || (same_cols && touch_y) {
                    rect = rect.union(existing);
                    return false;
                }
                true
            });
            if self.rects.len() == before {
                break;
            }
        }
        self.rects.push(rect);
        if self.rects.len() > MAX_RECTS {
            let bounds = self
                .rects
                .iter()
                .skip(1)
                .fold(self.rects[0], |bounds, rect| bounds.union(rect));
            self.rects.clear();
            self.rects.push(bounds);
        }
    }

    pub(crate) fn is_empty(&self) -> bool {
        self.rects.is_empty()
    }

    pub(crate) fn rects(&self) -> &[Rect] {
        &self.rects
    }

    pub(crate) fn take(&mut self) -> Vec<Rect> {
        core::mem::take(&mut self.rects)
    }

    pub(crate) fn clear(&mut self) {
        self.rects.clear();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn covered(rects: &[Rect], x: u32, y: u32) -> bool {
        rects
            .iter()
            .any(|r| x >= r.x && x < r.right() && y >= r.y && y < r.bottom())
    }

    #[test]
    fn dirty_rects_cover_every_write_merge_neighbours_and_stay_in_bounds() {
        let mut dirty = DirtyRects::default();
        // Three cells of one row written one by one become one rectangle.
        for x in 2..5 {
            dirty.add(Rect::new(x, 1, 1, 1), 10, 4);
        }
        assert_eq!(dirty.rects(), &[Rect::new(2, 1, 3, 1)]);
        // A contained write adds nothing; a covering one absorbs.
        dirty.add(Rect::new(3, 1, 1, 1), 10, 4);
        assert_eq!(dirty.rects().len(), 1);
        dirty.add(Rect::new(0, 0, 10, 2), 10, 4);
        assert_eq!(dirty.rects(), &[Rect::new(0, 0, 10, 2)]);
        // Out of bounds is clipped or dropped.
        dirty.add(Rect::new(8, 3, 100, 100), 10, 4);
        dirty.add(Rect::new(10, 0, 1, 1), 10, 4);
        dirty.add(Rect::new(u32::MAX, u32::MAX, u32::MAX, u32::MAX), 10, 4);
        assert!(dirty.rects().contains(&Rect::new(8, 3, 2, 1)));
        assert!(
            dirty
                .rects()
                .iter()
                .all(|r| r.right() <= 10 && r.bottom() <= 4)
        );

        // Scattered writes: every written cell stays covered, and the list
        // is bounded.
        let mut dirty = DirtyRects::default();
        let mut written = Vec::new();
        for i in 0..40u32 {
            let (x, y) = ((i * 7) % 50, (i * 13) % 30);
            dirty.add(Rect::new(x, y, 1, 1), 50, 30);
            written.push((x, y));
            assert!(dirty.rects().len() <= MAX_RECTS);
        }
        let rects = dirty.take();
        for (x, y) in written {
            assert!(covered(&rects, x, y), "({x}, {y}) lost");
        }
        assert!(dirty.is_empty());
    }
}