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
Sebastian Kuzminsky 2020-06-19 20:40:38 -06:00 committed by GitHub
parent 5ae88df6d5
commit 685f9a6eaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 219 additions and 0 deletions

View File

@ -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.")

View File

@ -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