diff --git a/svgpathtools/path.py b/svgpathtools/path.py index bcabff9..70d8ae3 100644 --- a/svgpathtools/path.py +++ b/svgpathtools/path.py @@ -4,7 +4,7 @@ Arc.""" # External dependencies from __future__ import division, absolute_import, print_function -from math import sqrt, cos, sin, acos, asin, degrees, radians, log, pi +from math import sqrt, cos, sin, acos, asin, degrees, radians, log, pi, ceil from cmath import exp, sqrt as csqrt, phase from collections import MutableSequence from warnings import warn @@ -2440,19 +2440,52 @@ class Path(MutableSequence): # Ts += [self.t2T(i, t) for t in seg.icurvature(kappa)] # return Ts - def area(self): - """returns the area enclosed by this Path object. - Note: negative area results from CW (as opposed to CCW) - parameterization of the Path object.""" + def area(self, chord_length=1e-2): + """Find area enclosed by path. + + Approximates any Arc segments in the Path with lines + approximately `chord_length` long, and returns the area enclosed + by the approximated Path. Default chord length is 0.01. To + ensure accurate results, make sure this `chord_length` is set to + a reasonable value (e.g. by checking curvature). + + Notes + ---- + * Negative area results from clockwise (as opposed to + counter-clockwise) parameterization of the input Path. + + To Contributors + --------------- + This is one of many parts of `svgpathtools` that could be + improved by a noble soul implementing a piecewise-linear + approximation scheme for paths (one with controls to + guarantee a desired accuracies). + """ + + def area_without_arcs(self): + area_enclosed = 0 + for seg in self: + x = real(seg.poly()) + dy = imag(seg.poly()).deriv() + integrand = x*dy + integral = integrand.integ() + area_enclosed += integral(1) - integral(0) + return area_enclosed + assert self.isclosed() - area_enclosed = 0 + + bezier_path_approximation = Path() for seg in self: - x = real(seg.poly()) - dy = imag(seg.poly()).deriv() - integrand = x*dy - integral = integrand.integ() - area_enclosed += integral(1) - integral(0) - return area_enclosed + if isinstance(seg, Arc): + num_lines = ceil(seg.length() / chord_length) # check curvature to improve + bezier_path_approximation = \ + [Line(seg.point(i/num_lines), seg.point((i+1)/num_lines)) + for i in range(int(num_lines))] + else: + approximated_path.append(seg) + + return area_without_arcs(approximated_path) + def intersect(self, other_curve, justonemode=False, tol=1e-12): """returns list of pairs of pairs ((T1, seg1, t1), (T2, seg2, t2)) diff --git a/test/test_path.py b/test/test_path.py index c58b182..079bb84 100644 --- a/test/test_path.py +++ b/test/test_path.py @@ -1747,5 +1747,33 @@ class TestPathTools(unittest.TestCase): # openinbrowser=True) + def test_path_area(self): + cw_square = Path() + cw_square.append(Line((0+0j), (0+100j))) + cw_square.append(Line((0+100j), (100+100j))) + cw_square.append(Line((100+100j), (100+0j))) + cw_square.append(Line((100+0j), (0+0j))) + self.assertEqual(cw_square.area(), -10000.0) + + ccw_square = Path() + ccw_square.append(Line((0+0j), (100+0j))) + ccw_square.append(Line((100+0j), (100+100j))) + ccw_square.append(Line((100+100j), (0+100j))) + ccw_square.append(Line((0+100j), (0+0j))) + self.assertEqual(ccw_square.area(), 10000.0) + + cw_half_circle = Path() + cw_half_circle.append(Line((0+0j), (0+100j))) + cw_half_circle.append(Arc(start=(0+100j), radius=(50+50j), rotation=0, large_arc=False, sweep=False, end=(0+0j))) + self.assertAlmostEqual(cw_half_circle.area(), -3926.9908169872415, places=3) + self.assertAlmostEqual(cw_half_circle.area(chord_length=1e-3), -3926.9908169872415, places=6) + + ccw_half_circle = Path() + ccw_half_circle.append(Line((0+100j), (0+0j))) + ccw_half_circle.append(Arc(start=(0+0j), radius=(50+50j), rotation=0, large_arc=False, sweep=True, end=(0+100j))) + self.assertAlmostEqual(ccw_half_circle.area(), 3926.9908169872415, places=3) + self.assertAlmostEqual(ccw_half_circle.area(chord_length=1e-3), 3926.9908169872415, places=6) + + if __name__ == '__main__': unittest.main()