use glam::Vec2;
use impeller_geometry::stroke::{LineCap, LineJoin, StrokeStyle};
use impeller_geometry::tessellate::Tessellator;
use impeller_geometry::{FillRule, Path, PathBuilder, MAX_COORDINATE};
use proptest::prelude::*;
fn hostile_coord() -> impl Strategy<Value = f32> {
prop_oneof![
8 => -2_000.0f32..2_000.0f32,
1 => Just(f32::NAN),
1 => Just(f32::INFINITY),
1 => Just(f32::NEG_INFINITY),
1 => Just(0.0f32),
1 => Just(-0.0f32),
1 => Just(f32::MIN_POSITIVE),
1 => Just(-f32::MIN_POSITIVE),
1 => Just(f32::MAX),
1 => Just(f32::MIN),
1 => Just(1e30f32),
1 => Just(-1e30f32),
]
}
fn hostile_point() -> impl Strategy<Value = Vec2> {
(hostile_coord(), hostile_coord()).prop_map(|(x, y)| Vec2::new(x, y))
}
#[derive(Debug, Clone)]
enum Verb {
Move(Vec2),
Line(Vec2),
Quad(Vec2, Vec2),
Conic(Vec2, Vec2, f32),
Cubic(Vec2, Vec2, Vec2),
Arc(Vec2, Vec2, f32, f32),
Close,
}
fn verb() -> impl Strategy<Value = Verb> {
prop_oneof![
hostile_point().prop_map(Verb::Move),
hostile_point().prop_map(Verb::Line),
(hostile_point(), hostile_point()).prop_map(|(c, p)| Verb::Quad(c, p)),
(hostile_point(), hostile_point(), hostile_coord())
.prop_map(|(c, p, w)| Verb::Conic(c, p, w)),
(hostile_point(), hostile_point(), hostile_point())
.prop_map(|(a, b, c)| Verb::Cubic(a, b, c)),
(
hostile_point(),
hostile_point(),
hostile_coord(),
hostile_coord()
)
.prop_map(|(c, r, s, w)| Verb::Arc(c, r, s, w)),
Just(Verb::Close),
]
}
fn build(verbs: &[Verb], rule: FillRule) -> Path {
let mut b = PathBuilder::new().with_fill_rule(rule);
for v in verbs {
match *v {
Verb::Move(p) => {
b.move_to(p);
}
Verb::Line(p) => {
b.line_to(p);
}
Verb::Quad(c, p) => {
b.quad_to(c, p);
}
Verb::Conic(c, p, w) => {
b.conic_to(c, p, w);
}
Verb::Cubic(a, c, p) => {
b.cubic_to(a, c, p);
}
Verb::Arc(c, r, s, w) => {
b.arc(c, r, s, w);
}
Verb::Close => {
b.close();
}
}
}
b.build()
}
fn fill_rule() -> impl Strategy<Value = FillRule> {
prop_oneof![Just(FillRule::NonZero), Just(FillRule::EvenOdd)]
}
fn hostile_tolerance() -> impl Strategy<Value = f32> {
prop_oneof![
6 => 0.01f32..4.0f32,
1 => Just(0.0f32),
1 => Just(-1.0f32),
1 => Just(f32::NAN),
1 => Just(f32::INFINITY),
1 => Just(1e-30f32),
1 => Just(1e30f32),
]
}
fn hostile_stroke() -> impl Strategy<Value = StrokeStyle> {
(
prop_oneof![
6 => 0.1f32..40.0f32,
1 => Just(0.0f32),
1 => Just(-4.0f32),
1 => Just(f32::NAN),
1 => Just(f32::INFINITY),
1 => Just(1e30f32),
],
prop_oneof![
Just(LineCap::Butt),
Just(LineCap::Round),
Just(LineCap::Square)
],
prop_oneof![
Just(LineJoin::Miter),
Just(LineJoin::Round),
Just(LineJoin::Bevel)
],
prop_oneof![
4 => 1.0f32..20.0f32,
1 => Just(0.0f32),
1 => Just(-1.0f32),
1 => Just(f32::NAN),
1 => Just(f32::INFINITY),
],
)
.prop_map(|(width, cap, join, limit)| {
StrokeStyle::new(width)
.with_cap(cap)
.with_join(join)
.with_miter_limit(limit)
})
}
fn in_range_coord() -> impl Strategy<Value = f32> {
prop_oneof![
8 => -2_000.0f32..2_000.0f32,
1 => Just(0.0f32),
1 => Just(-0.0f32),
1 => Just(f32::MIN_POSITIVE),
1 => Just(-f32::MIN_POSITIVE),
1 => Just(MAX_COORDINATE),
1 => Just(-MAX_COORDINATE),
1 => Just(MAX_COORDINATE - 1.0),
]
}
fn in_range_point() -> impl Strategy<Value = Vec2> {
(in_range_coord(), in_range_coord()).prop_map(|(x, y)| Vec2::new(x, y))
}
fn in_range_verb() -> impl Strategy<Value = Verb> {
prop_oneof![
in_range_point().prop_map(Verb::Move),
in_range_point().prop_map(Verb::Line),
(in_range_point(), in_range_point()).prop_map(|(c, p)| Verb::Quad(c, p)),
(in_range_point(), in_range_point(), -8.0f32..8.0f32)
.prop_map(|(c, p, w)| Verb::Conic(c, p, w)),
(in_range_point(), in_range_point(), in_range_point())
.prop_map(|(a, b, c)| Verb::Cubic(a, b, c)),
(
in_range_point(),
in_range_point(),
-20.0f32..20.0f32,
-20.0f32..20.0f32
)
.prop_map(|(c, r, s, w)| Verb::Arc(c, r, s, w)),
Just(Verb::Close),
]
}
fn in_range_stroke() -> impl Strategy<Value = StrokeStyle> {
(
0.01f32..400.0f32,
prop_oneof![
Just(LineCap::Butt),
Just(LineCap::Round),
Just(LineCap::Square)
],
prop_oneof![
Just(LineJoin::Miter),
Just(LineJoin::Round),
Just(LineJoin::Bevel)
],
0.0f32..40.0f32,
)
.prop_map(|(width, cap, join, limit)| {
StrokeStyle::new(width)
.with_cap(cap)
.with_join(join)
.with_miter_limit(limit)
})
}
proptest! {
#![proptest_config(ProptestConfig { cases: 4096, ..ProptestConfig::default() })]
#[test]
fn a_filled_path_of_any_shape_leaves_addressable_buffers(
verbs in prop::collection::vec(verb(), 0..24),
rule in fill_rule(),
tolerance in hostile_tolerance(),
) {
let path = build(&verbs, rule);
let mut tess = Tessellator::new();
let buffers = tess.fill(&path, tolerance);
prop_assert!(
buffers.is_well_formed(),
"fill produced {} indices over {} vertices",
buffers.indices.len(),
buffers.vertices.len()
);
for v in &buffers.vertices {
prop_assert!(v.is_finite(), "fill put {v:?} in the buffer");
}
}
#[test]
fn a_stroked_path_of_any_shape_leaves_addressable_buffers(
verbs in prop::collection::vec(verb(), 0..24),
rule in fill_rule(),
style in hostile_stroke(),
tolerance in hostile_tolerance(),
) {
let path = build(&verbs, rule);
let mut tess = Tessellator::new();
let buffers = tess.stroke(&path, &style, tolerance);
prop_assert!(
buffers.is_well_formed(),
"stroke produced {} indices over {} vertices",
buffers.indices.len(),
buffers.vertices.len()
);
for v in &buffers.vertices {
prop_assert!(v.is_finite(), "stroke put {v:?} in the buffer");
}
}
#[test]
fn accepted_input_tessellates_to_finite_output(
verbs in prop::collection::vec(in_range_verb(), 0..24),
rule in fill_rule(),
style in in_range_stroke(),
tolerance in 0.01f32..4.0f32,
) {
let path = build(&verbs, rule);
let mut tess = Tessellator::new();
for (what, buffers) in [
("fill", tess.fill(&path, tolerance).clone()),
("stroke", tess.stroke(&path, &style, tolerance).clone()),
] {
for v in &buffers.vertices {
prop_assert!(
v.is_finite(),
"{what} put {v:?} in the buffer from a path that is finite"
);
}
}
}
#[test]
fn a_path_answers_for_itself_however_it_was_built(
verbs in prop::collection::vec(verb(), 0..24),
rule in fill_rule(),
) {
let path = build(&verbs, rule);
let _ = path.bounds();
let _ = path.convexity();
let _ = path.is_finite();
let _ = path.is_empty();
let _ = path.as_rounded_rect();
prop_assert_eq!(path.fill_rule(), rule);
let counted: usize = path.segments().map(|(_, points)| points.len()).sum();
prop_assert!(counted <= path.points().len());
}
}
mod found_by_generation {
use super::*;
use impeller_geometry::MAX_COORDINATE;
#[test]
fn a_tolerance_of_zero_does_not_take_the_process_down() {
let mut b = PathBuilder::new();
b.cubic_to(Vec2::ZERO, Vec2::ZERO, Vec2::ZERO);
let path = b.build();
let mut tess = Tessellator::new();
assert!(tess.fill(&path, 0.0).is_well_formed());
assert!(tess
.stroke(&path, &StrokeStyle::new(0.1), 0.0)
.is_well_formed());
for absurd in [-1.0, f32::NAN, f32::INFINITY, f32::MIN_POSITIVE] {
assert!(
tess.fill(&path, absurd).is_well_formed(),
"fill at {absurd}"
);
assert!(
tess.stroke(&path, &StrokeStyle::new(0.1), absurd)
.is_well_formed(),
"stroke at {absurd}"
);
}
}
#[test]
fn a_finite_but_enormous_coordinate_does_not_take_the_process_down() {
let mut b = PathBuilder::new();
b.arc(Vec2::new(f32::MAX, 0.0), Vec2::ZERO, 0.0, -943.1208);
let path = b.build();
assert!(path.is_finite(), "the premise: every coordinate is finite");
assert!(!path.is_within_tessellation_range());
let mut tess = Tessellator::new();
assert!(tess.fill(&path, 0.25).is_empty());
assert!(tess.stroke(&path, &StrokeStyle::new(4.0), 0.25).is_empty());
}
#[test]
fn a_stroke_wider_than_the_coordinate_range_tessellates() {
let mut b = PathBuilder::new();
b.move_to(Vec2::new(10.0, 10.0))
.line_to(Vec2::new(50.0, 10.0))
.line_to(Vec2::new(50.0, 50.0));
let path = b.build();
let mut tess = Tessellator::new();
for width in [1e30f32, f32::MAX, 1e7] {
let style = StrokeStyle::new(width)
.with_cap(LineCap::Round)
.with_join(LineJoin::Round);
let out = tess.stroke(&path, &style, 0.25);
assert!(
!out.vertices.is_empty(),
"a stroke {width:e} wide should draw something"
);
assert!(
out.vertices
.iter()
.all(|p| p.x.is_finite() && p.y.is_finite()),
"a stroke {width:e} wide produced a vertex that is not a point"
);
}
for width in [f32::INFINITY, f32::NEG_INFINITY, f32::NAN, 0.0, -5.0] {
let style = StrokeStyle::new(width)
.with_cap(LineCap::Round)
.with_join(LineJoin::Round);
assert!(
tess.stroke(&path, &style, 0.25).is_empty(),
"a stroke {width:e} wide has no geometry in it"
);
}
}
#[test]
fn a_large_coordinate_cannot_allocate_without_bound() {
let curve = |m: f32| {
let mut b = PathBuilder::new();
b.move_to(Vec2::new(m, 0.0))
.cubic_to(Vec2::new(m, m), Vec2::new(0.0, m), Vec2::new(-m, -m))
.close();
b.build()
};
let mut tess = Tessellator::new();
let at_limit = tess
.stroke(&curve(MAX_COORDINATE), &StrokeStyle::new(4.0), 0.25)
.vertices
.len();
assert!(
at_limit < 100_000,
"a curve at the limit strokes to {at_limit} vertices"
);
for m in [1e8f32, 1e10, 1e12, 1e15, f32::MAX] {
let out = tess.stroke(&curve(m), &StrokeStyle::new(4.0), 0.25);
assert!(
out.is_empty(),
"a curve at {m:e} strokes to {} vertices",
out.vertices.len()
);
}
}
}
#[test]
fn a_path_at_the_coordinate_limit_tessellates_rather_than_panicking() {
let far = 16_777_215.0f32;
let mut builder = PathBuilder::new().with_fill_rule(FillRule::NonZero);
builder.arc(
Vec2::new(0.0, far),
Vec2::new(1162.7828, -479.33395),
7.361_401_6,
-0.362_522_57,
);
builder.close();
builder.conic_to(Vec2::new(0.0, 0.0), Vec2::new(-1.1754944e-38, far), 0.0);
builder.move_to(Vec2::new(MAX_COORDINATE, far));
builder.quad_to(Vec2::new(0.0, 0.0), Vec2::new(0.0, far));
let path = builder.build();
assert!(path.is_finite());
assert!(path.is_within_tessellation_range());
let mut tess = Tessellator::new();
let filled = tess.fill(&path, 0.01);
assert!(
!filled.is_empty(),
"the path should tessellate to something"
);
for v in &filled.vertices {
assert!(v.is_finite(), "fill put {v:?} in the buffer");
}
}