sequoia-draw 0.1.0

Sequoia GUI framework — vector paths and rasterization
Documentation
use sequoia_core::Point;

use crate::TOLERANCE;
use crate::path::{Path, PathSeg};

#[derive(Clone, Debug, Default, PartialEq)]
pub struct Contour {
    pub points: Vec<Point>,

    pub closed: bool,
}

impl Contour {
    pub fn is_degenerate(&self) -> bool {
        self.points.len() < 2
    }

    pub fn edge_count(&self) -> usize {
        if self.closed {
            self.points.len()
        } else {
            self.points.len().saturating_sub(1)
        }
    }
}

const MAX_SUBDIVISION: u32 = 4096;

pub fn flatten(path: &Path, tolerance: f32) -> Vec<Contour> {
    let tol = if tolerance.is_finite() {
        tolerance.max(0.01)
    } else {
        TOLERANCE
    };
    let mut out: Vec<Contour> = Vec::new();
    let mut current = Contour::default();
    let mut pen = Point::ZERO;
    let mut start = Point::ZERO;
    let mut open = false;

    for seg in path.segs() {
        match *seg {
            PathSeg::Move(p) => {
                finish(&mut current, &mut out, open);
                current = Contour {
                    points: vec![p],
                    closed: false,
                };
                pen = p;
                start = p;
                open = true;
            }
            PathSeg::Line(p) => {
                if !open {
                    begin(&mut current, pen);
                    start = pen;
                    open = true;
                }
                push_point(&mut current.points, p);
                pen = p;
            }
            PathSeg::Quad(c, p) => {
                if !open {
                    begin(&mut current, pen);
                    start = pen;
                    open = true;
                }
                flatten_quad(&mut current.points, pen, c, p, tol);
                pen = p;
            }
            PathSeg::Cubic(c1, c2, p) => {
                if !open {
                    begin(&mut current, pen);
                    start = pen;
                    open = true;
                }
                flatten_cubic(&mut current.points, pen, c1, c2, p, tol);
                pen = p;
            }
            PathSeg::Close => {
                if open {
                    push_point(&mut current.points, start);
                    current.closed = true;
                    finish(&mut current, &mut out, true);
                    current = Contour::default();
                    pen = start;
                    open = false;
                }
            }
        }
    }

    finish(&mut current, &mut out, open);
    out.retain(|c| !c.is_degenerate());
    out
}

fn begin(current: &mut Contour, pen: Point) {
    current.points.clear();
    current.points.push(pen);
}

fn finish(current: &mut Contour, out: &mut Vec<Contour>, open: bool) {
    if open && current.points.len() >= 2 {
        let mut contour = std::mem::take(current);
        contour.points.dedup_by(|a, b| a == b);
        if contour.points.len() >= 2 {
            out.push(contour);
        }
    } else {
        current.points.clear();
    }
}

fn push_point(points: &mut Vec<Point>, p: Point) {
    if points.last() != Some(&p) {
        points.push(p);
    }
}

pub(crate) fn flatten_quad(out: &mut Vec<Point>, p0: Point, c: Point, p2: Point, tol: f32) {
    let mid = Point::new((p0.x + p2.x) * 0.5, (p0.y + p2.y) * 0.5);
    let dev = ((c.x - mid.x).powi(2) + (c.y - mid.y).powi(2)).sqrt() * 0.5;
    if dev <= tol {
        push_point(out, p2);
        return;
    }

    let p01 = lerp(p0, c, 0.5);
    let p12 = lerp(c, p2, 0.5);
    let m = lerp(p01, p12, 0.5);
    flatten_quad(out, p0, p01, m, tol);
    flatten_quad(out, m, p12, p2, tol);
}

pub(crate) fn flatten_cubic(
    out: &mut Vec<Point>,
    p0: Point,
    c1: Point,
    c2: Point,
    p3: Point,
    tol: f32,
) {
    let mut steps = 0;
    flatten_cubic_rec(out, p0, c1, c2, p3, tol, &mut steps);
}

fn flatten_cubic_rec(
    out: &mut Vec<Point>,
    p0: Point,
    c1: Point,
    c2: Point,
    p3: Point,
    tol: f32,
    steps: &mut u32,
) {
    if *steps >= MAX_SUBDIVISION {
        push_point(out, p3);
        return;
    }
    let d1 = dist_to_segment(c1, p0, p3);
    let d2 = dist_to_segment(c2, p0, p3);
    if (d1.max(d2)) * 0.75 <= tol {
        push_point(out, p3);
        return;
    }
    *steps += 1;
    let p01 = lerp(p0, c1, 0.5);
    let p12 = lerp(c1, c2, 0.5);
    let p23 = lerp(c2, p3, 0.5);
    let p012 = lerp(p01, p12, 0.5);
    let p123 = lerp(p12, p23, 0.5);
    let m = lerp(p012, p123, 0.5);
    flatten_cubic_rec(out, p0, p01, p012, m, tol, steps);
    flatten_cubic_rec(out, m, p123, p23, p3, tol, steps);
}

fn lerp(a: Point, b: Point, t: f32) -> Point {
    Point::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t)
}

pub(crate) fn dist_to_segment(p: Point, a: Point, b: Point) -> f32 {
    let abx = b.x - a.x;
    let aby = b.y - a.y;
    let len2 = abx * abx + aby * aby;
    if len2 <= f32::MIN_POSITIVE {
        return ((p.x - a.x).powi(2) + (p.y - a.y).powi(2)).sqrt();
    }
    let t = (((p.x - a.x) * abx + (p.y - a.y) * aby) / len2).clamp(0.0, 1.0);
    let dx = p.x - (a.x + abx * t);
    let dy = p.y - (a.y + aby * t);
    (dx * dx + dy * dy).sqrt()
}

pub(crate) fn arc_steps(radius: f32, span_rad: f32, tol: f32) -> usize {
    if radius <= tol || !span_rad.is_finite() {
        return 1;
    }
    let cos_half = (1.0 - tol / radius).clamp(-1.0, 1.0);
    let max_step = 2.0 * cos_half.acos();
    if max_step <= 0.0 {
        return 1;
    }
    ((span_rad.abs() / max_step).ceil() as usize).clamp(1, MAX_SUBDIVISION as usize)
}