fix Arc/Arc intersections (#110)
* add some failing Arc.intersect(Arc) tests * implementing Arc.intersect(Arc) This commit adds special handling in Arc.intersect() when the other segment is an Arc, and when both segments are circular and non-rotated. This particular case is common, and quick and easy to solve algebraically. This commit fixes the failing tests added in the previous commit.pull/114/head
parent
5ae88df6d5
commit
685f9a6eaf
|
@ -1911,8 +1911,124 @@ class Arc(object):
|
|||
t2s = polyroots01(u1poly_mag2 - 1)
|
||||
t1s = [self.phase2t(phase(u1poly(t2))) for t2 in t2s]
|
||||
return list(zip(t1s, t2s))
|
||||
|
||||
elif isinstance(other_seg, Arc):
|
||||
assert other_seg != self
|
||||
|
||||
import sys
|
||||
|
||||
# From "Intersection of two circles", at
|
||||
# http://paulbourke.net/geometry/circlesphere/
|
||||
|
||||
# It's easy to find the intersections of two circles, so
|
||||
# compute that and see if any of those
|
||||
# intersection points are on the arcs.
|
||||
if (self.rotation == 0) and (self.radius.real == self.radius.imag) and (other_seg.rotation == 0) and (other_seg.radius.real == other_seg.radius.imag):
|
||||
r0 = self.radius.real
|
||||
r1 = other_seg.radius.real
|
||||
p0 = self.center
|
||||
p1 = other_seg.center
|
||||
d = abs(p0 - p1)
|
||||
possible_inters = []
|
||||
|
||||
if d > (r0 + r1):
|
||||
# The circles are farther apart than the sum of
|
||||
# their radii: no intersections possible.
|
||||
pass
|
||||
|
||||
elif d < abs(r0 - r1):
|
||||
# The small circle is wholly contained within the
|
||||
# large circle: no intersections possible.
|
||||
pass
|
||||
|
||||
elif (np.isclose(d, 0, rtol=0.0, atol=1e-6)) and (np.isclose(r0, r1, rtol=0.0, atol=1e-6)):
|
||||
# The Arcs lie on the same circle: they have the
|
||||
# same center and are of equal radius.
|
||||
|
||||
def point_in_seg_interior(point, seg):
|
||||
t = seg.point_to_t(point)
|
||||
if t is None: return False
|
||||
if np.isclose(t, 0.0, rtol=0.0, atol=1e-6): return False
|
||||
if np.isclose(t, 1.0, rtol=0.0, atol=1e-6): return False
|
||||
return True
|
||||
|
||||
# If either end of either segment is in the interior
|
||||
# of the other segment, then the Arcs overlap
|
||||
# in an infinite number of points, and we return
|
||||
# "no intersections".
|
||||
if point_in_seg_interior(self.start, other_seg): return []
|
||||
if point_in_seg_interior(self.end, other_seg): return []
|
||||
if point_in_seg_interior(other_seg.start, self): return []
|
||||
if point_in_seg_interior(other_seg.end, self): return []
|
||||
|
||||
# If they touch at their endpoint(s) and don't go
|
||||
# in "overlapping directions", then we accept that
|
||||
# as intersections.
|
||||
|
||||
if (self.start == other_seg.start) and (self.sweep != other_seg.sweep):
|
||||
possible_inters.append((0.0, 0.0))
|
||||
|
||||
if (self.start == other_seg.end) and (self.sweep == other_seg.sweep):
|
||||
possible_inters.append((0.0, 1.0))
|
||||
|
||||
if (self.end == other_seg.start) and (self.sweep == other_seg.sweep):
|
||||
possible_inters.append((1.0, 0.0))
|
||||
|
||||
if (self.end == other_seg.end) and (self.sweep != other_seg.sweep):
|
||||
possible_inters.append((1.0, 1.0))
|
||||
|
||||
elif np.isclose(d, r0 + r1, rtol=0.0, atol=1e-6):
|
||||
# The circles are tangent, so the Arcs may touch
|
||||
# at exactly one point. The circles lie outside
|
||||
# each other.
|
||||
l = Line(start=p0, end=p1)
|
||||
p = l.point(r0/d)
|
||||
possible_inters.append((self.point_to_t(p), other_seg.point_to_t(p)))
|
||||
|
||||
elif np.isclose(d, abs(r0 - r1), rtol=0.0, atol=1e-6):
|
||||
# The circles are tangent, so the Arcs may touch
|
||||
# at exactly one point. One circle lies inside
|
||||
# the other.
|
||||
# Make a line from the center of the inside circle
|
||||
# to the center of the outside circle, and walk
|
||||
# along it the negative of the small radius.
|
||||
l = Line(start=p0, end=p1)
|
||||
little_r = r0
|
||||
if r0 > r1:
|
||||
l = Line(start=p1, end=p0)
|
||||
little_r = r1
|
||||
p = l.point(-little_r/d)
|
||||
possible_inters.append((self.point_to_t(p), other_seg.point_to_t(p)))
|
||||
|
||||
else:
|
||||
a = (pow(r0, 2.0) - pow(r1, 2.0) + pow(d, 2.0)) / (2.0 * d)
|
||||
h = sqrt(pow(r0, 2.0) - pow(a, 2.0))
|
||||
p2 = p0 + (a * (p1 - p0) / d)
|
||||
|
||||
x30 = p2.real + (h * (p1.imag - p0.imag) / d)
|
||||
x31 = p2.real - (h * (p1.imag - p0.imag) / d)
|
||||
|
||||
y30 = p2.imag - (h * (p1.real - p0.real) / d)
|
||||
y31 = p2.imag + (h * (p1.real - p0.real) / d)
|
||||
|
||||
p30 = complex(x30, y30)
|
||||
p31 = complex(x31, y31)
|
||||
|
||||
possible_inters.append((self.point_to_t(p30), other_seg.point_to_t(p30)))
|
||||
possible_inters.append((self.point_to_t(p31), other_seg.point_to_t(p31)))
|
||||
|
||||
inters = []
|
||||
for p in possible_inters:
|
||||
self_t = p[0]
|
||||
if (self_t is None) or (self_t < 0.0) or (self_t > 1.0): continue
|
||||
other_t = p[1]
|
||||
if (other_t is None) or (other_t < 0.0) or (other_t > 1.0): continue
|
||||
assert(np.isclose(self.point(self_t), other_seg.point(other_t), rtol=0.0, atol=1e-6))
|
||||
i = (self_t, other_t)
|
||||
inters.append(i)
|
||||
|
||||
return inters
|
||||
|
||||
# This could be made explicit to increase efficiency
|
||||
longer_length = max(self.length(), other_seg.length())
|
||||
inters = bezier_intersections(self, other_seg,
|
||||
|
@ -1932,6 +2048,7 @@ class Arc(object):
|
|||
else:
|
||||
return [inters[0], inters[-1]]
|
||||
return inters
|
||||
|
||||
else:
|
||||
raise TypeError("other_seg should be a Arc, Line, "
|
||||
"QuadraticBezier, or CubicBezier object.")
|
||||
|
|
|
@ -1521,6 +1521,108 @@ class Test_intersect(unittest.TestCase):
|
|||
self.assertEqual(i, [])
|
||||
|
||||
|
||||
def test_arc_arc_0(self):
|
||||
# These arcs cross at a single point.
|
||||
a0 = Arc(start=(114.648+27.4280898219j), radius=(22+22j), rotation=0, large_arc=False, sweep=True, end=(118.542+39.925j))
|
||||
a1 = Arc(start=(118.542+15.795j), radius=(22+22j), rotation=0, large_arc=False, sweep=True, end=(96.542+37.795j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 1)
|
||||
|
||||
def test_arc_arc_1(self):
|
||||
# These touch at an endpoint, and are *nearly* segments of a larger arc.
|
||||
a0 = Arc(start=(-12.8272110776+72.6464538932j), radius=(44.029+44.029j), rotation=0.0, large_arc=False, sweep=False, end=(-60.6807543328+75.3104334473j))
|
||||
a1 = Arc(start=(-60.6807101078+75.3104011248j), radius=(44.029+44.029j), rotation=0.0, large_arc=False, sweep=False, end=(-77.7490636234+120.096609353j))
|
||||
intersections = a0.intersect(a1)
|
||||
print("intersections: %s" % intersections)
|
||||
assert_intersections(a0, a1, intersections, 1)
|
||||
|
||||
def test_arc_arc_2(self):
|
||||
# These arcs cross at a single point.
|
||||
a0 = Arc(start=(112.648+5j), radius=(24+24j), rotation=0, large_arc=False, sweep=True, end=(136.648+29j))
|
||||
a1 = Arc(start=(112.648+6.33538520071j), radius=(24+24j), rotation=0, large_arc=False, sweep=True, end=(120.542+5j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 1)
|
||||
|
||||
# The Arcs in this test are part of the same circle.
|
||||
def test_arc_arc_same_circle(self):
|
||||
# These touch at one endpoint, and go in the same direction.
|
||||
a0 = Arc(start=(0+0j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(-10+10j))
|
||||
a1 = Arc(start=(-10+10j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(0+20j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 1)
|
||||
|
||||
# These touch at both endpoints, and go in the same direction.
|
||||
a0 = Arc(start=(0+0j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(-10+10j))
|
||||
a1 = Arc(start=(-10+10j), radius=(10+10j), rotation=0.0, large_arc=True, sweep=False, end=(0+0j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 2)
|
||||
|
||||
# These touch at one endpoint, and go in opposite directions.
|
||||
a0 = Arc(start=(0+0j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(0+20j))
|
||||
a1 = Arc(start=(0+20j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=True, end=(-10+10j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 0)
|
||||
|
||||
# These touch at both endpoints, and go in opposite directions.
|
||||
a0 = Arc(start=(0+0j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(-10+10j))
|
||||
a1 = Arc(start=(-10+10j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=True, end=(0+0j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 0)
|
||||
|
||||
# These are totally disjoint.
|
||||
a0 = Arc(start=(0+0j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(-10+10j))
|
||||
a1 = Arc(start=(0+20j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(10+10j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 0)
|
||||
|
||||
# These overlap at one end and don't touch at the other.
|
||||
a0 = Arc(start=(0+0j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(0+20j))
|
||||
a1 = Arc(start=(-10+10j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(10+10j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 0)
|
||||
|
||||
# These overlap at one end and touch at the other.
|
||||
a0 = Arc(start=(0+0j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(0+20j))
|
||||
a1 = Arc(start=(-10+10j), radius=(10+10j), rotation=0.0, large_arc=True, sweep=False, end=(0+0j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 0)
|
||||
|
||||
# The Arcs in this test are part of tangent circles, outside each other.
|
||||
def test_arc_arc_tangent_circles_outside(self):
|
||||
a0 = Arc(start=(0+0j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(0+20j))
|
||||
a1 = Arc(start=(-20+0j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=True, end=(-20+20j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 1)
|
||||
|
||||
a0 = Arc(start=(0+0j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(0+20j))
|
||||
a1 = Arc(start=(-20+0j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(-20+20j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 0)
|
||||
|
||||
a0 = Arc(start=(10-10j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(10+10j))
|
||||
a1 = Arc(start=(-10-0j), radius=(5+5j), rotation=0.0, large_arc=True, sweep=True, end=(-5+5j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 1)
|
||||
|
||||
# The Arcs in this test are part of tangent circles, one inside the other.
|
||||
def test_arc_arc_tangent_circles_inside(self):
|
||||
a0 = Arc(start=(10-10j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(10+10j))
|
||||
a1 = Arc(start=(10-0j), radius=(5+5j), rotation=0.0, large_arc=True, sweep=True, end=(5+5j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 1)
|
||||
|
||||
a0 = Arc(start=(10-10j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(10+10j))
|
||||
a1 = Arc(start=(10-0j), radius=(5+5j), rotation=0.0, large_arc=True, sweep=False, end=(5+5j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 1)
|
||||
|
||||
a0 = Arc(start=(10-10j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=False, end=(10+10j))
|
||||
a1 = Arc(start=(10-0j), radius=(5+5j), rotation=0.0, large_arc=False, sweep=False, end=(5+5j))
|
||||
intersections = a0.intersect(a1)
|
||||
assert_intersections(a0, a1, intersections, 0)
|
||||
|
||||
|
||||
|
||||
class TestPathTools(unittest.TestCase):
|
||||
# moved from test_pathtools.py
|
||||
|
||||
|
|
Loading…
Reference in New Issue