Update Path.area to work with Arc segments (#65)

* add some tests of Path.area()

These tests currently fail because area() doesn't deal with Arc segments.
Fix in the following commit.

* make Path.area() approximate arcs

Fixes #37.

* Path.area(): fixup tabs/spaces for python3

* added asin to imports

* added asin to imports

* minor improvements to style, performance, and docstring
pull/76/head
Sebastian Kuzminsky 2018-11-04 22:40:56 -07:00 committed by Andy Port
parent e91a35c3da
commit b37e74f5f3
2 changed files with 73 additions and 12 deletions

View File

@ -4,7 +4,7 @@ Arc."""
# External dependencies # External dependencies
from __future__ import division, absolute_import, print_function 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 cmath import exp, sqrt as csqrt, phase
from collections import MutableSequence from collections import MutableSequence
from warnings import warn from warnings import warn
@ -2440,19 +2440,52 @@ class Path(MutableSequence):
# Ts += [self.t2T(i, t) for t in seg.icurvature(kappa)] # Ts += [self.t2T(i, t) for t in seg.icurvature(kappa)]
# return Ts # return Ts
def area(self): def area(self, chord_length=1e-2):
"""returns the area enclosed by this Path object. """Find area enclosed by path.
Note: negative area results from CW (as opposed to CCW)
parameterization of the Path object.""" 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() assert self.isclosed()
area_enclosed = 0
bezier_path_approximation = Path()
for seg in self: for seg in self:
x = real(seg.poly()) if isinstance(seg, Arc):
dy = imag(seg.poly()).deriv() num_lines = ceil(seg.length() / chord_length) # check curvature to improve
integrand = x*dy bezier_path_approximation = \
integral = integrand.integ() [Line(seg.point(i/num_lines), seg.point((i+1)/num_lines))
area_enclosed += integral(1) - integral(0) for i in range(int(num_lines))]
return area_enclosed else:
approximated_path.append(seg)
return area_without_arcs(approximated_path)
def intersect(self, other_curve, justonemode=False, tol=1e-12): def intersect(self, other_curve, justonemode=False, tol=1e-12):
"""returns list of pairs of pairs ((T1, seg1, t1), (T2, seg2, t2)) """returns list of pairs of pairs ((T1, seg1, t1), (T2, seg2, t2))

View File

@ -1747,5 +1747,33 @@ class TestPathTools(unittest.TestCase):
# openinbrowser=True) # 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__': if __name__ == '__main__':
unittest.main() unittest.main()