#[allow(unused_imports)]
pub use super::*;
#[cfg(test)]
#[allow(clippy::pedantic, clippy::nursery, clippy::float_cmp)]
mod autotest_generated {
use super::*;
fn pt(x: f32, y: f32) -> SvgPoint {
SvgPoint { x, y }
}
fn line_el(x1: f32, y1: f32, x2: f32, y2: f32) -> SvgPathElement {
SvgPathElement::line(SvgLine::new(pt(x1, y1), pt(x2, y2)))
}
fn make_path(items: Vec<SvgPathElement>) -> SvgPath {
SvgPath::create(SvgPathElementVec::from_vec(items))
}
fn quad() -> SvgQuadraticCurve {
SvgQuadraticCurve {
start: pt(0.0, 0.0),
ctrl: pt(5.0, 10.0),
end: pt(10.0, 0.0),
}
}
fn cubic() -> SvgCubicCurve {
SvgCubicCurve {
start: pt(0.0, 0.0),
ctrl_1: pt(0.0, 10.0),
ctrl_2: pt(10.0, 10.0),
end: pt(10.0, 0.0),
}
}
fn rect_contains(outer: &SvgRect, inner: &SvgRect) -> bool {
outer.x <= inner.x
&& outer.y <= inner.y
&& outer.x + outer.width >= inner.x + inner.width
&& outer.y + outer.height >= inner.y + inner.height
}
#[test]
fn svgline_new_keeps_fields_including_extreme_values() {
let l = SvgLine::new(pt(1.0, 2.0), pt(3.0, 4.0));
assert_eq!(l.get_start(), pt(1.0, 2.0));
assert_eq!(l.get_end(), pt(3.0, 4.0));
let extreme = SvgLine::new(pt(f32::MIN, f32::MAX), pt(f32::MAX, f32::MIN));
assert_eq!(extreme.start.x, f32::MIN);
assert_eq!(extreme.end.x, f32::MAX);
let nan = SvgLine::new(pt(f32::NAN, 0.0), pt(0.0, f32::NAN));
assert!(nan.get_start().x.is_nan());
assert!(nan.get_end().y.is_nan());
}
#[test]
fn svgline_reverse_is_an_involution() {
let orig = SvgLine::new(pt(-1.5, 2.5), pt(7.0, -9.0));
let mut l = orig;
l.reverse();
assert_eq!(l.get_start(), orig.get_end());
assert_eq!(l.get_end(), orig.get_start());
l.reverse();
assert_eq!(l, orig);
}
#[test]
fn svgline_reverse_does_not_panic_on_extreme_values() {
let mut l = SvgLine::new(pt(f32::INFINITY, f32::NAN), pt(f32::NEG_INFINITY, f32::MAX));
l.reverse();
assert!(l.get_start().x.is_infinite() && l.get_start().x < 0.0);
assert!(l.get_end().y.is_nan());
}
#[test]
fn svgline_inwards_normal_is_unit_length_and_90deg_right() {
let l = SvgLine::new(pt(0.0, 0.0), pt(10.0, 0.0));
let n = l
.inwards_normal()
.expect("non-degenerate line has a normal");
assert!((n.x - 0.0).abs() < 1e-6);
assert!((n.y - 1.0).abs() < 1e-6);
let len = (n.x * n.x + n.y * n.y).sqrt();
assert!((len - 1.0).abs() < 1e-6, "normal must be unit length");
}
#[test]
fn svgline_outwards_normal_is_the_negated_inwards_normal() {
let l = SvgLine::new(pt(3.0, -4.0), pt(-7.0, 11.0));
let i = l
.inwards_normal()
.expect("non-degenerate line has a normal");
let o = l
.outwards_normal()
.expect("non-degenerate line has a normal");
assert!((i.x + o.x).abs() < 1e-6);
assert!((i.y + o.y).abs() < 1e-6);
}
#[test]
fn svgline_normals_are_none_for_zero_length_line() {
let l = SvgLine::new(pt(5.0, 5.0), pt(5.0, 5.0));
assert_eq!(l.inwards_normal(), None);
assert_eq!(l.outwards_normal(), None);
}
#[test]
fn svgline_normals_are_none_for_nan_and_infinite_coords() {
let nan = SvgLine::new(pt(f32::NAN, 0.0), pt(1.0, 1.0));
assert_eq!(nan.inwards_normal(), None);
assert_eq!(nan.outwards_normal(), None);
let inf = SvgLine::new(pt(f32::NEG_INFINITY, f32::NEG_INFINITY), pt(1.0, 1.0));
assert_eq!(inf.inwards_normal(), None);
assert_eq!(inf.outwards_normal(), None);
}
#[test]
fn svgline_inwards_normal_on_overflowing_line_stays_defined() {
let l = SvgLine::new(pt(-f32::MAX, -f32::MAX), pt(f32::MAX, f32::MAX));
match l.inwards_normal() {
None => {}
Some(n) => assert!(
n.x.is_finite() && n.y.is_finite(),
"inwards_normal returned a non-finite point: {n:?}"
),
}
}
#[test]
fn svgline_get_x_y_at_t_hit_the_endpoints_exactly() {
let l = SvgLine::new(pt(2.0, -3.0), pt(12.0, 17.0));
assert_eq!(l.get_x_at_t(0.0), 2.0);
assert_eq!(l.get_y_at_t(0.0), -3.0);
assert_eq!(l.get_x_at_t(1.0), 12.0);
assert_eq!(l.get_y_at_t(1.0), 17.0);
assert!((l.get_x_at_t(0.5) - 7.0).abs() < 1e-9);
assert!((l.get_y_at_t(0.5) - 7.0).abs() < 1e-9);
}
#[test]
fn svgline_get_x_at_t_extrapolates_for_out_of_range_t() {
let l = SvgLine::new(pt(0.0, 0.0), pt(10.0, 10.0));
assert!((l.get_x_at_t(-1.0) - -10.0).abs() < 1e-9);
assert!((l.get_y_at_t(2.0) - 20.0).abs() < 1e-9);
}
#[test]
fn svgline_get_x_y_at_t_nan_and_inf_do_not_panic() {
let l = SvgLine::new(pt(0.0, 0.0), pt(10.0, 10.0));
assert!(l.get_x_at_t(f64::NAN).is_nan());
assert!(l.get_y_at_t(f64::NAN).is_nan());
assert!(l.get_x_at_t(f64::INFINITY).is_infinite());
assert!(l.get_y_at_t(f64::NEG_INFINITY).is_infinite());
let vertical = SvgLine::new(pt(4.0, 0.0), pt(4.0, 10.0));
assert!(vertical.get_x_at_t(f64::INFINITY).is_nan());
}
#[test]
fn svgline_get_x_at_t_at_f32_extremes_saturates_to_inf_not_panic() {
let l = SvgLine::new(pt(-f32::MAX, 0.0), pt(f32::MAX, 0.0));
assert!(l.get_x_at_t(0.5).abs() < 1e-9);
assert!(l.get_x_at_t(1.0).is_finite());
assert!(l.get_x_at_t(f64::MAX).is_infinite());
}
#[test]
fn svgline_get_length_is_euclidean_and_direction_independent() {
let l = SvgLine::new(pt(0.0, 0.0), pt(3.0, 4.0));
assert!((l.get_length() - 5.0).abs() < 1e-6);
let mut r = l;
r.reverse();
assert!((r.get_length() - l.get_length()).abs() < 1e-9);
assert_eq!(SvgLine::new(pt(1.0, 1.0), pt(1.0, 1.0)).get_length(), 0.0);
}
#[test]
fn svgline_get_length_overflow_and_nan_are_defined() {
let huge = SvgLine::new(pt(-f32::MAX, 0.0), pt(f32::MAX, 0.0));
let len = huge.get_length();
assert!(len.is_infinite() && len > 0.0);
let nan = SvgLine::new(pt(f32::NAN, 0.0), pt(0.0, 0.0));
assert!(nan.get_length().is_nan());
}
#[test]
fn svgline_get_t_at_offset_maps_arc_length_to_t() {
let l = SvgLine::new(pt(0.0, 0.0), pt(10.0, 0.0));
assert_eq!(l.get_t_at_offset(0.0), 0.0);
assert!((l.get_t_at_offset(5.0) - 0.5).abs() < 1e-6);
assert!((l.get_t_at_offset(10.0) - 1.0).abs() < 1e-6);
assert!((l.get_t_at_offset(-5.0) + 0.5).abs() < 1e-6);
assert!((l.get_t_at_offset(20.0) - 2.0).abs() < 1e-6);
}
#[test]
fn svgline_get_t_at_offset_on_zero_length_line_is_nan_or_inf_not_a_panic() {
let l = SvgLine::new(pt(1.0, 1.0), pt(1.0, 1.0));
assert!(l.get_t_at_offset(0.0).is_nan());
assert!(l.get_t_at_offset(5.0).is_infinite());
assert!(l.get_t_at_offset(-5.0).is_infinite());
assert!(l.get_t_at_offset(f64::NAN).is_nan());
}
#[test]
fn svgline_get_t_at_offset_round_trips_through_get_x_at_t() {
let l = SvgLine::new(pt(2.0, 2.0), pt(2.0, 12.0));
let t = l.get_t_at_offset(l.get_length());
assert!((l.get_y_at_t(t) - 12.0).abs() < 1e-6);
assert!((l.get_x_at_t(t) - 2.0).abs() < 1e-6);
}
#[test]
fn svgline_tangent_vector_is_normalized_and_zero_for_degenerate_lines() {
let l = SvgLine::new(pt(0.0, 0.0), pt(0.0, 5.0));
let t = l.get_tangent_vector_at_t();
assert!((t.x - 0.0).abs() < 1e-9);
assert!((t.y - 1.0).abs() < 1e-9);
let degenerate = SvgLine::new(pt(3.0, 3.0), pt(3.0, 3.0));
let t = degenerate.get_tangent_vector_at_t();
assert_eq!(t, SvgVector { x: 0.0, y: 0.0 });
}
#[test]
fn svgline_get_bounds_is_orientation_independent() {
let l = SvgLine::new(pt(10.0, 20.0), pt(-5.0, 4.0));
let mut r = l;
r.reverse();
assert_eq!(l.get_bounds(), r.get_bounds());
let b = l.get_bounds();
assert_eq!(b.x, -5.0);
assert_eq!(b.y, 4.0);
assert_eq!(b.width, 15.0);
assert_eq!(b.height, 16.0);
assert_eq!(b.radius_top_left, 0.0);
}
#[test]
fn svgline_get_bounds_of_degenerate_line_is_zero_sized() {
let b = SvgLine::new(pt(7.0, 8.0), pt(7.0, 8.0)).get_bounds();
assert_eq!((b.x, b.y, b.width, b.height), (7.0, 8.0, 0.0, 0.0));
}
#[test]
fn svgline_get_bounds_overflows_to_inf_width_without_panicking() {
let b = SvgLine::new(pt(-f32::MAX, 0.0), pt(f32::MAX, 1.0)).get_bounds();
assert!(b.width.is_infinite() && b.width > 0.0);
assert_eq!(b.x, -f32::MAX);
assert_eq!(b.height, 1.0);
}
#[test]
fn svgpathelement_constructors_wrap_the_right_variant() {
let l = SvgLine::new(pt(0.0, 0.0), pt(1.0, 1.0));
assert!(matches!(SvgPathElement::line(l), SvgPathElement::Line(_)));
assert!(matches!(
SvgPathElement::quadratic_curve(quad()),
SvgPathElement::QuadraticCurve(_)
));
assert!(matches!(
SvgPathElement::cubic_curve(cubic()),
SvgPathElement::CubicCurve(_)
));
}
#[test]
fn svgpathelement_constructors_accept_extreme_geometry() {
let l = SvgLine::new(pt(f32::NAN, f32::INFINITY), pt(f32::MIN, f32::MAX));
let el = SvgPathElement::line(l);
assert!(el.get_start().x.is_nan());
assert_eq!(el.get_end().x, f32::MIN);
}
#[test]
fn svgpathelement_set_first_last_round_trip_for_every_variant() {
let a = pt(-1.0, -2.0);
let b = pt(3.0, 4.0);
for mut el in [
SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(1.0, 1.0))),
SvgPathElement::quadratic_curve(quad()),
SvgPathElement::cubic_curve(cubic()),
] {
el.set_first(a);
el.set_last(b);
assert_eq!(el.get_start(), a);
assert_eq!(el.get_end(), b);
}
}
#[test]
fn svgpathelement_set_first_last_accept_zero_min_max_and_nan() {
let mut el = SvgPathElement::cubic_curve(cubic());
el.set_first(pt(0.0, 0.0));
el.set_last(pt(0.0, 0.0));
assert_eq!(el.get_start(), pt(0.0, 0.0));
assert_eq!(el.get_end(), pt(0.0, 0.0));
el.set_first(pt(f32::MIN, f32::MIN));
el.set_last(pt(f32::MAX, f32::MAX));
assert_eq!(el.get_start().x, f32::MIN);
assert_eq!(el.get_end().x, f32::MAX);
el.set_first(pt(f32::NAN, f32::NEG_INFINITY));
assert!(el.get_start().x.is_nan());
assert!(el.get_start().y.is_infinite());
}
#[test]
fn svgpathelement_reverse_is_an_involution_for_every_variant() {
for el in [
SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(1.0, 1.0))),
SvgPathElement::quadratic_curve(quad()),
SvgPathElement::cubic_curve(cubic()),
] {
let mut r = el;
r.reverse();
assert_eq!(r.get_start(), el.get_end());
assert_eq!(r.get_end(), el.get_start());
r.reverse();
assert_eq!(r, el, "reverse() applied twice must be the identity");
}
}
#[test]
fn svgpathelement_get_length_is_non_negative_for_every_variant() {
for el in [
SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(3.0, 4.0))),
SvgPathElement::quadratic_curve(quad()),
SvgPathElement::cubic_curve(cubic()),
] {
let len = el.get_length();
assert!(len.is_finite() && len >= 0.0, "bad length: {len}");
}
}
#[test]
fn svgpathelement_get_x_y_at_t_hit_endpoints_for_every_variant() {
for el in [
SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(10.0, 0.0))),
SvgPathElement::quadratic_curve(quad()),
SvgPathElement::cubic_curve(cubic()),
] {
assert!((el.get_x_at_t(0.0) - f64::from(el.get_start().x)).abs() < 1e-6);
assert!((el.get_y_at_t(0.0) - f64::from(el.get_start().y)).abs() < 1e-6);
assert!((el.get_x_at_t(1.0) - f64::from(el.get_end().x)).abs() < 1e-6);
assert!((el.get_y_at_t(1.0) - f64::from(el.get_end().y)).abs() < 1e-6);
}
}
#[test]
fn svgpathelement_get_x_y_at_t_nan_inf_do_not_panic() {
for el in [
SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(10.0, 0.0))),
SvgPathElement::quadratic_curve(quad()),
SvgPathElement::cubic_curve(cubic()),
] {
assert!(el.get_x_at_t(f64::NAN).is_nan());
assert!(el.get_y_at_t(f64::NAN).is_nan());
let _ = el.get_x_at_t(f64::INFINITY);
let _ = el.get_y_at_t(f64::NEG_INFINITY);
let _ = el.get_x_at_t(f64::MIN);
let _ = el.get_y_at_t(f64::MAX);
}
}
#[test]
fn svgpathelement_line_tangent_ignores_t_even_when_t_is_nan() {
let el = SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(0.0, 8.0)));
let at_half = el.get_tangent_vector_at_t(0.5);
assert_eq!(el.get_tangent_vector_at_t(f64::NAN), at_half);
assert_eq!(el.get_tangent_vector_at_t(f64::INFINITY), at_half);
assert_eq!(at_half, SvgVector { x: 0.0, y: 1.0 });
}
#[test]
fn svgpathelement_curve_tangent_at_nan_is_nan_not_a_panic() {
let el = SvgPathElement::quadratic_curve(quad());
let v = el.get_tangent_vector_at_t(f64::NAN);
assert!(v.x.is_nan() && v.y.is_nan());
}
#[test]
fn svgpathelement_curve_tangent_is_unit_length_in_range() {
for el in [
SvgPathElement::quadratic_curve(quad()),
SvgPathElement::cubic_curve(cubic()),
] {
for t in [0.0_f64, 0.25, 0.5, 0.75, 1.0] {
let v = el.get_tangent_vector_at_t(t);
let len = (v.x * v.x + v.y * v.y).sqrt();
assert!(
len.abs() < 1e-9 || (len - 1.0).abs() < 1e-6,
"tangent at t={t} has length {len}"
);
}
}
}
#[test]
fn svgpathelement_curve_t_at_offset_saturates_at_1_past_the_end() {
for el in [
SvgPathElement::quadratic_curve(quad()),
SvgPathElement::cubic_curve(cubic()),
] {
assert!((el.get_t_at_offset(f64::MAX) - 1.0).abs() < 1e-9);
assert!((el.get_t_at_offset(1.0e30) - 1.0).abs() < 1e-9);
assert!((el.get_t_at_offset(f64::NAN) - 1.0).abs() < 1e-9);
}
}
#[test]
fn svgpathelement_curve_t_at_offset_is_monotonic_and_in_range() {
let el = SvgPathElement::cubic_curve(cubic());
let len = el.get_length();
let t_quarter = el.get_t_at_offset(len * 0.25);
let t_half = el.get_t_at_offset(len * 0.5);
assert!(t_quarter.is_finite() && t_half.is_finite());
assert!((0.0..=1.0).contains(&t_quarter), "t={t_quarter}");
assert!((0.0..=1.0).contains(&t_half), "t={t_half}");
assert!(t_quarter <= t_half, "t must not decrease with offset");
}
#[test]
fn svgpathelement_curve_t_at_offset_negative_offset_is_non_positive() {
let el = SvgPathElement::cubic_curve(cubic());
let t = el.get_t_at_offset(-10.0);
assert!(t.is_finite(), "negative offset produced {t}");
assert!(t <= 0.0, "negative offset must not map to a forward t: {t}");
}
#[test]
fn svgpathelement_get_bounds_contains_both_endpoints() {
for el in [
SvgPathElement::line(SvgLine::new(pt(-3.0, 2.0), pt(9.0, -4.0))),
SvgPathElement::quadratic_curve(quad()),
SvgPathElement::cubic_curve(cubic()),
] {
let b = el.get_bounds();
let (s, e) = (el.get_start(), el.get_end());
assert!(b.width >= 0.0 && b.height >= 0.0);
assert!(s.x >= b.x && s.x <= b.x + b.width);
assert!(e.x >= b.x && e.x <= b.x + b.width);
assert!(s.y >= b.y && s.y <= b.y + b.height);
assert!(e.y >= b.y && e.y <= b.y + b.height);
}
}
#[test]
fn svgpath_empty_getters_are_none_and_bounds_are_default() {
let p = make_path(Vec::new());
assert_eq!(p.get_start(), None);
assert_eq!(p.get_end(), None);
assert!(!p.is_closed(), "an empty path is not closed");
assert_eq!(p.get_bounds(), SvgRect::default());
}
#[test]
fn svgpath_start_and_end_come_from_first_and_last_element() {
let p = make_path(vec![
line_el(0.0, 0.0, 1.0, 1.0),
line_el(1.0, 1.0, 5.0, 5.0),
line_el(5.0, 5.0, 9.0, 2.0),
]);
assert_eq!(p.get_start(), Some(pt(0.0, 0.0)));
assert_eq!(p.get_end(), Some(pt(9.0, 2.0)));
}
#[test]
fn svgpath_close_makes_is_closed_true_and_is_idempotent() {
let mut p = make_path(vec![
line_el(0.0, 0.0, 10.0, 0.0),
line_el(10.0, 0.0, 10.0, 10.0),
]);
assert!(!p.is_closed());
p.close();
assert!(p.is_closed(), "close() must establish is_closed()");
assert_eq!(p.items.len(), 3);
assert_eq!(p.get_end(), p.get_start());
p.close();
assert_eq!(p.items.len(), 3);
}
#[test]
fn svgpath_close_on_empty_path_is_a_noop() {
let mut p = make_path(Vec::new());
p.close();
assert_eq!(p.items.len(), 0);
assert!(!p.is_closed());
}
#[test]
fn svgpath_close_with_nan_coords_appends_once_and_does_not_panic() {
let mut p = make_path(vec![line_el(f32::NAN, 0.0, 10.0, 0.0)]);
p.close();
assert_eq!(p.items.len(), 2);
assert!(!p.is_closed(), "a NaN start point can never compare equal");
}
#[test]
fn svgpath_is_closed_for_single_degenerate_element() {
let p = make_path(vec![line_el(4.0, 4.0, 4.0, 4.0)]);
assert!(p.is_closed(), "start == end for the only element");
let open = make_path(vec![line_el(4.0, 4.0, 5.0, 4.0)]);
assert!(!open.is_closed());
}
#[test]
fn svgpath_reverse_is_an_involution_and_swaps_endpoints() {
let orig = make_path(vec![
line_el(0.0, 0.0, 1.0, 1.0),
SvgPathElement::cubic_curve(cubic()),
line_el(10.0, 0.0, 20.0, 5.0),
]);
let mut p = orig.clone();
p.reverse();
assert_eq!(p.get_start(), orig.get_end());
assert_eq!(p.get_end(), orig.get_start());
assert_eq!(p.items.len(), orig.items.len());
p.reverse();
assert_eq!(p, orig, "reverse() applied twice must be the identity");
}
#[test]
fn svgpath_reverse_on_empty_path_does_not_panic() {
let mut p = make_path(Vec::new());
p.reverse();
assert_eq!(p.items.len(), 0);
}
#[test]
fn svgpath_join_with_interpolates_the_join_point() {
let mut a = make_path(vec![line_el(0.0, 0.0, 10.0, 0.0)]);
let b = make_path(vec![line_el(20.0, 10.0, 30.0, 10.0)]);
assert_eq!(a.join_with(b), Some(()));
assert_eq!(a.items.len(), 2);
let mid = pt(15.0, 5.0);
assert_eq!(a.items.as_ref()[0].get_end(), mid);
assert_eq!(a.items.as_ref()[1].get_start(), mid);
assert_eq!(a.get_start(), Some(pt(0.0, 0.0)));
assert_eq!(a.get_end(), Some(pt(30.0, 10.0)));
}
#[test]
fn svgpath_join_with_empty_other_returns_none_and_leaves_self_intact() {
let mut a = make_path(vec![line_el(0.0, 0.0, 10.0, 0.0)]);
let before = a.clone();
assert_eq!(a.join_with(make_path(Vec::new())), None);
assert_eq!(a, before, "a failed join must not corrupt the receiver");
}
#[test]
fn svgpath_join_with_on_empty_self_returns_none_without_underflow() {
let mut a = make_path(Vec::new());
let b = make_path(vec![line_el(0.0, 0.0, 1.0, 1.0)]);
assert_eq!(a.join_with(b), None);
assert_eq!(a.items.len(), 0);
}
#[test]
fn svgpath_join_with_extreme_coords_does_not_panic() {
let mut a = make_path(vec![line_el(0.0, 0.0, f32::MAX, f32::MAX)]);
let b = make_path(vec![line_el(-f32::MAX, -f32::MAX, 0.0, 0.0)]);
assert_eq!(a.join_with(b), Some(()));
let join = a.items.as_ref()[0].get_end();
assert!(join.x.is_finite() && join.y.is_finite(), "join: {join:?}");
}
#[test]
fn svgpath_get_bounds_unions_every_element() {
let p = make_path(vec![
line_el(0.0, 0.0, 10.0, 0.0),
line_el(10.0, 0.0, 10.0, 20.0),
line_el(10.0, 20.0, -5.0, -8.0),
]);
let b = p.get_bounds();
assert_eq!(b.x, -5.0);
assert_eq!(b.y, -8.0);
assert_eq!(b.width, 15.0);
assert_eq!(b.height, 28.0);
for el in p.items.as_ref() {
assert!(
rect_contains(&b, &el.get_bounds()),
"path bounds must contain every element's bounds"
);
}
}
#[test]
fn svgmultipolygon_empty_has_default_bounds() {
let mp = SvgMultiPolygon::create(SvgPathVec::from_vec(Vec::new()));
assert_eq!(mp.get_bounds(), SvgRect::default());
}
#[test]
fn svgmultipolygon_bounds_union_all_rings() {
let mp = SvgMultiPolygon::create(SvgPathVec::from_vec(vec![
make_path(vec![line_el(0.0, 0.0, 10.0, 10.0)]),
make_path(vec![line_el(-4.0, 30.0, 2.0, 33.0)]),
]));
let b = mp.get_bounds();
assert_eq!(b.x, -4.0);
assert_eq!(b.y, 0.0);
assert_eq!(b.width, 14.0);
assert_eq!(b.height, 33.0);
}
#[test]
fn svgmultipolygon_bounds_must_contain_geometry_after_an_empty_first_ring() {
let mp = SvgMultiPolygon::create(SvgPathVec::from_vec(vec![
make_path(Vec::new()),
make_path(vec![line_el(100.0, 100.0, 200.0, 200.0)]),
]));
let b = mp.get_bounds();
let expected = SvgRect {
width: 100.0,
height: 100.0,
x: 100.0,
y: 100.0,
..SvgRect::default()
};
assert!(
rect_contains(&b, &expected),
"bounds {b:?} must contain the geometry of the non-empty ring {expected:?}"
);
}
#[test]
fn svgsimplenode_is_closed_per_variant() {
let circle = SvgCircle {
center_x: 0.0,
center_y: 0.0,
radius: 5.0,
};
assert!(SvgSimpleNode::Circle(circle).is_closed());
assert!(SvgSimpleNode::CircleHole(circle).is_closed());
assert!(SvgSimpleNode::Rect(SvgRect::default()).is_closed());
assert!(SvgSimpleNode::RectHole(SvgRect::default()).is_closed());
assert!(!SvgSimpleNode::Path(make_path(vec![line_el(0.0, 0.0, 1.0, 0.0)])).is_closed());
assert!(SvgSimpleNode::Path(make_path(vec![line_el(2.0, 2.0, 2.0, 2.0)])).is_closed());
assert!(!SvgSimpleNode::Path(make_path(Vec::new())).is_closed());
}
#[test]
fn svgsimplenode_get_bounds_per_variant() {
let circle = SvgCircle {
center_x: 10.0,
center_y: 10.0,
radius: 2.0,
};
let b = SvgSimpleNode::Circle(circle).get_bounds();
assert_eq!((b.x, b.y, b.width, b.height), (8.0, 8.0, 4.0, 4.0));
assert_eq!(SvgSimpleNode::CircleHole(circle).get_bounds(), b);
let rect = SvgRect {
width: 3.0,
height: 4.0,
x: 1.0,
y: 2.0,
..SvgRect::default()
};
assert_eq!(SvgSimpleNode::Rect(rect).get_bounds(), rect);
assert_eq!(SvgSimpleNode::RectHole(rect).get_bounds(), rect);
assert_eq!(
SvgSimpleNode::Path(make_path(Vec::new())).get_bounds(),
SvgRect::default()
);
}
#[test]
fn svgnode_get_bounds_of_empty_collections_is_default() {
assert_eq!(
SvgNode::MultiPolygonCollection(SvgMultiPolygonVec::from_vec(Vec::new())).get_bounds(),
SvgRect::default()
);
assert_eq!(
SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(Vec::new())).get_bounds(),
SvgRect::default()
);
assert_eq!(
SvgNode::Path(make_path(Vec::new())).get_bounds(),
SvgRect::default()
);
assert_eq!(
SvgNode::MultiPolygon(SvgMultiPolygon::create(SvgPathVec::from_vec(Vec::new())))
.get_bounds(),
SvgRect::default()
);
}
#[test]
fn svgnode_is_closed_is_vacuously_true_for_empty_collections() {
assert!(
SvgNode::MultiPolygonCollection(SvgMultiPolygonVec::from_vec(Vec::new())).is_closed()
);
assert!(SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(Vec::new())).is_closed());
assert!(
SvgNode::MultiPolygon(SvgMultiPolygon::create(SvgPathVec::from_vec(Vec::new())))
.is_closed()
);
assert!(!SvgNode::Path(make_path(Vec::new())).is_closed());
}
#[test]
fn svgnode_is_closed_false_when_any_subpath_is_open() {
let open = make_path(vec![line_el(0.0, 0.0, 1.0, 0.0)]);
let mut closed = make_path(vec![
line_el(0.0, 0.0, 1.0, 0.0),
line_el(1.0, 0.0, 1.0, 1.0),
]);
closed.close();
let mp = SvgMultiPolygon::create(SvgPathVec::from_vec(vec![closed.clone(), open.clone()]));
assert!(!SvgNode::MultiPolygon(mp.clone()).is_closed());
assert!(
!SvgNode::MultiPolygonCollection(SvgMultiPolygonVec::from_vec(vec![mp])).is_closed()
);
assert!(!SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(vec![
SvgSimpleNode::Path(open),
SvgSimpleNode::Circle(SvgCircle {
center_x: 0.0,
center_y: 0.0,
radius: 1.0,
}),
]))
.is_closed());
let all_closed = SvgMultiPolygon::create(SvgPathVec::from_vec(vec![closed]));
assert!(SvgNode::MultiPolygon(all_closed).is_closed());
}
#[test]
fn svgnode_get_bounds_contains_all_children() {
let a = make_path(vec![line_el(0.0, 0.0, 10.0, 10.0)]);
let b = make_path(vec![line_el(100.0, 100.0, 200.0, 200.0)]);
let node = SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(vec![
SvgSimpleNode::Path(a.clone()),
SvgSimpleNode::Path(b.clone()),
]));
let bounds = node.get_bounds();
assert!(rect_contains(&bounds, &a.get_bounds()));
assert!(rect_contains(&bounds, &b.get_bounds()));
}
#[test]
fn svgnode_rect_and_circle_bounds_are_passthrough() {
let rect = SvgRect {
width: 5.0,
height: 6.0,
x: -1.0,
y: -2.0,
..SvgRect::default()
};
assert_eq!(SvgNode::Rect(rect).get_bounds(), rect);
assert!(SvgNode::Rect(rect).is_closed());
let circle = SvgCircle {
center_x: 0.0,
center_y: 0.0,
radius: 1.0,
};
assert_eq!(SvgNode::Circle(circle).get_bounds(), circle.get_bounds());
assert!(SvgNode::Circle(circle).is_closed());
}
#[test]
fn svgcircle_contains_point_is_strict_and_correct() {
let c = SvgCircle {
center_x: 0.0,
center_y: 0.0,
radius: 1.0,
};
assert!(c.contains_point(0.0, 0.0));
assert!(c.contains_point(0.5, 0.5));
assert!(!c.contains_point(1.0, 0.0));
assert!(!c.contains_point(0.0, -1.0));
assert!(!c.contains_point(2.0, 0.0));
assert!(!c.contains_point(-0.8, -0.8));
}
#[test]
fn svgcircle_zero_radius_contains_nothing_not_even_its_center() {
let c = SvgCircle {
center_x: 3.0,
center_y: 3.0,
radius: 0.0,
};
assert!(!c.contains_point(3.0, 3.0));
assert!(!c.contains_point(0.0, 0.0));
}
#[test]
fn svgcircle_negative_radius_behaves_like_its_absolute_value() {
let neg = SvgCircle {
center_x: 0.0,
center_y: 0.0,
radius: -2.0,
};
let pos = SvgCircle {
center_x: 0.0,
center_y: 0.0,
radius: 2.0,
};
assert_eq!(neg.contains_point(0.0, 0.0), pos.contains_point(0.0, 0.0));
assert_eq!(neg.contains_point(1.9, 0.0), pos.contains_point(1.9, 0.0));
assert_eq!(neg.contains_point(5.0, 0.0), pos.contains_point(5.0, 0.0));
}
#[test]
fn svgcircle_contains_point_nan_and_inf_are_false_not_a_panic() {
let c = SvgCircle {
center_x: 0.0,
center_y: 0.0,
radius: 1.0,
};
assert!(!c.contains_point(f32::NAN, 0.0));
assert!(!c.contains_point(0.0, f32::NAN));
assert!(!c.contains_point(f32::INFINITY, 0.0));
assert!(!c.contains_point(f32::NEG_INFINITY, f32::NEG_INFINITY));
let nan_r = SvgCircle {
center_x: 0.0,
center_y: 0.0,
radius: f32::NAN,
};
assert!(!nan_r.contains_point(0.0, 0.0));
}
#[test]
fn svgcircle_contains_point_at_f32_extremes_does_not_panic() {
let c = SvgCircle {
center_x: 0.0,
center_y: 0.0,
radius: f32::MAX,
};
assert!(!c.contains_point(f32::MAX, f32::MAX));
assert!(c.contains_point(1.0, 1.0));
assert!(!c.contains_point(f32::MIN, 0.0));
}
#[test]
fn svgcircle_get_bounds_is_the_enclosing_square() {
let c = SvgCircle {
center_x: 5.0,
center_y: -5.0,
radius: 2.5,
};
let b = c.get_bounds();
assert_eq!((b.x, b.y, b.width, b.height), (2.5, -7.5, 5.0, 5.0));
assert!(!c.contains_point(b.x + 0.1, b.y + 0.1));
assert!(c.contains_point(c.center_x, c.center_y));
}
#[test]
fn svgcircle_get_bounds_with_nan_and_huge_radius_does_not_panic() {
let nan = SvgCircle {
center_x: 0.0,
center_y: 0.0,
radius: f32::NAN,
};
let b = nan.get_bounds();
assert!(b.width.is_nan() && b.x.is_nan());
let huge = SvgCircle {
center_x: 0.0,
center_y: 0.0,
radius: f32::MAX,
};
let b = huge.get_bounds();
assert!(b.width.is_infinite() && b.width > 0.0);
assert_eq!(b.x, -f32::MAX);
}
#[test]
fn tessellated_svg_node_empty_is_a_neutral_value() {
let e = TessellatedSvgNode::empty();
assert!(e.vertices.as_ref().is_empty());
assert!(e.indices.as_ref().is_empty());
assert_eq!(e, TessellatedSvgNode::default());
}
#[test]
fn tessellated_colored_svg_node_empty_is_a_neutral_value() {
let e = TessellatedColoredSvgNode::empty();
assert!(e.vertices.as_ref().is_empty());
assert!(e.indices.as_ref().is_empty());
assert_eq!(e, TessellatedColoredSvgNode::default());
}
#[test]
fn tessellated_svg_node_vec_ref_round_trips_through_as_slice() {
let node = TessellatedSvgNode {
vertices: vec![SvgVertex { x: 1.0, y: 2.0 }].into(),
indices: vec![0_u32].into(),
};
let v = TessellatedSvgNodeVec::from_vec(vec![node.clone(), TessellatedSvgNode::empty()]);
let r = v.get_ref();
assert_eq!(r.len, 2);
let slice = r.as_slice();
assert_eq!(slice.len(), 2);
assert_eq!(slice[0], node);
assert_eq!(slice[1], TessellatedSvgNode::empty());
}
#[test]
fn tessellated_svg_node_vec_ref_on_empty_vec_yields_an_empty_slice() {
let v = TessellatedSvgNodeVec::from_vec(Vec::new());
let r = v.get_ref();
assert_eq!(r.len, 0);
assert!(r.as_slice().is_empty());
assert!(!r.ptr.is_null(), "raw ptr must never be null");
}
#[test]
fn tessellated_colored_svg_node_vec_ref_round_trips_through_as_slice() {
let node = TessellatedColoredSvgNode {
vertices: vec![SvgColoredVertex {
x: 1.0,
y: 2.0,
z: 3.0,
r: 1.0,
g: 0.5,
b: 0.25,
a: 1.0,
}]
.into(),
indices: vec![0_u32, 1, 2].into(),
};
let v = TessellatedColoredSvgNodeVec::from_vec(vec![node.clone()]);
let r = v.get_ref();
assert_eq!(r.len, 1);
assert_eq!(r.as_slice().len(), 1);
assert_eq!(r.as_slice()[0], node);
}
#[test]
fn tessellated_colored_svg_node_vec_ref_on_empty_vec_yields_an_empty_slice() {
let v = TessellatedColoredSvgNodeVec::from_vec(Vec::new());
let r = v.get_ref();
assert_eq!(r.len, 0);
assert!(r.as_slice().is_empty());
assert!(!r.ptr.is_null(), "raw ptr must never be null");
}
fn matrix_of(u: &Uniform) -> [f32; 16] {
match u.uniform_type {
UniformType::Matrix4 { transpose, matrix } => {
assert!(!transpose, "SVG shaders expect a pre-transposed matrix");
matrix
}
_ => panic!("expected a Matrix4 uniform"),
}
}
fn bbox_of(u: &Uniform) -> [f32; 2] {
match u.uniform_type {
UniformType::FloatVec2(v) => v,
_ => panic!("expected a FloatVec2 uniform"),
}
}
#[test]
fn compute_svg_transform_uniforms_zero_size_no_transforms_is_finite() {
let (bbox, tf) = compute_svg_transform_uniforms(
PhysicalSizeU32 {
width: 0,
height: 0,
},
&[],
);
assert_eq!(bbox.uniform_name.as_str(), "vBboxSize");
assert_eq!(bbox_of(&bbox), [0.0, 0.0]);
assert_eq!(tf.uniform_name.as_str(), "vTransformMatrix");
let m = matrix_of(&tf);
assert!(
m.iter().all(|f| f.is_finite()),
"a zero-sized target must not produce NaN/inf in the matrix: {m:?}"
);
}
#[test]
fn compute_svg_transform_uniforms_at_u32_max_does_not_panic() {
let (bbox, tf) = compute_svg_transform_uniforms(
PhysicalSizeU32 {
width: u32::MAX,
height: u32::MAX,
},
&[],
);
let b = bbox_of(&bbox);
assert!(b[0].is_finite() && b[0] > 0.0);
assert_eq!(b[0], u32::MAX as f32);
assert_eq!(b[1], u32::MAX as f32);
let m = matrix_of(&tf);
assert!(m.iter().all(|f| f.is_finite()), "matrix: {m:?}");
}
#[test]
fn compute_svg_transform_uniforms_translation_changes_the_matrix() {
let size = PhysicalSizeU32 {
width: 800,
height: 600,
};
let identity = matrix_of(&compute_svg_transform_uniforms(size, &[]).1);
let translated = matrix_of(
&compute_svg_transform_uniforms(
size,
&[
StyleTransform::TranslateX(PixelValue::px(10.0)),
StyleTransform::TranslateY(PixelValue::px(-20.0)),
],
)
.1,
);
assert!(
translated.iter().all(|f| f.is_finite()),
"matrix: {translated:?}"
);
assert!(
identity != translated,
"a translation must actually alter the transform matrix"
);
}
#[test]
fn compute_svg_transform_uniforms_extreme_translation_does_not_panic() {
let size = PhysicalSizeU32 {
width: 1,
height: 1,
};
let (bbox, tf) = compute_svg_transform_uniforms(
size,
&[
StyleTransform::TranslateX(PixelValue::px(f32::MAX)),
StyleTransform::TranslateY(PixelValue::px(-f32::MAX)),
],
);
assert_eq!(bbox_of(&bbox), [1.0, 1.0]);
let _ = matrix_of(&tf);
}
#[test]
fn svgstyle_getters_read_through_to_both_variants() {
let fill = SvgStyle::Fill(SvgFillStyle::default());
assert!(fill.get_antialias());
assert!(!fill.get_high_quality_aa());
assert_eq!(fill.get_transform(), SvgTransform::default());
let stroke = SvgStyle::Stroke(SvgStrokeStyle::default());
assert!(stroke.get_antialias());
assert!(!stroke.get_high_quality_aa());
assert_eq!(stroke.get_transform(), SvgTransform::default());
}
#[test]
fn svgstyle_getters_reflect_non_default_fields() {
let transform = SvgTransform {
sx: 2.0,
kx: 0.5,
ky: -0.5,
sy: 3.0,
tx: 10.0,
ty: -10.0,
};
let fill = SvgStyle::Fill(SvgFillStyle {
anti_alias: false,
high_quality_aa: true,
transform,
..SvgFillStyle::default()
});
assert!(!fill.get_antialias());
assert!(fill.get_high_quality_aa());
assert_eq!(fill.get_transform(), transform);
let stroke = SvgStyle::Stroke(SvgStrokeStyle {
anti_alias: false,
high_quality_aa: true,
transform,
..SvgStrokeStyle::default()
});
assert!(!stroke.get_antialias());
assert!(stroke.get_high_quality_aa());
assert_eq!(stroke.get_transform(), transform);
}
#[test]
fn svgparseerror_display_is_non_empty_for_every_variant() {
let variants = [
SvgParseError::NoParserAvailable,
SvgParseError::ElementsLimitReached,
SvgParseError::NotAnUtf8Str,
SvgParseError::MalformedGZip,
SvgParseError::InvalidSize,
SvgParseError::ParsingFailed(XmlError::NoRootNode),
];
for v in &variants {
let s = v.to_string();
assert!(!s.is_empty(), "empty Display output for {v:?}");
assert!(
!s.contains("\u{0}"),
"Display output must not contain NUL bytes"
);
}
}
#[test]
fn svgparseerror_display_of_parsing_failed_embeds_the_inner_error() {
let inner = XmlError::NoRootNode;
let s = SvgParseError::ParsingFailed(inner.clone()).to_string();
assert!(s.starts_with("Error parsing SVG:"), "got: {s}");
assert!(
s.contains(&inner.to_string()),
"outer message {s:?} must embed the inner XmlError message"
);
}
}
#[cfg(test)]
mod contains_point_tests {
use azul_css::props::basic::{SvgPoint, SvgVector};
use crate::svg::{SvgLine, SvgMultiPolygon, SvgPath, SvgPathElement, SvgPathVec};
fn p(x: f32, y: f32) -> SvgPoint {
SvgPoint { x, y }
}
fn polygon(points: &[(f32, f32)]) -> SvgMultiPolygon {
let mut items = Vec::new();
for i in 0..points.len() {
let (ax, ay) = points[i];
let (bx, by) = points[(i + 1) % points.len()];
items.push(SvgPathElement::Line(SvgLine::new(p(ax, ay), p(bx, by))));
}
SvgMultiPolygon::create(SvgPathVec::from_vec(vec![SvgPath {
items: crate::svg::SvgPathElementVec::from_vec(items),
}]))
}
#[test]
fn a_triangle_contains_its_own_half_and_not_the_other() {
let t = polygon(&[(0.0, 0.0), (0.0, 16.0), (16.0, 16.0)]);
assert!(t.contains_point(3.0, 13.0), "inside the triangle");
assert!(!t.contains_point(13.0, 3.0), "the clipped-away corner");
assert!(!t.contains_point(20.0, 20.0), "outside the box entirely");
}
#[test]
fn a_reversed_inner_ring_is_a_hole() {
let outer = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
let inner = [(3.0, 3.0), (3.0, 7.0), (7.0, 7.0), (7.0, 3.0)]; let mut rings = polygon(&outer).rings.into_library_owned_vec();
rings.extend(polygon(&inner).rings.into_library_owned_vec());
let donut = SvgMultiPolygon::create(SvgPathVec::from_vec(rings));
assert!(donut.contains_point(1.5, 5.0), "the ring itself is solid");
assert!(!donut.contains_point(5.0, 5.0), "the hole is not");
}
#[test]
fn an_empty_path_contains_nothing() {
let empty = SvgMultiPolygon::create(SvgPathVec::from_vec(Vec::new()));
assert!(!empty.contains_point(0.0, 0.0));
assert!(!empty.contains_point(5.0, 5.0));
let degenerate = polygon(&[(1.0, 1.0)]);
assert!(!degenerate.contains_point(1.0, 1.0));
}
#[test]
fn a_curved_path_is_flattened_finely_enough_to_test() {
use crate::svg::SvgCubicCurve;
const K: f32 = 4.418_278; let arcs = [
(p(8.0, 0.0), p(8.0 + K, 0.0), p(16.0, 8.0 - K), p(16.0, 8.0)),
(p(16.0, 8.0), p(16.0, 8.0 + K), p(8.0 + K, 16.0), p(8.0, 16.0)),
(p(8.0, 16.0), p(8.0 - K, 16.0), p(0.0, 8.0 + K), p(0.0, 8.0)),
(p(0.0, 8.0), p(0.0, 8.0 - K), p(8.0 - K, 0.0), p(8.0, 0.0)),
];
let items: Vec<SvgPathElement> = arcs
.into_iter()
.map(|(start, ctrl_1, ctrl_2, end)| {
SvgPathElement::CubicCurve(SvgCubicCurve {
start,
ctrl_1,
ctrl_2,
end,
})
})
.collect();
let circle = SvgMultiPolygon::create(SvgPathVec::from_vec(vec![SvgPath {
items: crate::svg::SvgPathElementVec::from_vec(items),
}]));
assert!(circle.contains_point(8.0, 8.0), "the centre is inside");
assert!(
!circle.contains_point(0.5, 0.5),
"the square's corner is outside the inscribed circle"
);
let _ = SvgVector { x: 0.0, y: 0.0 };
}
}