From 2da39e4c02469f133db50136d21d6f401bfb8d6b Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Wed, 22 Aug 2018 01:19:05 -0600 Subject: [PATCH] Arc line intersect, take 2 (#60) * add Line.point_to_t() and tests * add Arc.point_to_t() and tests * add a bunch of failing arc/line intersection tests This commit contains a bunch of failing arc/line intersections that I and other people have run into. All these tests are fixed in the following commit. * better implementation of Arc.intersect(Line) Fixes mathandy/svgpathtools#35. This commit fixes all the arc/line intersection test cases added in the previous commit. This implementation provides special handling in Arc.intersect() when `self` is a non-rotated Arc and `other_seg` is a Line. In this case it uses the straight-forward closed-form solution to identify the intersection points. Rotated Arcs and Arcs intersecting with non-Line objects still use the pre-existing intersection code, that part is totally untouched by this commit. --- svgpathtools/path.py | 265 +++++++++++++++++++++++++++++++++++- test/test_path.py | 316 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 579 insertions(+), 2 deletions(-) diff --git a/svgpathtools/path.py b/svgpathtools/path.py index b39c5cd..bcabff9 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, degrees, radians, log, pi +from math import sqrt, cos, sin, acos, asin, degrees, radians, log, pi from cmath import exp, sqrt as csqrt, phase from collections import MutableSequence from warnings import warn @@ -686,6 +686,29 @@ class Line(object): ymax = max(self.start.imag, self.end.imag) return xmin, xmax, ymin, ymax + def point_to_t(self, point): + """If the point lies on the Line, returns its `t` parameter. + If the point does not lie on the Line, returns None.""" + + # Single-precision floats have only 7 significant figures of + # resolution, so test that we're within 6 sig figs. + if np.isclose(point, self.start, rtol=0, atol=1e-6): + return 0.0 + elif np.isclose(point, self.end, rtol=0, atol=1e-6): + return 1.0 + + # Finding the point "by hand" here is much faster than calling + # radialrange(), see the discussion on PR #40: + # https://github.com/mathandy/svgpathtools/pull/40#issuecomment-358134261 + + p = self.poly() + # p(t) = (p_1 * t) + p_0 = point + # t = (point - p_0) / p_1 + t = (point - p[0]) / p[1] + if np.isclose(t.imag, 0) and (t.real >= 0.0) and (t.real <= 1.0): + return t.real + return None + def cropped(self, t0, t1): """returns a cropped copy of this segment which starts at self.point(t0) and ends at self.point(t1).""" @@ -1448,6 +1471,128 @@ class Arc(object): y = rx*sinphi*cos(angle) + ry*cosphi*sin(angle) + self.center.imag return complex(x, y) + def point_to_t(self, point): + """If the point lies on the Arc, returns its `t` parameter. + If the point does not lie on the Arc, returns None. + This function only works on Arcs with rotation == 0.0""" + + def in_range(min, max, val): + return (min <= val) and (max >= val) + + # Single-precision floats have only 7 significant figures of + # resolution, so test that we're within 6 sig figs. + if np.isclose(point, self.start, rtol=0.0, atol=1e-6): + return 0.0 + elif np.isclose(point, self.end, rtol=0.0, atol=1e-6): + return 1.0 + + if self.rotation != 0.0: + raise ValueError("Arc.point_to_t() only works on non-rotated Arcs.") + + v = point - self.center + distance_from_center = sqrt((v.real * v.real) + (v.imag * v.imag)) + min_radius = min(self.radius.real, self.radius.imag) + max_radius = max(self.radius.real, self.radius.imag) + if (distance_from_center < min_radius) and not np.isclose(distance_from_center, min_radius): + return None + if (distance_from_center > max_radius) and not np.isclose(distance_from_center, max_radius): + return None + + # x = center_x + radius_x cos(radians(theta + t delta)) + # y = center_y + radius_y sin(radians(theta + t delta)) + # + # For x: + # cos(radians(theta + t delta)) = (x - center_x) / radius_x + # radians(theta + t delta) = acos((x - center_x) / radius_x) + # theta + t delta = degrees(acos((x - center_x) / radius_x)) + # t_x = (degrees(acos((x - center_x) / radius_x)) - theta) / delta + # + # Similarly for y: + # t_y = (degrees(asin((y - center_y) / radius_y)) - theta) / delta + + x = point.real + y = point.imag + + # + # +Y points down! + # + # sweep mean clocwise + # sweep && (delta > 0) + # !sweep && (delta < 0) + # + # -180 <= theta_1 <= 180 + # + # large_arc && (-360 <= delta <= 360) + # !large_arc && (-180 < delta < 180) + # + + end_angle = self.theta + self.delta + min_angle = min(self.theta, end_angle) + max_angle = max(self.theta, end_angle) + + acos_arg = (x - self.center.real) / self.radius.real + if acos_arg > 1.0: + acos_arg = 1.0 + elif acos_arg < -1.0: + acos_arg = -1.0 + + x_angle_0 = degrees(acos(acos_arg)) + while x_angle_0 < min_angle: + x_angle_0 += 360.0 + while x_angle_0 > max_angle: + x_angle_0 -= 360.0 + + x_angle_1 = -1.0 * x_angle_0 + while x_angle_1 < min_angle: + x_angle_1 += 360.0 + while x_angle_1 > max_angle: + x_angle_1 -= 360.0 + + t_x_0 = (x_angle_0 - self.theta) / self.delta + t_x_1 = (x_angle_1 - self.theta) / self.delta + + asin_arg = (y - self.center.imag) / self.radius.imag + if asin_arg > 1.0: + asin_arg = 1.0 + elif asin_arg < -1.0: + asin_arg = -1.0 + + y_angle_0 = degrees(asin(asin_arg)) + while y_angle_0 < min_angle: + y_angle_0 += 360.0 + while y_angle_0 > max_angle: + y_angle_0 -= 360.0 + + y_angle_1 = 180 - y_angle_0 + while y_angle_1 < min_angle: + y_angle_1 += 360.0 + while y_angle_1 > max_angle: + y_angle_1 -= 360.0 + + t_y_0 = (y_angle_0 - self.theta) / self.delta + t_y_1 = (y_angle_1 - self.theta) / self.delta + + t = None + if np.isclose(t_x_0, t_y_0): + t = (t_x_0 + t_y_0) / 2.0 + elif np.isclose(t_x_0, t_y_1): + t= (t_x_0 + t_y_1) / 2.0 + elif np.isclose(t_x_1, t_y_0): + t = (t_x_1 + t_y_0) / 2.0 + elif np.isclose(t_x_1, t_y_1): + t = (t_x_1 + t_y_1) / 2.0 + else: + # Comparing None and float yields a result in python2, + # but throws TypeError in python3. This fix (suggested by + # @CatherineH) explicitly handles and avoids the case where + # the None-vs-float comparison would have happened below. + return None + + if (t >= 0.0) and (t <= 1.0): + return t + + return None + def centeriso(self, z): """This is an isometry that translates and rotates self so that it is centered on the origin and has its axes aligned with the xy axes.""" @@ -1619,7 +1764,123 @@ class Arc(object): to let me know if you're interested in such a feature -- or even better please submit an implementation if you want to code one.""" - if is_bezier_segment(other_seg): + # This special case can be easily solved algebraically. + if (self.rotation == 0) and isinstance(other_seg, Line): + a = self.radius.real + b = self.radius.imag + + # Ignore the ellipse's center point (to pretend that it's + # centered at the origin), and translate the Line to match. + l = Line(start=(other_seg.start-self.center), end=(other_seg.end-self.center)) + + # This gives us the translated Line as a parametric equation. + # s = p1 t + p0 + p = l.poly() + + if p[1].real == 0.0: + # The `x` value doesn't depend on `t`, the line is vertical. + c = p[0].real + x_values = [c] + + # Substitute the line `x = c` into the equation for the + # (origin-centered) ellipse. + # + # x^2/a^2 + y^2/b^2 = 1 + # c^2/a^2 + y^2/b^2 = 1 + # y^2/b^2 = 1 - c^2/a^2 + # y^2 = b^2(1 - c^2/a^2) + # y = +-b sqrt(1 - c^2/a^2) + + discriminant = 1 - (c * c)/(a * a) + if discriminant < 0: + return [] + elif discriminant == 0: + y_values = [0] + else: + val = b * sqrt(discriminant) + y_values = [val, -val] + + else: + # This is a non-vertical line. + # + # Convert the Line's parametric equation to the "y = mx + c" format. + # x = p1.real t + p0.real + # y = p1.imag t + p0.imag + # + # t = (x - p0.real) / p1.real + # t = (y - p0.imag) / p1.imag + # + # (y - p0.imag) / p1.imag = (x - p0.real) / p1.real + # (y - p0.imag) = ((x - p0.real) * p1.imag) / p1.real + # y = ((x - p0.real) * p1.imag) / p1.real + p0.imag + # y = (x p1.imag - p0.real * p1.imag) / p1.real + p0.imag + # y = x p1.imag/p1.real - p0.real p1.imag / p1.real + p0.imag + # m = p1.imag/p1.real + # c = -m p0.real + p0.imag + m = p[1].imag / p[1].real + c = (-m * p[0].real) + p[0].imag + + # Substitute the line's y(x) equation into the equation for + # the ellipse. We can pretend the ellipse is centered at the + # origin, since we shifted the Line by the ellipse's center. + # + # x^2/a^2 + y^2/b^2 = 1 + # x^2/a^2 + (mx+c)^2/b^2 = 1 + # (b^2 x^2 + a^2 (mx+c)^2)/(a^2 b^2) = 1 + # b^2 x^2 + a^2 (mx+c)^2 = a^2 b^2 + # b^2 x^2 + a^2(m^2 x^2 + 2mcx + c^2) = a^2 b^2 + # b^2 x^2 + a^2 m^2 x^2 + 2a^2 mcx + a^2 c^2 - a^2 b^2 = 0 + # (a^2 m^2 + b^2)x^2 + 2a^2 mcx + a^2(c^2 - b^2) = 0 + # + # The quadratic forumla tells us: x = (-B +- sqrt(B^2 - 4AC)) / 2A + # Where: + # A = a^2 m^2 + b^2 + # B = 2 a^2 mc + # C = a^2(c^2 - b^2) + # + # The determinant is: B^2 - 4AC + # + # The solution simplifies to: + # x = (-a^2 mc +- a b sqrt(a^2 m^2 + b^2 - c^2)) / (a^2 m^2 + b^2) + # + # Solving the line for x(y) and substituting *that* into + # the equation for the ellipse gives this solution for y: + # y = (b^2 c +- abm sqrt(a^2 m^2 + b^2 - c^2)) / (a^2 m^2 + b^2) + + denominator = (a * a * m * m) + (b * b) + + discriminant = denominator - (c * c) + if discriminant < 0: + return [] + + x_sqrt = a * b * sqrt(discriminant) + x1 = (-(a * a * m * c) + x_sqrt) / denominator + x2 = (-(a * a * m * c) - x_sqrt) / denominator + x_values = [x1] + if x1 != x2: + x_values.append(x2) + + y_sqrt = x_sqrt * m + y1 = ((b * b * c) + y_sqrt) / denominator + y2 = ((b * b * c) - y_sqrt) / denominator + y_values = [y1] + if y1 != y2: + y_values.append(y2) + + intersections = [] + for x in x_values: + for y in y_values: + p = complex(x, y) + self.center + my_t = self.point_to_t(p) + if my_t == None: + continue + other_t = other_seg.point_to_t(p) + if other_t == None: + continue + intersections.append([my_t, other_t]) + return intersections + + elif is_bezier_segment(other_seg): u1poly = self.u1transform(other_seg.poly()) u1poly_mag2 = real(u1poly)**2 + imag(u1poly)**2 t2s = polyroots01(u1poly_mag2 - 1) diff --git a/test/test_path.py b/test/test_path.py index ce4d86a..067d2c9 100644 --- a/test/test_path.py +++ b/test/test_path.py @@ -4,6 +4,7 @@ import unittest from math import sqrt, pi from operator import itemgetter import numpy as np +import random # Internal dependencies from svgpathtools import * @@ -16,6 +17,48 @@ from svgpathtools.path import _NotImplemented4ArcException # to be correct visually with the disvg() function. +def random_line(): + x = (random.random() - 0.5) * 2000 + y = (random.random() - 0.5) * 2000 + start = complex(x, y) + + x = (random.random() - 0.5) * 2000 + y = (random.random() - 0.5) * 2000 + end = complex(x, y) + + return Line(start, end) + + +def random_arc(): + x = (random.random() - 0.5) * 2000 + y = (random.random() - 0.5) * 2000 + start = complex(x, y) + + x = (random.random() - 0.5) * 2000 + y = (random.random() - 0.5) * 2000 + end = complex(x, y) + + x = (random.random() - 0.5) * 2000 + y = (random.random() - 0.5) * 2000 + radius = complex(x, y) + + large_arc = random.choice([True, False]) + sweep = random.choice([True, False]) + + return Arc(start=start, radius=radius, rotation=0.0, large_arc=large_arc, sweep=sweep, end=end) + + +def assert_intersections(a_seg, b_seg, intersections, count): + if count != None: + assert(len(intersections) == count) + for i in intersections: + assert(i[0] >= 0.0) + assert(i[0] <= 1.0) + assert(i[1] >= 0.0) + assert(i[1] <= 1.0) + assert(np.isclose(a_seg.point(i[0]), b_seg.point(i[1]))) + + class LineTest(unittest.TestCase): def test_lines(self): @@ -56,6 +99,73 @@ class LineTest(unittest.TestCase): self.assertTrue(line != str(line)) self.assertFalse(cubic == line) + def test_point_to_t(self): + l = Line(start=(0+0j), end=(0+10j)) + assert(l.point_to_t(0+0j) == 0.0) + assert(np.isclose(l.point_to_t(0+5j), 0.5)) + assert(l.point_to_t(0+10j) == 1.0) + assert(l.point_to_t(1+0j) == None) + assert(l.point_to_t(0-1j) == None) + assert(l.point_to_t(0+11j) == None) + + l = Line(start=(0+0j), end=(10+10j)) + assert(l.point_to_t(0+0j) == 0.0) + assert(np.isclose(l.point_to_t(5+5j), 0.5)) + assert(l.point_to_t(10+10j) == 1.0) + assert(l.point_to_t(1+0j) == None) + assert(l.point_to_t(0-1j) == None) + assert(l.point_to_t(0+11j) == None) + assert(l.point_to_t(10.001+10.001j) == None) + assert(l.point_to_t(-0.001-0.001j) == None) + + l = Line(start=(0+0j), end=(10+0j)) + assert(l.point_to_t(0+0j) == 0.0) + assert(np.isclose(l.point_to_t(5+0j), 0.5)) + assert(l.point_to_t(10+0j) == 1.0) + assert(l.point_to_t(0+1j) == None) + assert(l.point_to_t(0-1j) == None) + assert(l.point_to_t(0+11j) == None) + assert(l.point_to_t(10.001+0j) == None) + assert(l.point_to_t(-0.001-0j) == None) + + l = Line(start=(-2-1j), end=(11-20j)) + assert(l.point_to_t(-2-1j) == 0.0) + assert(np.isclose(l.point_to_t(4.5-10.5j), 0.5)) + assert(l.point_to_t(11-20j) == 1.0) + assert(l.point_to_t(0+1j) == None) + assert(l.point_to_t(0-1j) == None) + assert(l.point_to_t(0+11j) == None) + assert(l.point_to_t(10.001+0j) == None) + assert(l.point_to_t(-0.001-0j) == None) + + l = Line(start=(40.234-32.613j), end=(12.7-32.613j)) + assert(l.point_to_t(40.234-32.613j) == 0.0) + assert(np.isclose(l.point_to_t(33.3505-32.613j), 0.25)) + assert(np.isclose(l.point_to_t(26.467-32.613j), 0.50)) + assert(np.isclose(l.point_to_t(19.5835-32.613j), 0.75)) + assert(l.point_to_t(12.7-32.613j) == 1.0) + assert(l.point_to_t(40.25-32.613j) == None) + assert(l.point_to_t(12.65-32.613j) == None) + assert(l.point_to_t(11-20j) == None) + assert(l.point_to_t(0+1j) == None) + assert(l.point_to_t(0-1j) == None) + assert(l.point_to_t(0+11j) == None) + assert(l.point_to_t(10.001+0j) == None) + assert(l.point_to_t(-0.001-0j) == None) + + random.seed() + for line_index in range(100): + l = random_line() + print(l) + for t_index in range(100): + orig_t = random.random() + p = l.point(orig_t) + computed_t = l.point_to_t(p) + print("orig_t=%f, p=%s, computed_t=%f" % (orig_t, p, computed_t)) + assert(np.isclose(orig_t, computed_t)) + + + class CubicBezierTest(unittest.TestCase): def test_approx_circle(self): @@ -478,6 +588,71 @@ class ArcTest(unittest.TestCase): self.assertTrue(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)) self.assertTrue(segment != Arc(0j, 100 + 50j, 0, 1, 0, 100 + 50j)) + def test_point_to_t(self): + a = Arc(start=(0+0j), radius=(5+5j), rotation=0.0, large_arc=True, sweep=True, end=(0+10j)) + assert(a.point_to_t(0+0j) == 0.0) + assert(np.isclose(a.point_to_t(5+5j), 0.5)) + assert(a.point_to_t(0+10j) == 1.0) + assert(a.point_to_t(-5+5j) == None) + assert(a.point_to_t(0+5j) == None) + assert(a.point_to_t(1+0j) == None) + assert(a.point_to_t(0-1j) == None) + assert(a.point_to_t(0+11j) == None) + + a = Arc(start=(0+0j), radius=(5+5j), rotation=0.0, large_arc=True, sweep=False, end=(0+10j)) + assert(a.point_to_t(0+0j) == 0.0) + assert(np.isclose(a.point_to_t(-5+5j), 0.5)) + assert(a.point_to_t(0+10j) == 1.0) + assert(a.point_to_t(5+5j) == None) + assert(a.point_to_t(0+5j) == None) + assert(a.point_to_t(1+0j) == None) + assert(a.point_to_t(0-1j) == None) + assert(a.point_to_t(0+11j) == None) + + a = Arc(start=(-10+0j), radius=(10+20j), rotation=0.0, large_arc=True, sweep=True, end=(10+0j)) + assert(a.point_to_t(-10+0j) == 0.0) + assert(np.isclose(a.point_to_t(0-20j), 0.5)) + assert(a.point_to_t(10+0j) == 1.0) + assert(a.point_to_t(0+20j) == None) + assert(a.point_to_t(-5+5j) == None) + assert(a.point_to_t(0+5j) == None) + assert(a.point_to_t(1+0j) == None) + assert(a.point_to_t(0-1j) == None) + assert(a.point_to_t(0+11j) == None) + + a = Arc(start=(100.834+27.987j), radius=(60.6+60.6j), rotation=0.0, large_arc=False, sweep=False, end=(40.234-32.613j)) + assert(a.point_to_t(100.834+27.987j) == 0.0) + assert(np.isclose(a.point_to_t(96.2210993246+4.7963831644j), 0.25)) + assert(np.isclose(a.point_to_t(83.0846703014-14.8636715784j), 0.50)) + assert(np.isclose(a.point_to_t(63.4246151671-28.0001000158j), 0.75)) + assert(a.point_to_t(40.234-32.613j) == 1.00) + assert(a.point_to_t(-10+0j) == None) + assert(a.point_to_t(0+0j) == None) + + a = Arc(start=(423.049961698-41.3779390229j), radius=(904.283878032+597.298520765j), rotation=0.0, large_arc=True, sweep=False, end=(548.984030235-312.385118044j)) + orig_t = 0.854049465076 + p = a.point(orig_t) + computed_t = a.point_to_t(p) + assert(np.isclose(orig_t, computed_t)) + + a = Arc(start=(-1-750j), radius=(750+750j), rotation=0.0, large_arc=True, sweep=False, end=1-750j) + assert(np.isclose(a.point_to_t(730.5212132777968+169.8191111892562j), 0.71373858)) + assert(a.point_to_t(730.5212132777968+169j) == None) + assert(a.point_to_t(730.5212132777968+171j) == None) + + random.seed() + for arc_index in range(100): + a = random_arc() + print(a) + for t_index in range(100): + orig_t = random.random() + p = a.point(orig_t) + computed_t = a.point_to_t(p) + print("t:", orig_t) + print("p:", p) + print("computed t:", computed_t) + assert(np.isclose(orig_t, computed_t)) + class TestPath(unittest.TestCase): @@ -1205,6 +1380,147 @@ class Test_intersect(unittest.TestCase): assert(abs(l0.point(i[0][0])-l1.point(i[0][1])) < 1e-9) + def test_arc_line(self): + l = Line(start=(-20+1j), end=(20+1j)) + a = Arc(start=(-10+0), radius=(10+10j), rotation=0.0, large_arc=True, sweep=False, end=(10+0j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 2) + + l = Line(start=(-20-1j), end=(20-1j)) + a = Arc(start=(-10+0), radius=(10+10j), rotation=0.0, large_arc=True, sweep=False, end=(10+0j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 0) + + l = Line(start=(-20+1j), end=(20+1j)) + a = Arc(start=(-10+0), radius=(10+10j), rotation=0.0, large_arc=True, sweep=True, end=(10+0j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 0) + + l = Line(start=(-20-1j), end=(20-1j)) + a = Arc(start=(-10+0), radius=(10+10j), rotation=0.0, large_arc=True, sweep=True, end=(10+0j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 2) + + l = Line(start=(-20+0j), end=(20+0j)) + a = Arc(start=(-10+0), radius=(10+10j), rotation=0.0, large_arc=True, sweep=True, end=(10+0j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 2) + + l = Line(start=(-20+0j), end=(20+0j)) + a = Arc(start=(-10+0), radius=(10+10j), rotation=0.0, large_arc=True, sweep=False, end=(10+0j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 2) + + l = Line(start=(-20+10j), end=(20+10j)) + a = Arc(start=(-10+0), radius=(10+10j), rotation=0.0, large_arc=True, sweep=False, end=(10+0j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 1) + + l = Line(start=(229.226097475-282.403591377j), end=(751.681212592+188.907748894j)) + a = Arc(start=(-1-750j), radius=(750+750j), rotation=0.0, large_arc=True, sweep=False, end=(1-750j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 1) + + # end of arc touches start of horizontal line + l = Line(start=(40.234-32.613j), end=(12.7-32.613j)) + a = Arc(start=(100.834+27.987j), radius=(60.6+60.6j), rotation=0.0, large_arc=False, sweep=False, end=(40.234-32.613j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 1) + + # vertical line, intersects half-arc once + l = Line(start=(1-100j), end=(1+100j)) + a = Arc(start=(10.0+0j), radius=(10+10j), rotation=0, large_arc=False, sweep=True, end=(-10.0+0j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 1) + + # vertical line, intersects nearly-full arc twice + l = Line(start=(1-100j), end=(1+100j)) + a = Arc(start=(0.1-10j), radius=(10+10j), rotation=0, large_arc=True, sweep=True, end=(-0.1-10j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 2) + + # vertical line, start of line touches end of arc + l = Line(start=(15.4+100j), end=(15.4+90.475j)) + a = Arc(start=(25.4+90j), radius=(10+10j), rotation=0, large_arc=False, sweep=True, end=(15.4+100j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 1) + + l = Line(start=(100-60.913j), end=(40+59j)) + a = Arc(start=(100.834+27.987j), radius=(60.6+60.6j), rotation=0.0, large_arc=False, sweep=False, end=(40.234-32.613j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 1) + + l = Line(start=(128.57143 + 380.93364j), end=(300.00001 + 389.505069j)) + a = Arc(start=(214.28572 + 598.07649j), radius=(85.714287 + 108.57143j), rotation=0.0, large_arc=False, sweep=True, end=(128.57143 + 489.50507j)) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, 0) + + random.seed() + for arc_index in range(50): + a = random_arc() + print(a) + for line_index in range(100): + l = random_line() + print(l) + intersections = a.intersect(l) + assert_intersections(a, l, intersections, None) + + + def test_intersect_arc_line_1(self): + + """Verify the return value of intersects() when an Arc ends at + the starting point of a Line.""" + + a = Arc(start=(0+0j), radius=(10+10j), rotation=0, large_arc=False, + sweep=False, end=(10+10j), autoscale_radius=False) + l = Line(start=(10+10j), end=(20+10j)) + + i = a.intersect(l) + assert(len(i) == 1) + assert(i[0][0] == 1.0) + assert(i[0][1] == 0.0) + + + def test_intersect_arc_line_2(self): + + """Verify the return value of intersects() when an Arc is pierced + once by a Line.""" + + a = Arc(start=(0+0j), radius=(10+10j), rotation=0, large_arc=False, + sweep=False, end=(10+10j), autoscale_radius=False) + l = Line(start=(0+9j), end=(20+9j)) + + i = a.intersect(l) + assert(len(i) == 1) + assert(i[0][0] >= 0.0) + assert(i[0][0] <= 1.0) + assert(i[0][1] >= 0.0) + assert(i[0][1] <= 1.0) + + + def test_intersect_arc_line_3(self): + + """Verify the return value of intersects() when an Arc misses + a Line, but the circle that the Arc is part of hits the Line.""" + + a = Arc(start=(0+0j), radius=(10+10j), rotation=0, large_arc=False, + sweep=False, end=(10+10j), autoscale_radius=False) + l = Line(start=(11+100j), end=(11-100j)) + + i = a.intersect(l) + assert(len(i) == 0) + + + def test_intersect_arc_line_disjoint_bboxes(self): + # The arc is very short, which contributes to the problem here. + l = Line(start=(125.314540561+144.192926144j), end=(125.798713132+144.510685287j)) + a = Arc(start=(128.26640649+146.908463323j), radius=(2+2j), + rotation=0, large_arc=False, sweep=True, + end=(128.26640606+146.90846449j)) + i = l.intersect(a) + assert(i == []) + + class TestPathTools(unittest.TestCase): # moved from test_pathtools.py