cranpose-ui-graphics 0.9.6

Geometry, units, paths and render data for Cranpose UI
Documentation
use super::*;
use crate::{BlendMode, Brush, Color, DrawPrimitive, DrawScope, DrawScopeDefault, Size};

fn stroke(width: f32, cap: StrokeCap, join: StrokeJoin) -> Stroke {
    Stroke { width, cap, join }
}

fn lines(points: &[Point], closed: bool, stroke: Stroke) -> Vec<LineGeometry> {
    let mut out = Vec::new();
    for_each_stroke_line(points, closed, stroke, |line| out.push(line));
    out
}

#[test]
fn a_path_keeps_its_contours_and_whether_each_closes() {
    let mut path = Path::new();
    assert!(path.is_empty());
    path.move_to(Point::new(0.0, 0.0));
    path.line_to(Point::new(10.0, 0.0));
    path.line_to(Point::new(10.0, 10.0));
    path.close();
    path.line_to(Point::new(-5.0, 5.0));
    let contours = path.contours();
    assert_eq!(contours.len(), 2);
    assert_eq!(
        contours[0],
        PathContour {
            points: vec![
                Point::new(0.0, 0.0),
                Point::new(10.0, 0.0),
                Point::new(10.0, 10.0)
            ],
            closed: true,
        }
    );
    assert_eq!(
        contours[1].points,
        vec![Point::new(0.0, 0.0), Point::new(-5.0, 5.0)],
        "an edge after a close starts at the closed contour's first point"
    );
    assert!(!path.is_empty());
    assert_eq!(
        path.bounds(),
        Rect {
            x: -5.0,
            y: 0.0,
            width: 15.0,
            height: 10.0,
        }
    );
    path.reset();
    assert!(path.contours().is_empty());
    assert_eq!(path.bounds(), Rect::EMPTY);
}

#[test]
fn curves_flatten_to_points_on_the_curve() {
    let mut path = Path::new();
    path.move_to(Point::new(0.0, 0.0));
    path.quadratic_to(Point::new(50.0, 100.0), Point::new(100.0, 0.0));
    let quadratic = &path.contours()[0].points;
    assert!(quadratic.len() > 4, "a curve flattens to several edges");
    assert_eq!(quadratic.last(), Some(&Point::new(100.0, 0.0)));
    let peak = quadratic.iter().map(|point| point.y).fold(0.0, f32::max);
    assert!(
        (peak - 50.0).abs() < 1.0,
        "the quadratic peaks halfway to its control: {peak}"
    );

    let mut path = Path::new();
    path.move_to(Point::new(0.0, 0.0));
    path.cubic_to(
        Point::new(0.0, 60.0),
        Point::new(60.0, 60.0),
        Point::new(60.0, 0.0),
    );
    let cubic = &path.contours()[0].points;
    assert_eq!(cubic.first(), Some(&Point::new(0.0, 0.0)));
    assert_eq!(cubic.last(), Some(&Point::new(60.0, 0.0)));
    let peak = cubic.iter().map(|point| point.y).fold(0.0, f32::max);
    assert!(
        (peak - 45.0).abs() < 1.0,
        "the cubic peaks at three quarters: {peak}"
    );
}

#[test]
fn a_filled_path_closes_every_contour_into_a_vector_path() {
    let mut path = Path::new();
    path.move_to(Point::new(0.0, 0.0));
    path.line_to(Point::new(10.0, 0.0));
    path.line_to(Point::new(10.0, 10.0));
    let fill = path.to_vector_path(PathFillRule::EvenOdd);
    assert_eq!(fill.subpaths().len(), 1);
    assert_eq!(fill.fill_rule(), PathFillRule::EvenOdd);
    assert!(!fill.is_empty());
}

#[test]
fn dashes_follow_the_pattern_from_its_phase_across_corners() {
    let dash = DashPathEffect::new(&[4.0, 2.0], 5.0);
    assert_eq!(dash.intervals(), &[4.0, 2.0]);
    let mut dashes = Vec::new();
    dash.for_each_dash(
        &[
            Point::new(0.0, 0.0),
            Point::new(10.0, 0.0),
            Point::new(10.0, 6.0),
        ],
        |piece| {
            dashes.push(piece.to_vec());
        },
    );
    assert_eq!(
        dashes,
        vec![
            vec![Point::new(1.0, 0.0), Point::new(5.0, 0.0)],
            vec![
                Point::new(7.0, 0.0),
                Point::new(10.0, 0.0),
                Point::new(10.0, 1.0)
            ],
            vec![Point::new(10.0, 3.0), Point::new(10.0, 6.0)],
        ],
        "five into the pattern the walk starts one unit into a gap; the middle dash turns the corner"
    );
}

#[test]
fn a_pattern_that_cannot_dash_draws_the_polyline_whole() {
    let points = [Point::new(0.0, 0.0), Point::new(10.0, 0.0)];
    for dash in [
        DashPathEffect::new(&[4.0], 0.0),
        DashPathEffect::new(&[0.0, 0.0], 0.0),
    ] {
        let mut pieces = Vec::new();
        dash.for_each_dash(&points, |piece| pieces.push(piece.to_vec()));
        assert_eq!(pieces, vec![points.to_vec()]);
    }
}

#[test]
fn a_round_stroke_draws_every_edge_with_round_ends() {
    let points = [
        Point::new(0.0, 0.0),
        Point::new(10.0, 0.0),
        Point::new(10.0, 10.0),
    ];
    let drawn = lines(
        &points,
        false,
        stroke(2.0, StrokeCap::Round, StrokeJoin::Round),
    );
    assert_eq!(drawn.len(), 2);
    assert!(drawn.iter().all(|line| line.cap == StrokeCap::Round));
    assert_eq!(
        (drawn[0].start, drawn[0].end),
        (Point::new(0.0, 0.0), Point::new(10.0, 0.0))
    );
}

#[test]
fn a_butt_capped_mitred_stroke_grows_its_inner_ends_by_half_the_width() {
    let points = [
        Point::new(0.0, 0.0),
        Point::new(10.0, 0.0),
        Point::new(10.0, 10.0),
    ];
    let drawn = lines(
        &points,
        false,
        stroke(2.0, StrokeCap::Butt, StrokeJoin::Miter),
    );
    assert_eq!(
        drawn
            .iter()
            .map(|line| (line.start, line.end, line.cap))
            .collect::<Vec<_>>(),
        vec![
            (Point::new(0.0, 0.0), Point::new(11.0, 0.0), StrokeCap::Butt),
            (
                Point::new(10.0, -1.0),
                Point::new(10.0, 10.0),
                StrokeCap::Butt
            ),
        ],
        "the corner is covered as a miter covers a right angle; the open ends stay butt"
    );
}

#[test]
fn a_round_join_on_a_butt_stroke_adds_a_dot_at_each_corner() {
    let points = [
        Point::new(0.0, 0.0),
        Point::new(10.0, 0.0),
        Point::new(10.0, 10.0),
    ];
    let drawn = lines(
        &points,
        false,
        stroke(2.0, StrokeCap::Butt, StrokeJoin::Round),
    );
    let dots: Vec<Point> = drawn
        .iter()
        .filter(|line| line.start == line.end)
        .map(|line| line.start)
        .collect();
    assert_eq!(dots, vec![Point::new(10.0, 0.0), Point::new(10.0, 0.0)]);
    assert!(
        drawn
            .iter()
            .filter(|line| line.start != line.end)
            .all(|line| line.cap == StrokeCap::Butt)
    );
}

#[test]
fn a_closed_contour_strokes_its_closing_edge_with_corners_all_round() {
    let points = [
        Point::new(0.0, 0.0),
        Point::new(10.0, 0.0),
        Point::new(10.0, 10.0),
    ];
    let drawn = lines(
        &points,
        true,
        stroke(2.0, StrokeCap::Butt, StrokeJoin::Round),
    );
    assert_eq!(drawn.len(), 3, "three edges, closing edge included");
    assert!(drawn.iter().all(|line| line.cap == StrokeCap::Round));
    assert_eq!(
        (drawn[2].start, drawn[2].end),
        (Point::new(10.0, 10.0), Point::new(0.0, 0.0))
    );
}

fn primitives(draw: impl FnOnce(&mut DrawScopeDefault)) -> Vec<DrawPrimitive> {
    let mut scope = DrawScopeDefault::new(Size::new(64.0, 64.0));
    draw(&mut scope);
    scope.into_primitives()
}

#[test]
fn a_drawn_path_strokes_as_lines_and_fills_as_an_image() {
    let mut path = Path::new();
    path.move_to(Point::new(4.0, 4.0));
    path.line_to(Point::new(40.0, 4.0));
    path.line_to(Point::new(40.0, 40.0));
    let brush = Brush::solid(Color::WHITE);
    let stroked = primitives(|scope| {
        scope.draw_path(
            &path,
            brush.clone(),
            DrawStyle::Stroke(stroke(2.0, StrokeCap::Round, StrokeJoin::Round)),
        );
    });
    assert_eq!(stroked.len(), 2);
    assert!(stroked.iter().all(|primitive| matches!(
        primitive,
        DrawPrimitive::Line { stroke, .. } if stroke.cap == StrokeCap::Round
    )));
    let filled = primitives(|scope| scope.draw_path(&path, brush.clone(), DrawStyle::Fill));
    assert!(matches!(filled.as_slice(), [DrawPrimitive::Image { .. }]));
    let blended = primitives(|scope| {
        scope.draw_path_blend(&path, brush.clone(), DrawStyle::Fill, BlendMode::Plus);
    });
    assert!(matches!(
        blended.as_slice(),
        [DrawPrimitive::Blend {
            blend_mode: BlendMode::Plus,
            ..
        }]
    ));
    let dashed = primitives(|scope| {
        scope.draw_path(
            &path,
            brush.clone(),
            DrawStyle::DashedStroke(
                stroke(2.0, StrokeCap::Round, StrokeJoin::Round),
                DashPathEffect::new(&[6.0, 6.0], 0.0),
            ),
        );
    });
    assert_eq!(
        dashed.len(),
        6,
        "72 units of edge in 12-unit periods: six dashes, three on each edge"
    );
}