diff --git a/src/arc.rs b/src/arc.rs index 2c2ef96b..2637882c 100644 --- a/src/arc.rs +++ b/src/arc.rs @@ -165,7 +165,7 @@ impl Shape for Arc { /// Note: shape isn't closed so area is not well defined. #[inline] - fn area(&self) -> f64 { + fn area(&self, _tolerance: f64) -> f64 { let Vec2 { x, y } = self.radii; PI * x * y } @@ -177,19 +177,19 @@ impl Shape for Arc { /// /// [wikipedia]: https://en.wikipedia.org/wiki/Ellipse#Circumference #[inline] - fn perimeter(&self, accuracy: f64) -> f64 { - self.path_segments(0.1).perimeter(accuracy) + fn perimeter(&self, tolerance: f64) -> f64 { + self.path_segments(tolerance).perimeter(tolerance) } /// Note: shape isn't closed, so a point's winding number is not well defined. #[inline] - fn winding(&self, pt: Point) -> i32 { - self.path_segments(0.1).winding(pt) + fn winding(&self, pt: Point, tolerance: f64) -> i32 { + self.path_segments(tolerance).winding(pt) } #[inline] - fn bounding_box(&self) -> Rect { - self.path_segments(0.1).bounding_box() + fn bounding_box(&self, tolerance: f64) -> Rect { + self.path_segments(tolerance).bounding_box() } } diff --git a/src/bezpath.rs b/src/bezpath.rs index eddd03e7..a8a41b7e 100644 --- a/src/bezpath.rs +++ b/src/bezpath.rs @@ -794,23 +794,23 @@ impl> Segments { /// Here, `accuracy` specifies the accuracy for each Bézier segment. At worst, /// the total error is `accuracy` times the number of Bézier segments. - // TODO: pub? Or is this subsumed by method of &[PathEl]? - pub(crate) fn perimeter(self, accuracy: f64) -> f64 { + /// Version of [`Shape::perimeter`] which consumes the iterator. + pub fn perimeter(self, accuracy: f64) -> f64 { self.map(|seg| seg.arclen(accuracy)).sum() } - // Same - pub(crate) fn area(self) -> f64 { + /// Version of [`Shape::area`] which consumes the iterator and needs no tolerance parameter. + pub fn area(self) -> f64 { self.map(|seg| seg.signed_area()).sum() } - // Same - pub(crate) fn winding(self, p: Point) -> i32 { + /// Version of [`Shape::winding`] which consumes the iterator and needs no tolerance parameter. + pub fn winding(self, p: Point) -> i32 { self.map(|seg| seg.winding(p)).sum() } - // Same - pub(crate) fn bounding_box(self) -> Rect { + /// Version of [`Shape::bounding_box`] which consumes the iterator and needs no tolerance parameter. + pub fn bounding_box(self) -> Rect { let mut bbox: Option = None; for seg in self { let seg_bb = ParamCurveExtrema::bounding_box(&seg); @@ -1293,8 +1293,8 @@ impl Shape for BezPath { } /// Signed area. - fn area(&self) -> f64 { - self.elements().area() + fn area(&self, tolerance: f64) -> f64 { + self.elements().area(tolerance) } fn perimeter(&self, accuracy: f64) -> f64 { @@ -1302,12 +1302,12 @@ impl Shape for BezPath { } /// Winding number of point. - fn winding(&self, pt: Point) -> i32 { - self.elements().winding(pt) + fn winding(&self, pt: Point, tolerance: f64) -> i32 { + self.elements().winding(pt, tolerance) } - fn bounding_box(&self) -> Rect { - self.elements().bounding_box() + fn bounding_box(&self, tolerance: f64) -> Rect { + self.elements().bounding_box(tolerance) } fn as_path_slice(&self) -> Option<&[PathEl]> { @@ -1371,7 +1371,7 @@ impl<'a> Shape for &'a [PathEl] { } /// Signed area. - fn area(&self) -> f64 { + fn area(&self, _tolerance: f64) -> f64 { segments(self.iter().copied()).area() } @@ -1380,11 +1380,11 @@ impl<'a> Shape for &'a [PathEl] { } /// Winding number of point. - fn winding(&self, pt: Point) -> i32 { + fn winding(&self, pt: Point, _tolerance: f64) -> i32 { segments(self.iter().copied()).winding(pt) } - fn bounding_box(&self) -> Rect { + fn bounding_box(&self, _tolerance: f64) -> Rect { segments(self.iter().copied()).bounding_box() } @@ -1411,7 +1411,7 @@ impl Shape for [PathEl; N] { } /// Signed area. - fn area(&self) -> f64 { + fn area(&self, _tolerance: f64) -> f64 { segments(self.iter().copied()).area() } @@ -1420,11 +1420,11 @@ impl Shape for [PathEl; N] { } /// Winding number of point. - fn winding(&self, pt: Point) -> i32 { + fn winding(&self, pt: Point, _tolerance: f64) -> i32 { segments(self.iter().copied()).winding(pt) } - fn bounding_box(&self) -> Rect { + fn bounding_box(&self, _tolerance: f64) -> Rect { segments(self.iter().copied()).bounding_box() } @@ -1448,10 +1448,10 @@ impl Shape for PathSeg { PathSegIter { seg: *self, ix: 0 } } - /// The area under the curve. + /// The area between the curve and the origin. /// /// We could just return `0`, but this seems more useful. - fn area(&self) -> f64 { + fn area(&self, _tolerance: f64) -> f64 { self.signed_area() } @@ -1460,12 +1460,12 @@ impl Shape for PathSeg { self.arclen(accuracy) } - fn winding(&self, _pt: Point) -> i32 { + fn winding(&self, _pt: Point, _tolerance: f64) -> i32 { 0 } #[inline] - fn bounding_box(&self) -> Rect { + fn bounding_box(&self, _tolerance: f64) -> Rect { ParamCurveExtrema::bounding_box(self) } @@ -1585,8 +1585,8 @@ mod tests { path.line_to((1.0, 1.0)); path.line_to((2.0, 0.0)); path.close_path(); - assert_eq!(path.winding(Point::new(1.0, 0.5)), -1); - assert!(path.contains(Point::new(1.0, 0.5))); + assert_eq!(path.winding(Point::new(1.0, 0.5), 1.0), -1); + assert!(path.contains(Point::new(1.0, 0.5), 1.0)); } // get_seg(i) should produce the same results as path_segments().nth(i - 1). @@ -1606,7 +1606,7 @@ mod tests { // cbox is wildly different than tight box let path = BezPath::from_svg("M200,300 C50,50 350,50 200,300").unwrap(); assert_eq!(Rect::new(50.0, 50.0, 350.0, 300.0), path.control_box()); - assert!(path.control_box().area() > path.bounding_box().area()); + assert!(path.control_box().area() > path.bounding_box(1e-9).area()); } #[test] diff --git a/src/circle.rs b/src/circle.rs index e91f696d..ee4af303 100644 --- a/src/circle.rs +++ b/src/circle.rs @@ -125,7 +125,7 @@ impl Shape for Circle { } #[inline] - fn area(&self) -> f64 { + fn area(&self, _tolerance: f64) -> f64 { PI * self.radius.powi(2) } @@ -134,7 +134,7 @@ impl Shape for Circle { (2.0 * PI * self.radius).abs() } - fn winding(&self, pt: Point) -> i32 { + fn winding(&self, pt: Point, _tolerance: f64) -> i32 { if (pt - self.center).hypot2() < self.radius.powi(2) { 1 } else { @@ -143,7 +143,7 @@ impl Shape for Circle { } #[inline] - fn bounding_box(&self) -> Rect { + fn bounding_box(&self, _tolerance: f64) -> Rect { let r = self.radius.abs(); let (x, y) = self.center.into(); Rect::new(x - r, y - r, x + r, y + r) @@ -320,7 +320,7 @@ impl Shape for CircleSegment { } #[inline] - fn area(&self) -> f64 { + fn area(&self, _tolerance: f64) -> f64 { 0.5 * (self.outer_radius.powi(2) - self.inner_radius.powi(2)).abs() * self.sweep_angle } @@ -330,7 +330,7 @@ impl Shape for CircleSegment { + self.sweep_angle * (self.inner_radius + self.outer_radius) } - fn winding(&self, pt: Point) -> i32 { + fn winding(&self, pt: Point, _tolerance: f64) -> i32 { let angle = (pt - self.center).atan2(); if angle < self.start_angle || angle > self.start_angle + self.sweep_angle { return 0; @@ -347,7 +347,7 @@ impl Shape for CircleSegment { } #[inline] - fn bounding_box(&self) -> Rect { + fn bounding_box(&self, _tolerance: f64) -> Rect { // todo this is currently not tight let r = self.inner_radius.max(self.outer_radius); let (x, y) = self.center.into(); @@ -380,21 +380,24 @@ mod tests { fn area_sign() { let center = Point::new(5.0, 5.0); let c = Circle::new(center, 5.0); - assert_approx_eq(c.area(), 25.0 * PI); + assert_approx_eq(c.area(1e-9), 25.0 * PI); - assert_eq!(c.winding(center), 1); + assert_eq!(c.winding(center, 1e-9), 1); let p = c.to_path(1e-9); - assert_approx_eq(c.area(), p.area()); - assert_eq!(c.winding(center), p.winding(center)); + assert_approx_eq(c.area(1e-9), p.area(1e-9)); + assert_eq!(c.winding(center, 1e-9), p.winding(center, 1e-9)); let c_neg_radius = Circle::new(center, -5.0); - assert_approx_eq(c_neg_radius.area(), 25.0 * PI); + assert_approx_eq(c_neg_radius.area(1e-9), 25.0 * PI); - assert_eq!(c_neg_radius.winding(center), 1); + assert_eq!(c_neg_radius.winding(center, 1e-9), 1); let p_neg_radius = c_neg_radius.to_path(1e-9); - assert_approx_eq(c_neg_radius.area(), p_neg_radius.area()); - assert_eq!(c_neg_radius.winding(center), p_neg_radius.winding(center)); + assert_approx_eq(c_neg_radius.area(1e-9), p_neg_radius.area(1e-9)); + assert_eq!( + c_neg_radius.winding(center, 1e-9), + p_neg_radius.winding(center, 1e-9) + ); } } diff --git a/src/cubicbez.rs b/src/cubicbez.rs index 3a59f8f3..f7acb5be 100644 --- a/src/cubicbez.rs +++ b/src/cubicbez.rs @@ -470,7 +470,7 @@ impl Shape for CubicBez { } } - fn area(&self) -> f64 { + fn area(&self, _tolerance: f64) -> f64 { 0.0 } @@ -479,12 +479,12 @@ impl Shape for CubicBez { self.arclen(accuracy) } - fn winding(&self, _pt: Point) -> i32 { + fn winding(&self, _pt: Point, _tolerance: f64) -> i32 { 0 } #[inline] - fn bounding_box(&self) -> Rect { + fn bounding_box(&self, _tolerance: f64) -> Rect { ParamCurveExtrema::bounding_box(self) } } diff --git a/src/ellipse.rs b/src/ellipse.rs index af1c11e6..f67c69fd 100644 --- a/src/ellipse.rs +++ b/src/ellipse.rs @@ -220,7 +220,7 @@ impl Shape for Ellipse { } #[inline] - fn area(&self) -> f64 { + fn area(&self, _tolerance: f64) -> f64 { let Vec2 { x, y } = self.radii(); PI * x * y } @@ -236,7 +236,7 @@ impl Shape for Ellipse { self.path_segments(0.1).perimeter(accuracy) } - fn winding(&self, pt: Point) -> i32 { + fn winding(&self, pt: Point, _tolerance: f64) -> i32 { // Strategy here is to apply the inverse map to the point and see if it is in the unit // circle. let inv = self.inner.inverse(); @@ -259,7 +259,7 @@ impl Shape for Ellipse { // // We can then use the method in the link with the translation to get the bounding box. #[inline] - fn bounding_box(&self) -> Rect { + fn bounding_box(&self, _tolerance: f64) -> Rect { let aff = self.inner.as_coeffs(); let a2 = aff[0] * aff[0]; let b2 = aff[1] * aff[1]; @@ -293,23 +293,26 @@ mod tests { fn area_sign() { let center = Point::new(5.0, 5.0); let e = Ellipse::new(center, (5.0, 5.0), 1.0); - assert_approx_eq(e.area(), 25.0 * PI); + assert_approx_eq(e.area(1.0), 25.0 * PI); let e = Ellipse::new(center, (5.0, 10.0), 1.0); - assert_approx_eq(e.area(), 50.0 * PI); + assert_approx_eq(e.area(1.0), 50.0 * PI); - assert_eq!(e.winding(center), 1); + assert_eq!(e.winding(center, 1.0), 1); let p = e.to_path(1e-9); - assert_approx_eq(e.area(), p.area()); - assert_eq!(e.winding(center), p.winding(center)); + assert_approx_eq(e.area(1.0), p.area(1.0)); + assert_eq!(e.winding(center, 1.0), p.winding(center, 1.0)); let e_neg_radius = Ellipse::new(center, (-5.0, 10.0), 1.0); - assert_approx_eq(e_neg_radius.area(), 50.0 * PI); + assert_approx_eq(e_neg_radius.area(1.0), 50.0 * PI); - assert_eq!(e_neg_radius.winding(center), 1); + assert_eq!(e_neg_radius.winding(center, 1.0), 1); let p_neg_radius = e_neg_radius.to_path(1e-9); - assert_approx_eq(e_neg_radius.area(), p_neg_radius.area()); - assert_eq!(e_neg_radius.winding(center), p_neg_radius.winding(center)); + assert_approx_eq(e_neg_radius.area(1.0), p_neg_radius.area(1.0)); + assert_eq!( + e_neg_radius.winding(center, 1.0), + p_neg_radius.winding(center, 1.0) + ); } } diff --git a/src/line.rs b/src/line.rs index 92810e6e..adf7503f 100644 --- a/src/line.rs +++ b/src/line.rs @@ -252,7 +252,7 @@ impl Shape for Line { /// only meaningful for closed shapes), but an argument can be made /// that the contract should be tightened to include the Green's /// theorem contribution. - fn area(&self) -> f64 { + fn area(&self, _tolerance: f64) -> f64 { 0.0 } @@ -262,12 +262,12 @@ impl Shape for Line { } /// Same consideration as `area`. - fn winding(&self, _pt: Point) -> i32 { + fn winding(&self, _pt: Point, _tolerance: f64) -> i32 { 0 } #[inline] - fn bounding_box(&self) -> Rect { + fn bounding_box(&self, _tolerance: f64) -> Rect { Rect::from_points(self.p0, self.p1) } diff --git a/src/param_curve.rs b/src/param_curve.rs index 4faab7b5..5a0ea0a3 100644 --- a/src/param_curve.rs +++ b/src/param_curve.rs @@ -123,20 +123,29 @@ pub trait ParamCurveArclen: ParamCurve { } } -/// A parametrized curve that can have its signed area measured. +/// A parametrized curve (or a section of one) that can have its signed area measured. pub trait ParamCurveArea { - /// Compute the signed area under the curve. + /// Compute the signed (counterclockwise from +x to +y) area between the curve and the origin. + /// Equivalently (using Green's theorem), + /// this is integral of the form `(x*dy - y*dx)/2` along the curve. + /// + /// For closed curves, this is the curve's area. + /// For open curves, this is the the area of the resulting shape that would be created if + /// the curve was closed with two line segments between the endpoints and the origin. + /// This allows the area of a piecewise curve to be computed by adding the areas of its segments, + /// generalizing the "shoelace formula." + /// + /// For an open curve with endpoints `(x0, y0)` and `(x1, y1)`, this value + /// is also equivalent to `-integral(y*dx) - (x0*y0 + x1*y1)/2`. /// - /// For a closed path, the signed area of the path is the sum of signed - /// areas of the segments. This is a variant of the "shoelace formula." /// See: /// and /// /// - /// This can be computed exactly for Béziers thanks to Green's theorem, - /// and also for simple curves such as circular arcs. For more exotic - /// curves, it's probably best to subdivide to cubics. We leave that - /// to the caller, which is why we don't give an accuracy param here. + /// This can be computed exactly for Béziers, + /// and also for simple curves such as circular arcs. + /// For more exotic curves, it's probably best to subdivide to cubics. + /// We leave that to the caller, which is why we don't give an accuracy param here. fn signed_area(&self) -> f64; } diff --git a/src/quadbez.rs b/src/quadbez.rs index f309d33d..fa3c942b 100644 --- a/src/quadbez.rs +++ b/src/quadbez.rs @@ -126,7 +126,7 @@ impl Shape for QuadBez { QuadBezIter { quad: *self, ix: 0 } } - fn area(&self) -> f64 { + fn area(&self, _tolerance: f64) -> f64 { 0.0 } @@ -135,12 +135,12 @@ impl Shape for QuadBez { self.arclen(accuracy) } - fn winding(&self, _pt: Point) -> i32 { + fn winding(&self, _pt: Point, _tolerance: f64) -> i32 { 0 } #[inline] - fn bounding_box(&self) -> Rect { + fn bounding_box(&self, _tolerance: f64) -> Rect { ParamCurveExtrema::bounding_box(self) } } diff --git a/src/rect.rs b/src/rect.rs index 397b9785..7b6e86fc 100644 --- a/src/rect.rs +++ b/src/rect.rs @@ -590,12 +590,12 @@ impl Shape for Rect { // It's a bit of duplication having both this and the impl method, but // removing that would require using the trait. We'll leave it for now. #[inline] - fn area(&self) -> f64 { + fn area(&self, _tolerance: f64) -> f64 { Rect::area(self) } #[inline] - fn perimeter(&self, _accuracy: f64) -> f64 { + fn perimeter(&self, _tolerance: f64) -> f64 { 2.0 * (self.width().abs() + self.height().abs()) } @@ -603,7 +603,7 @@ impl Shape for Rect { /// tiled with rectangles, the winding number will be nonzero for exactly /// one of them. #[inline] - fn winding(&self, pt: Point) -> i32 { + fn winding(&self, pt: Point, _tolerance: f64) -> i32 { let xmin = self.x0.min(self.x1); let xmax = self.x0.max(self.x1); let ymin = self.y0.min(self.y1); @@ -620,7 +620,7 @@ impl Shape for Rect { } #[inline] - fn bounding_box(&self) -> Rect { + fn bounding_box(&self, _tolerance: f64) -> Rect { self.abs() } @@ -630,7 +630,7 @@ impl Shape for Rect { } #[inline] - fn contains(&self, pt: Point) -> bool { + fn contains(&self, pt: Point, _tolerance: f64) -> bool { self.contains(pt) } } @@ -695,19 +695,19 @@ mod tests { let center = r.center(); assert_approx_eq(r.area(), 100.0); - assert_eq!(r.winding(center), 1); + assert_eq!(r.winding(center, 1.0), 1); let p = r.to_path(1e-9); - assert_approx_eq(r.area(), p.area()); - assert_eq!(r.winding(center), p.winding(center)); + assert_approx_eq(r.area(), p.area(1.0)); + assert_eq!(r.winding(center, 1.0), p.winding(center, 1.0)); let r_flip = Rect::new(0.0, 10.0, 10.0, 0.0); assert_approx_eq(r_flip.area(), -100.0); - assert_eq!(r_flip.winding(Point::new(5.0, 5.0)), -1); + assert_eq!(r_flip.winding(Point::new(5.0, 5.0), 1.0), -1); let p_flip = r_flip.to_path(1e-9); - assert_approx_eq(r_flip.area(), p_flip.area()); - assert_eq!(r_flip.winding(center), p_flip.winding(center)); + assert_approx_eq(r_flip.area(), p_flip.area(1.0)); + assert_eq!(r_flip.winding(center, 1.0), p_flip.winding(center, 1.0)); } #[test] diff --git a/src/rounded_rect.rs b/src/rounded_rect.rs index 1f1661cd..e27cc3a8 100644 --- a/src/rounded_rect.rs +++ b/src/rounded_rect.rs @@ -225,7 +225,7 @@ impl Shape for RoundedRect { } #[inline] - fn area(&self) -> f64 { + fn area(&self, _tolerance: f64) -> f64 { // A corner is a quarter-circle, i.e. // .............# // . ###### @@ -289,7 +289,7 @@ impl Shape for RoundedRect { } #[inline] - fn winding(&self, mut pt: Point) -> i32 { + fn winding(&self, mut pt: Point, _tolerance: f64) -> i32 { let center = self.center(); // 1. Translate the point relative to the center of the rectangle. @@ -339,8 +339,8 @@ impl Shape for RoundedRect { } #[inline] - fn bounding_box(&self) -> Rect { - self.rect.bounding_box() + fn bounding_box(&self, accuracy: f64) -> Rect { + self.rect.bounding_box(accuracy) } #[inline] @@ -443,26 +443,26 @@ mod tests { // Extremum: 0.0 radius corner -> rectangle let rect = Rect::new(0.0, 0.0, 100.0, 100.0); let rounded_rect = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 0.0); - assert!((rect.area() - rounded_rect.area()).abs() < epsilon); + assert!((rect.area() - rounded_rect.area(1.0)).abs() < epsilon); // Extremum: half-size radius corner -> circle let circle = Circle::new((0.0, 0.0), 50.0); let rounded_rect = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 50.0); - assert!((circle.area() - rounded_rect.area()).abs() < epsilon); + assert!((circle.area(1.0) - rounded_rect.area(1.0)).abs() < epsilon); } #[test] fn winding() { let rect = RoundedRect::new(-5.0, -5.0, 10.0, 20.0, (5.0, 5.0, 5.0, 0.0)); - assert_eq!(rect.winding(Point::new(0.0, 0.0)), 1); - assert_eq!(rect.winding(Point::new(-5.0, 0.0)), 1); // left edge - assert_eq!(rect.winding(Point::new(0.0, 20.0)), 1); // bottom edge - assert_eq!(rect.winding(Point::new(10.0, 20.0)), 0); // bottom-right corner - assert_eq!(rect.winding(Point::new(-5.0, 20.0)), 1); // bottom-left corner (has a radius of 0) - assert_eq!(rect.winding(Point::new(-10.0, 0.0)), 0); + assert_eq!(rect.winding(Point::new(0.0, 0.0), 1.0), 1); + assert_eq!(rect.winding(Point::new(-5.0, 0.0), 1.0), 1); // left edge + assert_eq!(rect.winding(Point::new(0.0, 20.0), 1.0), 1); // bottom edge + assert_eq!(rect.winding(Point::new(10.0, 20.0), 1.0), 0); // bottom-right corner + assert_eq!(rect.winding(Point::new(-5.0, 20.0), 1.0), 1); // bottom-left corner (has a radius of 0) + assert_eq!(rect.winding(Point::new(-10.0, 0.0), 1.0), 0); let rect = RoundedRect::new(-10.0, -20.0, 10.0, 20.0, 0.0); // rectangle - assert_eq!(rect.winding(Point::new(10.0, 20.0)), 1); // bottom-right corner + assert_eq!(rect.winding(Point::new(10.0, 20.0), 1.0), 1); // bottom-right corner } #[test] @@ -471,7 +471,7 @@ mod tests { let p = rect.to_path(1e-9); // Note: could be more systematic about tolerance tightness. let epsilon = 1e-7; - assert!((rect.area() - p.area()).abs() < epsilon); - assert_eq!(p.winding(Point::new(0.0, 0.0)), 1); + assert!((rect.area(1.0) - p.area(1.0)).abs() < epsilon); + assert_eq!(p.winding(Point::new(0.0, 0.0), 1.0), 1); } } diff --git a/src/shape.rs b/src/shape.rs index c850dab9..da2de696 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -111,11 +111,29 @@ pub trait Shape: Sized { /// positive. Thus, it is clockwise when down is increasing y (the /// usual convention for graphics), and anticlockwise when /// up is increasing y (the usual convention for math). - fn area(&self) -> f64; + /// + /// The `tolerance` parameter behaves the same as in [`path_elements()`], + /// allowing the default implmentation that first converts to a Beziér path. + /// Note this allows the returned area to be off by a most the tolerance times the perimiter. + /// + /// Tolerance should be ignored (by implementing this method directly) + /// if there is an easy way to return an accurate result. + fn area(&self, tolerance: f64) -> f64 { + segments(self.path_elements(tolerance)).area() + } /// Total length of perimeter. - //FIXME: document the accuracy param - fn perimeter(&self, accuracy: f64) -> f64; + /// + /// The `tolerance` parameter behaves the same as in [`path_elements()`], + /// allowing the default implmentation that first converts to a Beziér path. + /// In addition, it may also be used to control the accuracy of integration of the arc length + /// (note that varying a sufficiently smooth curve will vary the perimeter by a similar amount as the deflection). + /// + /// Tolerance should be ignored (by implementing this method directly) + /// if there is an easy way to return an accurate result. + fn perimeter(&self, tolerance: f64) -> f64 { + segments(self.path_elements(tolerance)).perimeter(tolerance) + } /// The [winding number] of a point. /// @@ -126,19 +144,34 @@ pub trait Shape: Sized { /// and -1 when it is inside a negative area shape. Of course, greater /// magnitude values are also possible when the shape is more complex. /// + /// The `tolerance` parameter behaves the same as in [`path_elements()`], + /// allowing the default implmentation that first converts to a Beziér path. + /// As a result, the returned value is allowed to be the winding number of + /// a point at most tolerance away from p. + /// + /// Tolerance should be ignored (by implementing this method directly) + /// if there is an easy way to return an accurate result. + /// /// [`area`]: Shape::area /// [winding number]: https://mathworld.wolfram.com/ContourWindingNumber.html - fn winding(&self, pt: Point) -> i32; + fn winding(&self, pt: Point, tolerance: f64) -> i32 { + segments(self.path_elements(tolerance)).winding(pt) + } - /// Returns `true` if the [`Point`] is inside this shape. + /// Returns `true` if the [`Point`] (up to tolerance, see [`winding`]) is inside this shape. /// /// This is only meaningful for closed shapes. - fn contains(&self, pt: Point) -> bool { - self.winding(pt) != 0 + fn contains(&self, pt: Point, tolerance: f64) -> bool { + self.winding(pt, tolerance) != 0 } /// The smallest rectangle that encloses the shape. - fn bounding_box(&self) -> Rect; + /// + /// The `tolerance` parameter behaves the same as in [`path_elements()`], + /// allowing the default implmentation that first converts to a Beziér path. + /// It should be ignored (by implementing this method directly) + /// if there is an easy way to return a precice result. + fn bounding_box(&self, tolerance: f64) -> Rect; /// If the shape is a line, make it available. fn as_line(&self) -> Option { @@ -189,20 +222,20 @@ impl<'a, T: Shape> Shape for &'a T { (*self).path_segments(tolerance) } - fn area(&self) -> f64 { - (*self).area() + fn area(&self, tolerance: f64) -> f64 { + (*self).area(tolerance) } - fn perimeter(&self, accuracy: f64) -> f64 { - (*self).perimeter(accuracy) + fn perimeter(&self, tolerance: f64) -> f64 { + (*self).perimeter(tolerance) } - fn winding(&self, pt: Point) -> i32 { - (*self).winding(pt) + fn winding(&self, pt: Point, tolerance: f64) -> i32 { + (*self).winding(pt, tolerance) } - fn bounding_box(&self) -> Rect { - (*self).bounding_box() + fn bounding_box(&self, tolerance: f64) -> Rect { + (*self).bounding_box(tolerance) } fn as_line(&self) -> Option { diff --git a/src/svg.rs b/src/svg.rs index ecdb1901..fbbeb287 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -513,7 +513,7 @@ mod tests { fn test_parse_svg_arc_pie() { let path = BezPath::from_svg("M 100 100 h 25 a 25 25 0 1 0 -25 25 z").unwrap(); // Approximate figures, but useful for regression testing - assert_eq!(path.area().round(), -1473.0); + assert_eq!(path.area(1.0).round(), -1473.0); assert_eq!(path.perimeter(1e-6).round(), 168.0); }