sequoia-draw 0.1.1

Sequoia GUI framework — vector paths and rasterization
Documentation
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;

use sequoia_core::{Point, Rect};

use crate::path::{FillRule, Path};
use crate::raster::{self, FillStyle, RasterOptions};
use crate::stroke::{self, StrokeStyle};
use crate::{Span, union_rect};

#[derive(Clone, Debug, PartialEq)]
pub struct Shape {
    pub spans: Rc<[Span]>,

    pub bounds: Rect,
}

impl Shape {
    pub fn new(spans: Vec<Span>) -> Self {
        let mut bounds = Rect::ZERO;
        for span in &spans {
            bounds = union_rect(bounds, span.rect);
        }
        Self {
            spans: spans.into(),
            bounds,
        }
    }

    pub fn empty() -> Self {
        Self {
            spans: Rc::from(Vec::new()),
            bounds: Rect::ZERO,
        }
    }

    pub fn len(&self) -> usize {
        self.spans.len()
    }

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

    pub fn iter(&self) -> std::slice::Iter<'_, Span> {
        self.spans.iter()
    }
}

impl<'a> IntoIterator for &'a Shape {
    type Item = &'a Span;
    type IntoIter = std::slice::Iter<'a, Span>;

    fn into_iter(self) -> Self::IntoIter {
        self.spans.iter()
    }
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ShapeStats {
    pub hits: u64,

    pub misses: u64,

    pub evictions: u64,
}

impl ShapeStats {
    pub fn hit_rate(&self) -> f32 {
        let total = self.hits + self.misses;
        if total == 0 {
            0.0
        } else {
            self.hits as f32 / total as f32
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct Key {
    kind: u8,
    fingerprint: u64,
    style: u64,
    opts: u64,
}

#[derive(Debug)]
struct Entry {
    shape: Rc<Shape>,
    used: u64,
}

pub const DEFAULT_CAPACITY: usize = 256;

#[derive(Debug)]
pub struct ShapeCache {
    inner: RefCell<Inner>,
}

#[derive(Debug)]
struct Inner {
    entries: HashMap<Key, Entry>,
    capacity: usize,
    tick: u64,
    stats: ShapeStats,
}

impl Default for ShapeCache {
    fn default() -> Self {
        Self::new(DEFAULT_CAPACITY)
    }
}

impl ShapeCache {
    pub fn new(capacity: usize) -> Self {
        Self {
            inner: RefCell::new(Inner {
                entries: HashMap::new(),
                capacity,
                tick: 0,
                stats: ShapeStats::default(),
            }),
        }
    }

    pub fn capacity(&self) -> usize {
        self.inner.borrow().capacity
    }

    pub fn len(&self) -> usize {
        self.inner.borrow().entries.len()
    }

    pub fn is_empty(&self) -> bool {
        self.inner.borrow().entries.is_empty()
    }

    pub fn stats(&self) -> ShapeStats {
        self.inner.borrow().stats
    }

    pub fn reset_stats(&self) {
        self.inner.borrow_mut().stats = ShapeStats::default();
    }

    pub fn clear(&self) {
        self.inner.borrow_mut().entries.clear();
    }

    pub fn fill(&self, path: &Path, rule: FillRule, opts: RasterOptions) -> Rc<Shape> {
        self.fill_styled(path, FillStyle::new(rule, opts))
    }

    pub fn fill_styled(&self, path: &Path, style: FillStyle) -> Rc<Shape> {
        let key = Key {
            kind: 0,
            fingerprint: path.fingerprint(),
            style: style.rule as u64,
            opts: opts_key(style.opts),
        };
        self.get_or_build(key, |out| {
            raster::fill(path, style.rule, style.opts, out);
        })
    }

    pub fn stroke(&self, path: &Path, style: &StrokeStyle, opts: RasterOptions) -> Rc<Shape> {
        let key = Key {
            kind: 1,
            fingerprint: path.fingerprint(),
            style: style.fingerprint(),
            opts: opts_key(opts),
        };
        self.get_or_build(key, |out| {
            stroke::stroke_path(path, style, opts, out);
        })
    }

    pub fn fill_contours(
        &self,
        contours: &[crate::Contour],
        rule: FillRule,
        opts: RasterOptions,
    ) -> Rc<Shape> {
        let key = Key {
            kind: 2,
            fingerprint: contours_fingerprint(contours),
            style: rule as u64,
            opts: opts_key(opts),
        };
        self.get_or_build(key, |out| {
            raster::fill_contours(contours, rule, opts, out);
        })
    }

    pub fn disk(&self, center: Point, radius: f32) -> Rc<Shape> {
        if !(radius > 0.0) || !radius.is_finite() {
            return Rc::new(Shape::empty());
        }
        Rc::new(Shape::new(vec![Span::rounded(
            Rect::from_xywh(
                center.x - radius,
                center.y - radius,
                radius * 2.0,
                radius * 2.0,
            ),
            radius,
        )]))
    }

    pub fn rounded_rect(&self, rect: Rect, radius: f32) -> Rc<Shape> {
        let radius = radius
            .clamp(0.0, rect.size.width.min(rect.size.height) * 0.5)
            .max(0.0);
        Rc::new(Shape::new(vec![Span::rounded(rect, radius)]))
    }

    pub fn rect(&self, rect: Rect) -> Rc<Shape> {
        Rc::new(Shape::new(vec![Span::rect(rect)]))
    }

    fn get_or_build(&self, key: Key, build: impl FnOnce(&mut Vec<Span>)) -> Rc<Shape> {
        let mut inner = self.inner.borrow_mut();
        inner.tick += 1;
        let tick = inner.tick;
        if let Some(entry) = inner.entries.get_mut(&key) {
            entry.used = tick;
            let shape = entry.shape.clone();
            inner.stats.hits += 1;
            return shape;
        }
        inner.stats.misses += 1;
        if inner.capacity == 0 {
            drop(inner);
            let mut spans = Vec::new();
            build(&mut spans);
            return Rc::new(Shape::new(spans));
        }

        let mut spans = Vec::new();
        build(&mut spans);
        let shape = Rc::new(Shape::new(spans));
        if inner.entries.len() >= inner.capacity
            && let Some(oldest) = inner
                .entries
                .iter()
                .min_by_key(|(_, entry)| entry.used)
                .map(|(key, _)| *key)
        {
            inner.entries.remove(&oldest);
            inner.stats.evictions += 1;
        }
        inner.entries.insert(
            key,
            Entry {
                shape: shape.clone(),
                used: tick,
            },
        );
        shape
    }
}

fn opts_key(opts: RasterOptions) -> u64 {
    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
    for v in [opts.tolerance, opts.row_height, opts.merge_tol] {
        hash ^= u64::from(v.to_bits());
        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    }
    hash ^= u64::from(opts.aa_samples);
    hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    hash ^= u64::from(opts.analytic);
    hash.wrapping_mul(0x0000_0100_0000_01b3)
}

fn contours_fingerprint(contours: &[crate::Contour]) -> u64 {
    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
    let mut eat = |v: u64| {
        hash ^= v;
        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    };
    for c in contours {
        eat(c.points.len() as u64);
        eat(u64::from(c.closed));
        for p in &c.points {
            eat(u64::from(p.x.to_bits()));
            eat(u64::from(p.y.to_bits()));
        }
    }
    hash
}