keyset 0.1.0

A Rust tool to create pretty keyset layout diagrams using correct fonts and icons.
Documentation
use svg::node::Value;

use crate::utils::{Path, Trim};
use crate::utils::{PathSegment, RoundRect, Size};
use crate::ToSvg;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EdgeType {
    Line,
    CurveStretch,
    CurveLineCurve,
    InsetCurve,
}

pub trait KeyHelpers {
    fn start(rect: RoundRect) -> Self;

    fn corner_top_left(&mut self, rect: RoundRect);
    fn corner_top_right(&mut self, rect: RoundRect);
    fn corner_bottom_right(&mut self, rect: RoundRect);
    fn corner_bottom_left(&mut self, rect: RoundRect);

    fn edge_top(&mut self, rect: RoundRect, size: Size, typ: EdgeType, curve: f32);
    fn edge_right(&mut self, rect: RoundRect, size: Size, typ: EdgeType, curve: f32);
    fn edge_bottom(&mut self, rect: RoundRect, size: Size, typ: EdgeType, curve: f32);
    fn edge_left(&mut self, rect: RoundRect, size: Size, typ: EdgeType, curve: f32);

    fn radius(curve: f32, distance: f32) -> Size {
        let r = (curve.powf(2.) + (distance.powf(2.) / 4.)) / (2. * curve);
        Size::new(r, r)
    }
}

impl KeyHelpers for Path {
    fn start(rect: RoundRect) -> Self {
        let mut slf = Self::new();
        slf.abs_move(rect.position() + Size::new(0., rect.ry));
        slf
    }

    fn corner_top_left(&mut self, rect: RoundRect) {
        let radius = Size::new(rect.rx.abs(), rect.ry.abs());
        self.rel_arc(radius, 0., false, true, Size::new(rect.rx, -rect.ry));
    }

    fn corner_top_right(&mut self, rect: RoundRect) {
        let radius = Size::new(rect.rx.abs(), rect.ry.abs());
        self.rel_arc(radius, 0., false, true, Size::new(rect.rx, rect.ry));
    }

    fn corner_bottom_right(&mut self, rect: RoundRect) {
        let radius = Size::new(rect.rx.abs(), rect.ry.abs());
        self.rel_arc(radius, 0., false, true, Size::new(-rect.rx, rect.ry));
    }

    fn corner_bottom_left(&mut self, rect: RoundRect) {
        let radius = Size::new(rect.rx.abs(), rect.ry.abs());
        self.rel_arc(radius, 0., false, true, Size::new(-rect.rx, -rect.ry));
    }

    fn edge_top(&mut self, rect: RoundRect, size: Size, typ: EdgeType, curve: f32) {
        let rect_dx = rect.w - 2. * rect.rx;
        let size_dx = size.w - 1e3;
        let dx = rect_dx + size_dx;
        match typ {
            EdgeType::Line => self.rel_horiz_line(dx),
            EdgeType::CurveLineCurve if size_dx > 0.01 => {
                let radius = Self::radius(curve, rect_dx);
                self.rel_arc(radius, 0., false, true, Size::new(rect_dx / 2., -curve));
                self.rel_horiz_line(size_dx);
                self.rel_arc(radius, 0., false, true, Size::new(rect_dx / 2., curve));
            }
            EdgeType::CurveLineCurve | EdgeType::CurveStretch => {
                let radius = Self::radius(curve, dx);
                self.rel_arc(radius, 0., false, true, Size::new(dx, 0.));
            }
            EdgeType::InsetCurve => {
                let radius = Self::radius(curve, dx);
                self.rel_arc(radius, 0., false, false, Size::new(dx, 0.));
            }
        }
    }

    fn edge_right(&mut self, rect: RoundRect, size: Size, typ: EdgeType, curve: f32) {
        let rect_dy = rect.h - 2. * rect.ry;
        let size_dy = size.h - 1e3;
        let dy = rect_dy + size_dy;
        match typ {
            EdgeType::Line => self.rel_vert_line(dy),
            EdgeType::CurveLineCurve if size_dy > 0.01 => {
                let radius = Self::radius(curve, rect_dy);
                self.rel_arc(radius, 0., false, true, Size::new(curve, rect_dy / 2.));
                self.rel_vert_line(size_dy);
                self.rel_arc(radius, 0., false, true, Size::new(-curve, rect_dy / 2.));
            }
            EdgeType::CurveLineCurve | EdgeType::CurveStretch => {
                let radius = Self::radius(curve, dy);
                self.rel_arc(radius, 0., false, true, Size::new(0., dy));
            }
            EdgeType::InsetCurve => {
                let radius = Self::radius(curve, dy);
                self.rel_arc(radius, 0., false, false, Size::new(0., dy));
            }
        }
    }

    fn edge_bottom(&mut self, rect: RoundRect, size: Size, typ: EdgeType, curve: f32) {
        let rect_dx = rect.w - 2. * rect.rx;
        let size_dx = size.w - 1e3;
        let dx = rect_dx + size_dx;
        match typ {
            EdgeType::Line => self.rel_horiz_line(-dx),
            EdgeType::CurveLineCurve if size_dx > 0.01 => {
                let radius = Self::radius(curve, rect_dx);
                self.rel_arc(radius, 0., false, true, Size::new(-rect_dx / 2., curve));
                self.rel_horiz_line(-size_dx);
                self.rel_arc(radius, 0., false, true, Size::new(-rect_dx / 2., -curve));
            }
            EdgeType::CurveLineCurve | EdgeType::CurveStretch => {
                let radius = Self::radius(curve, dx);
                self.rel_arc(radius, 0., false, true, Size::new(-dx, 0.));
            }
            EdgeType::InsetCurve => {
                let radius = Self::radius(curve, dx);
                self.rel_arc(radius, 0., false, false, Size::new(-dx, 0.));
            }
        }
    }

    fn edge_left(&mut self, rect: RoundRect, size: Size, typ: EdgeType, curve: f32) {
        let rect_dy = rect.h - 2. * rect.ry;
        let size_dy = size.h - 1e3;
        let dy = rect_dy + size_dy;
        match typ {
            EdgeType::Line => self.rel_vert_line(-dy),
            EdgeType::CurveLineCurve if size_dy > 0.01 => {
                let radius = Self::radius(curve, rect_dy);
                self.rel_arc(radius, 0., false, true, Size::new(-curve, -rect_dy / 2.));
                self.rel_vert_line(size_dy);
                self.rel_arc(radius, 0., false, true, Size::new(curve, -rect_dy / 2.));
            }
            EdgeType::CurveLineCurve | EdgeType::CurveStretch => {
                let radius = Self::radius(curve, dy);
                self.rel_arc(radius, 0., false, true, Size::new(0., -dy));
            }
            EdgeType::InsetCurve => {
                let radius = Self::radius(curve, dy);
                self.rel_arc(radius, 0., false, false, Size::new(0., -dy));
            }
        }
    }
}

impl ToSvg for PathSegment {
    fn to_svg(&self) -> String {
        match *self {
            Self::Move(p) => format!("M{} {}", Trim(p.x), Trim(p.y)),
            Self::Line(d) => format!("l{} {}", Trim(d.w), Trim(d.h)),
            Self::CubicBezier(d1, d2, d) => format!(
                "c{} {} {} {} {} {}",
                Trim(d1.w),
                Trim(d1.h),
                Trim(d2.w),
                Trim(d2.h),
                Trim(d.w),
                Trim(d.h)
            ),
            Self::QuadraticBezier(d1, d) => {
                format!("q{} {} {} {}", Trim(d1.w), Trim(d1.h), Trim(d.w), Trim(d.h))
            }
            Self::Close => "z".into(),
        }
    }
}

impl ToSvg for Path {
    fn to_svg(&self) -> String {
        self.data
            .iter()
            .fold(String::new(), |result, seg| result + seg.to_svg().as_str())
    }
}

impl From<Path> for Value {
    fn from(value: Path) -> Self {
        value.to_svg().into()
    }
}

#[cfg(test)]
mod tests {
    use std::f32::consts::{FRAC_1_SQRT_2, SQRT_2};

    use assert_approx_eq::assert_approx_eq;
    use assert_matches::assert_matches;
    use maplit::hashmap;

    use crate::utils::Point;

    use super::*;

    #[test]
    fn test_radius() {
        assert_approx_eq!(Path::radius(1. - FRAC_1_SQRT_2, SQRT_2), Size::new(1., 1.));
    }

    #[test]
    fn test_corners() {
        let rect = RoundRect::new(200., 100., 600., 600., 50., 50.);
        let path = Path::start(rect);

        let corner_funcs: Vec<fn(&mut Path, RoundRect)> = vec![
            Path::corner_top_left,
            Path::corner_top_right,
            Path::corner_bottom_right,
            Path::corner_bottom_left,
        ];

        for func in corner_funcs {
            let mut path = path.clone();
            func(&mut path, rect);
            assert_eq!(path.data.len(), 2);
            assert_matches!(path.data[1], PathSegment::CubicBezier(..));
        }
    }

    #[test]
    fn test_edges() {
        let rect = RoundRect::new(200., 100., 600., 600., 50., 50.);
        let size = Size::new(2e3, 2e3);
        let curve = 20.;
        let path = Path::start(rect);

        let edge_funcs: Vec<fn(&mut Path, RoundRect, Size, EdgeType, f32)> = vec![
            Path::edge_top,
            Path::edge_right,
            Path::edge_bottom,
            Path::edge_left,
        ];
        let edge_type_len = hashmap! {
            EdgeType::Line => 1,
            EdgeType::CurveStretch => 1,
            EdgeType::CurveLineCurve => 3,
            EdgeType::InsetCurve => 1,
        };

        for func in edge_funcs {
            for (&edge_type, &len) in &edge_type_len {
                let mut path = path.clone();
                func(&mut path, rect, size, edge_type, curve);

                assert_eq!(path.data.len(), len + 1);
            }
        }
    }

    #[test]
    fn test_path_to_svg() {
        let mut path = Path::new();
        path.abs_move(Point::new(0., 0.));
        path.rel_line(Size::new(1., 1.));
        path.rel_cubic_bezier(Size::new(0.5, 0.5), Size::new(1.5, 0.5), Size::new(2., 0.));
        path.rel_quadratic_bezier(Size::new(0.5, -0.5), Size::new(1., 0.));
        path.close();

        assert_eq!(path.to_svg(), "M0 0l1 1c0.5 0.5 1.5 0.5 2 0q0.5 -0.5 1 0z");
    }
}