71 lines
3.4 KiB
Rust
71 lines
3.4 KiB
Rust
//! One-sided variable widths in stroke outlines: each side offset by its own width, lopsided
|
||
//! caps around the middle of the end, dashes and closed paths.
|
||
|
||
use figcraft_doc::{StrokeAlign, StrokeCap, StrokeProps, WidthPoint, WidthProfile};
|
||
use figcraft_geom::kurbo::{BezPath, Shape};
|
||
|
||
use crate::stroke_outline;
|
||
|
||
/// A horizontal line from (0, 0) to (100, 0): looking along it, its right side is +y (y down).
|
||
fn line() -> BezPath {
|
||
let mut p = BezPath::new();
|
||
p.move_to((0.0, 0.0));
|
||
p.line_to((100.0, 0.0));
|
||
p
|
||
}
|
||
|
||
fn props(points: Vec<WidthPoint>) -> StrokeProps {
|
||
StrokeProps { weight: 10.0, align: StrokeAlign::Center, width_profile: Some(WidthProfile { points }.sanitized()), ..Default::default() }
|
||
}
|
||
|
||
#[test]
|
||
fn each_side_is_offset_by_its_own_width() {
|
||
// All on the right: 0 to the left, 10 to the right (side scale 2 × weight 10 / 2).
|
||
let p = props(vec![WidthPoint::from_sides(0.0, 0.0, 2.0), WidthPoint::from_sides(1.0, 0.0, 2.0)]);
|
||
let b = stroke_outline(&line(), &p, false).bounding_box();
|
||
assert!(b.y0.abs() < 1e-6 && (b.y1 - 10.0).abs() < 1e-6 && b.x0.abs() < 1e-6 && (b.x1 - 100.0).abs() < 1e-6, "{b:?}");
|
||
assert!((stroke_outline(&line(), &p, false).area().abs() - 1000.0).abs() < 1.0);
|
||
// Lopsided: 2 on the left, 8 on the right.
|
||
let p = props(vec![WidthPoint::from_sides(0.5, 0.4, 1.6)]);
|
||
let b = stroke_outline(&line(), &p, false).bounding_box();
|
||
assert!((b.y0 + 2.0).abs() < 1e-6 && (b.y1 - 8.0).abs() < 1e-6, "{b:?}");
|
||
// The right side swells in the middle while the left stays straight.
|
||
let p = props(vec![WidthPoint::new(0.0, 1.0), WidthPoint::from_sides(0.5, 1.0, 4.0), WidthPoint::new(1.0, 1.0)]);
|
||
let b = stroke_outline(&line(), &p, false).bounding_box();
|
||
assert!((b.y0 + 5.0).abs() < 1e-6 && (b.y1 - 20.0).abs() < 1e-6, "{b:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn caps_wrap_the_middle_of_a_lopsided_end() {
|
||
// 0 on the left, 10 on the right: a round cap is a half circle of radius 5 around (100, 5).
|
||
let mut p = props(vec![WidthPoint::from_sides(0.0, 0.0, 2.0), WidthPoint::from_sides(1.0, 0.0, 2.0)]);
|
||
p.cap_start = StrokeCap::Round;
|
||
p.cap_end = StrokeCap::Round;
|
||
let b = stroke_outline(&line(), &p, false).bounding_box();
|
||
assert!((b.x1 - 105.0).abs() < 0.05 && (b.x0 + 5.0).abs() < 0.05, "{b:?}");
|
||
assert!(b.y0 > -0.05 && (b.y1 - 10.0).abs() < 0.05, "{b:?}");
|
||
p.cap_end = StrokeCap::Square;
|
||
let b = stroke_outline(&line(), &p, false).bounding_box();
|
||
assert!((b.x1 - 105.0).abs() < 1e-6 && b.y0 > -1e-6, "{b:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn dashes_and_closed_paths_keep_their_sides() {
|
||
let mut p = props(vec![WidthPoint::from_sides(0.0, 0.0, 2.0), WidthPoint::from_sides(1.0, 0.0, 2.0)]);
|
||
p.dashes = vec![10.0, 10.0];
|
||
let s = stroke_outline(&line(), &p, false);
|
||
let b = s.bounding_box();
|
||
assert!(b.y0.abs() < 1e-6 && (b.y1 - 10.0).abs() < 1e-6, "{b:?}");
|
||
assert!((s.area().abs() - 500.0).abs() < 5.0, "{}", s.area());
|
||
// A closed square drawn clockwise on screen: its right side is the inside.
|
||
let mut sq = BezPath::new();
|
||
sq.move_to((0.0, 0.0));
|
||
sq.line_to((100.0, 0.0));
|
||
sq.line_to((100.0, 100.0));
|
||
sq.line_to((0.0, 100.0));
|
||
sq.close_path();
|
||
let p = props(vec![WidthPoint::from_sides(0.0, 0.0, 2.0), WidthPoint::from_sides(1.0, 0.0, 2.0)]);
|
||
let b = stroke_outline(&sq, &p, true).bounding_box();
|
||
assert!(b.x0 > -1e-6 && b.y0 > -1e-6 && b.x1 < 100.0 + 1e-6 && b.y1 < 100.0 + 1e-6, "all inside: {b:?}");
|
||
}
|