Arc line intersect, 3rd try (#64)
* add Line.point_to_t() and tests These tests don't print anything while they run, and they use use the assert() helpers from the unittest module. * add Arc.point_to_t() and tests These tests don't print anything while they run, and they use use the assert() helpers from the unittest module. * 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. These tests don't print anything while they run, and they use use the assert() helpers from the unittest module where possible. 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.pull/65/head^2
parent
d810653b63
commit
2feb3c92b5
|
@ -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, 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 cmath import exp, sqrt as csqrt, phase
|
||||||
from collections import MutableSequence
|
from collections import MutableSequence
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
@ -686,6 +686,29 @@ class Line(object):
|
||||||
ymax = max(self.start.imag, self.end.imag)
|
ymax = max(self.start.imag, self.end.imag)
|
||||||
return xmin, xmax, ymin, ymax
|
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):
|
def cropped(self, t0, t1):
|
||||||
"""returns a cropped copy of this segment which starts at
|
"""returns a cropped copy of this segment which starts at
|
||||||
self.point(t0) and ends at self.point(t1)."""
|
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
|
y = rx*sinphi*cos(angle) + ry*cosphi*sin(angle) + self.center.imag
|
||||||
return complex(x, y)
|
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):
|
def centeriso(self, z):
|
||||||
"""This is an isometry that translates and rotates self so that it
|
"""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."""
|
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
|
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."""
|
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 = self.u1transform(other_seg.poly())
|
||||||
u1poly_mag2 = real(u1poly)**2 + imag(u1poly)**2
|
u1poly_mag2 = real(u1poly)**2 + imag(u1poly)**2
|
||||||
t2s = polyroots01(u1poly_mag2 - 1)
|
t2s = polyroots01(u1poly_mag2 - 1)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import unittest
|
||||||
from math import sqrt, pi
|
from math import sqrt, pi
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import random
|
||||||
|
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
from svgpathtools import *
|
from svgpathtools import *
|
||||||
|
@ -16,6 +17,48 @@ from svgpathtools.path import _NotImplemented4ArcException
|
||||||
# to be correct visually with the disvg() function.
|
# 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):
|
class LineTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_lines(self):
|
def test_lines(self):
|
||||||
|
@ -56,6 +99,71 @@ class LineTest(unittest.TestCase):
|
||||||
self.assertTrue(line != str(line))
|
self.assertTrue(line != str(line))
|
||||||
self.assertFalse(cubic == line)
|
self.assertFalse(cubic == line)
|
||||||
|
|
||||||
|
def test_point_to_t(self):
|
||||||
|
l = Line(start=(0+0j), end=(0+10j))
|
||||||
|
self.assertEqual(l.point_to_t(0+0j), 0.0)
|
||||||
|
self.assertAlmostEqual(l.point_to_t(0+5j), 0.5)
|
||||||
|
self.assertEqual(l.point_to_t(0+10j), 1.0)
|
||||||
|
self.assertIsNone(l.point_to_t(1+0j))
|
||||||
|
self.assertIsNone(l.point_to_t(0-1j))
|
||||||
|
self.assertIsNone(l.point_to_t(0+11j))
|
||||||
|
|
||||||
|
l = Line(start=(0+0j), end=(10+10j))
|
||||||
|
self.assertEqual(l.point_to_t(0+0j), 0.0)
|
||||||
|
self.assertAlmostEqual(l.point_to_t(5+5j), 0.5)
|
||||||
|
self.assertEqual(l.point_to_t(10+10j), 1.0)
|
||||||
|
self.assertIsNone(l.point_to_t(1+0j))
|
||||||
|
self.assertIsNone(l.point_to_t(0-1j))
|
||||||
|
self.assertIsNone(l.point_to_t(0+11j))
|
||||||
|
self.assertIsNone(l.point_to_t(10.001+10.001j))
|
||||||
|
self.assertIsNone(l.point_to_t(-0.001-0.001j))
|
||||||
|
|
||||||
|
l = Line(start=(0+0j), end=(10+0j))
|
||||||
|
self.assertEqual(l.point_to_t(0+0j), 0.0)
|
||||||
|
self.assertAlmostEqual(l.point_to_t(5+0j), 0.5)
|
||||||
|
self.assertEqual(l.point_to_t(10+0j), 1.0)
|
||||||
|
self.assertIsNone(l.point_to_t(0+1j))
|
||||||
|
self.assertIsNone(l.point_to_t(0-1j))
|
||||||
|
self.assertIsNone(l.point_to_t(0+11j))
|
||||||
|
self.assertIsNone(l.point_to_t(10.001+0j))
|
||||||
|
self.assertIsNone(l.point_to_t(-0.001-0j))
|
||||||
|
|
||||||
|
l = Line(start=(-2-1j), end=(11-20j))
|
||||||
|
self.assertEqual(l.point_to_t(-2-1j), 0.0)
|
||||||
|
self.assertAlmostEqual(l.point_to_t(4.5-10.5j), 0.5)
|
||||||
|
self.assertEqual(l.point_to_t(11-20j), 1.0)
|
||||||
|
self.assertIsNone(l.point_to_t(0+1j))
|
||||||
|
self.assertIsNone(l.point_to_t(0-1j))
|
||||||
|
self.assertIsNone(l.point_to_t(0+11j))
|
||||||
|
self.assertIsNone(l.point_to_t(10.001+0j))
|
||||||
|
self.assertIsNone(l.point_to_t(-0.001-0j))
|
||||||
|
|
||||||
|
l = Line(start=(40.234-32.613j), end=(12.7-32.613j))
|
||||||
|
self.assertEqual(l.point_to_t(40.234-32.613j), 0.0)
|
||||||
|
self.assertAlmostEqual(l.point_to_t(33.3505-32.613j), 0.25)
|
||||||
|
self.assertAlmostEqual(l.point_to_t(26.467-32.613j), 0.50)
|
||||||
|
self.assertAlmostEqual(l.point_to_t(19.5835-32.613j), 0.75)
|
||||||
|
self.assertEqual(l.point_to_t(12.7-32.613j), 1.0)
|
||||||
|
self.assertIsNone(l.point_to_t(40.25-32.613j))
|
||||||
|
self.assertIsNone(l.point_to_t(12.65-32.613j))
|
||||||
|
self.assertIsNone(l.point_to_t(11-20j))
|
||||||
|
self.assertIsNone(l.point_to_t(0+1j))
|
||||||
|
self.assertIsNone(l.point_to_t(0-1j))
|
||||||
|
self.assertIsNone(l.point_to_t(0+11j))
|
||||||
|
self.assertIsNone(l.point_to_t(10.001+0j))
|
||||||
|
self.assertIsNone(l.point_to_t(-0.001-0j))
|
||||||
|
|
||||||
|
random.seed()
|
||||||
|
for line_index in range(100):
|
||||||
|
l = random_line()
|
||||||
|
for t_index in range(100):
|
||||||
|
orig_t = random.random()
|
||||||
|
p = l.point(orig_t)
|
||||||
|
computed_t = l.point_to_t(p)
|
||||||
|
self.assertAlmostEqual(orig_t, computed_t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CubicBezierTest(unittest.TestCase):
|
class CubicBezierTest(unittest.TestCase):
|
||||||
def test_approx_circle(self):
|
def test_approx_circle(self):
|
||||||
|
@ -478,6 +586,67 @@ class ArcTest(unittest.TestCase):
|
||||||
self.assertTrue(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j))
|
self.assertTrue(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j))
|
||||||
self.assertTrue(segment != Arc(0j, 100 + 50j, 0, 1, 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))
|
||||||
|
self.assertEqual(a.point_to_t(0+0j), 0.0)
|
||||||
|
self.assertAlmostEqual(a.point_to_t(5+5j), 0.5)
|
||||||
|
self.assertEqual(a.point_to_t(0+10j), 1.0)
|
||||||
|
self.assertIsNone(a.point_to_t(-5+5j))
|
||||||
|
self.assertIsNone(a.point_to_t(0+5j))
|
||||||
|
self.assertIsNone(a.point_to_t(1+0j))
|
||||||
|
self.assertIsNone(a.point_to_t(0-1j))
|
||||||
|
self.assertIsNone(a.point_to_t(0+11j))
|
||||||
|
|
||||||
|
a = Arc(start=(0+0j), radius=(5+5j), rotation=0.0, large_arc=True, sweep=False, end=(0+10j))
|
||||||
|
self.assertEqual(a.point_to_t(0+0j), 0.0)
|
||||||
|
self.assertAlmostEqual(a.point_to_t(-5+5j), 0.5)
|
||||||
|
self.assertEqual(a.point_to_t(0+10j), 1.0)
|
||||||
|
self.assertIsNone(a.point_to_t(5+5j))
|
||||||
|
self.assertIsNone(a.point_to_t(0+5j))
|
||||||
|
self.assertIsNone(a.point_to_t(1+0j))
|
||||||
|
self.assertIsNone(a.point_to_t(0-1j))
|
||||||
|
self.assertIsNone(a.point_to_t(0+11j))
|
||||||
|
|
||||||
|
a = Arc(start=(-10+0j), radius=(10+20j), rotation=0.0, large_arc=True, sweep=True, end=(10+0j))
|
||||||
|
self.assertEqual(a.point_to_t(-10+0j), 0.0)
|
||||||
|
self.assertAlmostEqual(a.point_to_t(0-20j), 0.5)
|
||||||
|
self.assertEqual(a.point_to_t(10+0j), 1.0)
|
||||||
|
self.assertIsNone(a.point_to_t(0+20j))
|
||||||
|
self.assertIsNone(a.point_to_t(-5+5j))
|
||||||
|
self.assertIsNone(a.point_to_t(0+5j))
|
||||||
|
self.assertIsNone(a.point_to_t(1+0j))
|
||||||
|
self.assertIsNone(a.point_to_t(0-1j))
|
||||||
|
self.assertIsNone(a.point_to_t(0+11j))
|
||||||
|
|
||||||
|
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))
|
||||||
|
self.assertEqual(a.point_to_t(100.834+27.987j), 0.0)
|
||||||
|
self.assertAlmostEqual(a.point_to_t(96.2210993246+4.7963831644j), 0.25)
|
||||||
|
self.assertAlmostEqual(a.point_to_t(83.0846703014-14.8636715784j), 0.50)
|
||||||
|
self.assertAlmostEqual(a.point_to_t(63.4246151671-28.0001000158j), 0.75)
|
||||||
|
self.assertEqual(a.point_to_t(40.234-32.613j), 1.00)
|
||||||
|
self.assertIsNone(a.point_to_t(-10+0j))
|
||||||
|
self.assertIsNone(a.point_to_t(0+0j))
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.assertAlmostEqual(orig_t, computed_t)
|
||||||
|
|
||||||
|
a = Arc(start=(-1-750j), radius=(750+750j), rotation=0.0, large_arc=True, sweep=False, end=1-750j)
|
||||||
|
self.assertAlmostEqual(a.point_to_t(730.5212132777968+169.8191111892562j), 0.71373858)
|
||||||
|
self.assertIsNone(a.point_to_t(730.5212132777968+169j))
|
||||||
|
self.assertIsNone(a.point_to_t(730.5212132777968+171j))
|
||||||
|
|
||||||
|
random.seed()
|
||||||
|
for arc_index in range(100):
|
||||||
|
a = random_arc()
|
||||||
|
for t_index in range(100):
|
||||||
|
orig_t = random.random()
|
||||||
|
p = a.point(orig_t)
|
||||||
|
computed_t = a.point_to_t(p)
|
||||||
|
self.assertAlmostEqual(orig_t, computed_t)
|
||||||
|
|
||||||
|
|
||||||
class TestPath(unittest.TestCase):
|
class TestPath(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -1205,6 +1374,145 @@ class Test_intersect(unittest.TestCase):
|
||||||
assert(abs(l0.point(i[0][0])-l1.point(i[0][1])) < 1e-9)
|
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()
|
||||||
|
for line_index in range(100):
|
||||||
|
l = random_line()
|
||||||
|
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)
|
||||||
|
self.assertEqual(len(i), 1)
|
||||||
|
self.assertEqual(i[0][0], 1.0)
|
||||||
|
self.assertEqual(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)
|
||||||
|
self.assertEqual(len(i), 1)
|
||||||
|
self.assertGreaterEqual(i[0][0], 0.0)
|
||||||
|
self.assertLessEqual(i[0][0], 1.0)
|
||||||
|
self.assertGreaterEqual(i[0][1], 0.0)
|
||||||
|
self.assertLessEqual(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)
|
||||||
|
self.assertEqual(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)
|
||||||
|
self.assertEqual(i, [])
|
||||||
|
|
||||||
|
|
||||||
class TestPathTools(unittest.TestCase):
|
class TestPathTools(unittest.TestCase):
|
||||||
# moved from test_pathtools.py
|
# moved from test_pathtools.py
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue