Bug Corrections. Closes #113, #95, #94, and #71 (#136)

* Closes #113

Previous fix stopped working because numpy is more liberal dividing by zero and returning nan values.

* Closes #71

* Closes #95

* Closes #94
pull/149/head
tatarize 2021-01-16 20:08:58 -08:00 committed by GitHub
parent 091394b5e3
commit 3a1fe8695d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 77 additions and 29 deletions

View File

@ -18,7 +18,7 @@ from itertools import tee
# in order to encourage code that generalizes to vector inputs # in order to encourage code that generalizes to vector inputs
from numpy import sqrt, cos, sin, tan, arccos as acos, arcsin as asin, \ from numpy import sqrt, cos, sin, tan, arccos as acos, arcsin as asin, \
degrees, radians, log, pi, ceil degrees, radians, log, pi, ceil
from numpy import exp, sqrt as csqrt, angle as phase from numpy import exp, sqrt as csqrt, angle as phase, isnan
try: try:
from scipy.integrate import quad from scipy.integrate import quad
@ -898,31 +898,30 @@ class QuadraticBezier(object):
if abs(a) < 1e-12: if abs(a) < 1e-12:
s = abs(b)*(t1 - t0) s = abs(b)*(t1 - t0)
elif abs(a_dot_b + abs(a)*abs(b)) < 1e-12:
tstar = abs(b)/(2*abs(a))
if t1 < tstar:
return abs(a)*(t0**2 - t1**2) - abs(b)*(t0 - t1)
elif tstar < t0:
return abs(a)*(t1**2 - t0**2) - abs(b)*(t1 - t0)
else: else:
return abs(a)*(t1**2 + t0**2) - abs(b)*(t1 + t0) + \ c2 = 4 * (a.real ** 2 + a.imag ** 2)
abs(b)**2/(2*abs(a)) c1 = 4 * a_dot_b
else: c0 = b.real ** 2 + b.imag ** 2
c2 = 4*(a.real**2 + a.imag**2)
c1 = 4*a_dot_b
c0 = b.real**2 + b.imag**2
beta = c1/(2*c2) beta = c1 / (2 * c2)
gamma = c0/c2 - beta**2 gamma = c0 / c2 - beta ** 2
dq1_mag = sqrt(c2*t1**2 + c1*t1 + c0) dq1_mag = sqrt(c2 * t1 ** 2 + c1 * t1 + c0)
dq0_mag = sqrt(c2*t0**2 + c1*t0 + c0) dq0_mag = sqrt(c2 * t0 ** 2 + c1 * t0 + c0)
logarand = (sqrt(c2)*(t1 + beta) + dq1_mag) / \ logarand = (sqrt(c2) * (t1 + beta) + dq1_mag) / \
(sqrt(c2)*(t0 + beta) + dq0_mag) (sqrt(c2) * (t0 + beta) + dq0_mag)
s = (t1 + beta) * dq1_mag - (t0 + beta) * dq0_mag + \
s = (t1 + beta)*dq1_mag - (t0 + beta)*dq0_mag + \ gamma * sqrt(c2) * log(logarand)
gamma*sqrt(c2)*log(logarand)
s /= 2 s /= 2
if isnan(s):
tstar = abs(b) / (2 * abs(a))
if t1 < tstar:
return abs(a) * (t0 ** 2 - t1 ** 2) - abs(b) * (t0 - t1)
elif tstar < t0:
return abs(a) * (t1 ** 2 - t0 ** 2) - abs(b) * (t1 - t0)
else:
return abs(a) * (t1 ** 2 + t0 ** 2) - abs(b) * (t1 + t0) + \
abs(b) ** 2 / (2 * abs(a))
if t0 == 1 and t1 == 0: if t0 == 1 and t1 == 0:
self._length_info['length'] = s self._length_info['length'] = s
@ -2445,7 +2444,10 @@ class Path(MutableSequence):
lengths = [each.length(error=error, min_depth=min_depth) for each in lengths = [each.length(error=error, min_depth=min_depth) for each in
self._segments] self._segments]
self._length = sum(lengths) self._length = sum(lengths)
self._lengths = [each/self._length for each in lengths] if self._length == 0:
self._lengths = lengths # all lengths are 0.
else:
self._lengths = [each / self._length for each in lengths]
def point(self, pos): def point(self, pos):
@ -2522,7 +2524,10 @@ class Path(MutableSequence):
return self.start == self.end return self.start == self.end
def _is_closable(self): def _is_closable(self):
try:
end = self[-1].end end = self[-1].end
except IndexError:
return True
for segment in self: for segment in self:
if segment.start == end: if segment.start == end:
return True return True
@ -2574,7 +2579,8 @@ class Path(MutableSequence):
"""Returns a path d-string for the path object. """Returns a path d-string for the path object.
For an explanation of useSandT and use_closed_attrib, see the For an explanation of useSandT and use_closed_attrib, see the
compatibility notes in the README.""" compatibility notes in the README."""
if len(self) == 0:
return ''
if use_closed_attrib: if use_closed_attrib:
self_closed = self.iscontinuous() and self.isclosed() self_closed = self.iscontinuous() and self.isclosed()
if self_closed: if self_closed:
@ -2866,8 +2872,7 @@ class Path(MutableSequence):
# redundant intersection. This code block checks for and removes said # redundant intersection. This code block checks for and removes said
# redundancies. # redundancies.
if intersection_list: if intersection_list:
pts = [seg1.point(_t1) pts = [_seg1.point(_t1) for _T1, _seg1, _t1 in list(zip(*intersection_list))[0]]
for _T1, _seg1, _t1 in list(zip(*intersection_list))[0]]
indices2remove = [] indices2remove = []
for ind1 in range(len(pts)): for ind1 in range(len(pts)):
for ind2 in range(ind1 + 1, len(pts)): for ind2 in range(ind1 + 1, len(pts)):

View File

@ -687,7 +687,6 @@ class ArcTest(unittest.TestCase):
self.assertAlmostEqual(d,0.0, delta=2) self.assertAlmostEqual(d,0.0, delta=2)
class TestPath(unittest.TestCase): class TestPath(unittest.TestCase):
def test_circle(self): def test_circle(self):
@ -1660,7 +1659,6 @@ class Test_intersect(unittest.TestCase):
assert_intersections(a0, a1, intersections, 0) assert_intersections(a0, a1, intersections, 0)
class TestPathTools(unittest.TestCase): class TestPathTools(unittest.TestCase):
# moved from test_pathtools.py # moved from test_pathtools.py
@ -1950,5 +1948,50 @@ class TestPathTools(unittest.TestCase):
self.assertTrue(enclosing_shape.is_contained_by(larger_shape)) self.assertTrue(enclosing_shape.is_contained_by(larger_shape))
class TestPathBugs(unittest.TestCase):
def test_issue_113(self):
"""
Tests against issue regebro/svg.path#61 mathandy/svgpathtools#113
"""
p = Path('M 206.5,525 Q 162.5,583 162.5,583')
self.assertAlmostEqual(p.length(), 72.80109889280519)
p = Path('M 425.781 446.289 Q 410.40000000000003 373.047 410.4 373.047')
self.assertAlmostEqual(p.length(), 74.83959997888816)
p = Path('M 639.648 568.115 Q 606.6890000000001 507.568 606.689 507.568')
self.assertAlmostEqual(p.length(), 68.93645544992873)
p = Path('M 288.818 616.699 Q 301.025 547.3629999999999 301.025 547.363')
self.assertAlmostEqual(p.length(), 70.40235610403947)
p = Path('M 339.927 706.25 Q 243.92700000000002 806.25 243.927 806.25')
self.assertAlmostEqual(p.length(), 138.6217876093077)
p = Path('M 539.795 702.637 Q 548.0959999999999 803.4669999999999 548.096 803.467')
self.assertAlmostEqual(p.length(), 101.17111989594662)
p = Path('M 537.815 555.042 Q 570.1680000000001 499.1600000000001 570.168 499.16')
self.assertAlmostEqual(p.length(), 64.57177814649368)
p = Path('M 615.297 470.503 Q 538.797 694.5029999999999 538.797 694.503')
self.assertAlmostEqual(p.length(), 236.70287281737836)
def test_issue_71(self):
p = Path("M327 468z")
m = p.closed
q = p.d() # Failing to Crash is good.
def test_issue_95(self):
"""
Corrects:
https://github.com/mathandy/svgpathtools/issues/95
"""
p = Path('M261 166 L261 166')
self.assertEqual(p.length(), 0)
def test_issue_94(self):
# clipping rectangle
p1 = Path('M0.0 0.0 L27.84765625 0.0 L27.84765625 242.6669922 L0.0 242.6669922 z')
# clipping rectangle
p2 = Path('M166.8359375,235.5478516c0,3.7773438-3.0859375,6.8691406-6.8701172,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805c3.7841797,0,6.8701172,3.0927734,6.8701172,6.8701172v228.4277344z')
self.assertEqual(len(p1.intersect(p2)), len(p2.intersect(p1)))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()