Skip to content

Commit

Permalink
Fix findIntersection() for overlapping segments. (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
oceanful authored May 10, 2019
1 parent fcff711 commit 81284b8
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 43 deletions.
99 changes: 74 additions & 25 deletions bugs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,31 @@ func dump(poly polyclip.Polygon) string {
return fmt.Sprintf("%v", normalize(poly))
}

type testCase struct {
op polyclip.Op
subject polyclip.Polygon
clipping polyclip.Polygon
result polyclip.Polygon
}

type testCases []testCase

func (cases testCases) verify(t *T) {
t.Helper()
for i, c := range cases {
result := dump(c.subject.Construct(c.op, c.clipping))
if result != dump(c.result) {
t.Errorf("case %d: %v\nsubject: %v\nclipping: %v\nexpected: %v\ngot: %v",
i, c.op, c.subject, c.clipping, c.result, result)
}
}
}

func TestBug3(t *T) {
cases := []struct{ subject, clipping, result polyclip.Polygon }{
testCases{
// original reported github issue #3
{
op: polyclip.UNION,
subject: polyclip.Polygon{{{1, 1}, {1, 2}, {2, 2}, {2, 1}}},
clipping: polyclip.Polygon{
{{2, 1}, {2, 2}, {3, 2}, {3, 1}},
Expand All @@ -74,6 +95,7 @@ func TestBug3(t *T) {
},
// simplified variant of issue #3, for easier debugging
{
op: polyclip.UNION,
subject: polyclip.Polygon{{{1, 2}, {2, 2}, {2, 1}}},
clipping: polyclip.Polygon{
{{2, 1}, {2, 2}, {3, 2}},
Expand All @@ -82,6 +104,7 @@ func TestBug3(t *T) {
result: polyclip.Polygon{{{1, 2}, {2, 3}, {3, 2}, {2, 1}}},
},
{
op: polyclip.UNION,
subject: polyclip.Polygon{{{1, 2}, {2, 2}, {2, 1}}},
clipping: polyclip.Polygon{
{{1, 2}, {2, 3}, {2, 2}},
Expand All @@ -90,12 +113,14 @@ func TestBug3(t *T) {
},
// another variation, now with single degenerated curve
{
op: polyclip.UNION,
subject: polyclip.Polygon{{{1, 2}, {2, 2}, {2, 1}}},
clipping: polyclip.Polygon{
{{1, 2}, {2, 3}, {2, 2}, {2, 3}, {3, 2}}},
result: polyclip.Polygon{{{1, 2}, {2, 3}, {3, 2}, {2, 2}, {2, 1}}},
},
{
op: polyclip.UNION,
subject: polyclip.Polygon{{{1, 2}, {2, 2}, {2, 1}}},
clipping: polyclip.Polygon{
{{2, 1}, {2, 2}, {2, 3}, {3, 2}},
Expand All @@ -104,18 +129,52 @@ func TestBug3(t *T) {
},
// "union" with effectively empty polygon (wholly self-intersecting)
{
op: polyclip.UNION,
subject: polyclip.Polygon{{{1, 2}, {2, 2}, {2, 1}}},
clipping: polyclip.Polygon{{{1, 2}, {2, 2}, {2, 3}, {1, 2}, {2, 2}, {2, 3}}},
result: polyclip.Polygon{{{1, 2}, {2, 2}, {2, 1}}},
},
}
for _, c := range cases {
result := dump(c.subject.Construct(polyclip.UNION, c.clipping))
if result != dump(c.result) {
t.Errorf("case UNION:\nsubject: %v\nclipping: %v\nexpected: %v\ngot: %v",
c.subject, c.clipping, c.result, result)
}
}
}.verify(t)
}

func TestSelfIntersectionAvoidance(t *T) {
testCases{
{
op: polyclip.DIFFERENCE,
subject: polyclip.Polygon{{
{38.5721239031346, 172.33955556881023},
{39.99999999999999, 171.3397459621556},
{41.57979856674331, 170.60307379214092},
{43.2635182233307, 170.15192246987792},
{45, 170},
{46.7364817766693, 170.15192246987792},
{48.42020143325668, 170.60307379214092},
{50, 171.3397459621556},
{51.42787609686539, 172.33955556881023},
}},
clipping: polyclip.Polygon{{
{51.42787609686539, 172.33955556881023},
{50, 171.3397459621556},
{48.42020143325668, 170.60307379214092},
{46.7364817766693, 170.15192246987792},
{45, 170},
{43.2635182233307, 170.15192246987792},
{42.78116786015871, 170.28116786015872},
{42.65192246987792, 170.7635182233307},
{42.5, 172},
}},
result: polyclip.Polygon{{
{51.42787609686539, 172.33955556881023},
{42.5, 172},
{42.65192246987792, 170.7635182233307},
{42.78116786015871, 170.28116786015872},
// Should not contain this point: {43.2635182233307, 170.15192246987792},
{41.57979856674331, 170.60307379214092},
{39.99999999999999, 171.3397459621556},
{38.5721239031346, 172.33955556881023},
}},
},
}.verify(t)
}

func TestNonReductiveSegmentDivisions(t *T) {
Expand Down Expand Up @@ -304,35 +363,25 @@ func TestBug5(t *T) {
rect := polyclip.Polygon{{{24, 7}, {36, 7}, {36, 23}, {24, 23}}}
circle := polyclip.Polygon{{{24, 7}, {24.83622770614123, 7.043824837053814}, {25.66329352654208, 7.174819194129555}, {26.472135954999587, 7.391547869638773}, {27.253893144606412, 7.691636338859195}, {28.00000000000001, 8.071796769724493}, {28.702282018339798, 8.527864045000424}, {29.35304485087088, 9.054841396180851}, {29.94515860381917, 9.646955149129141}, {30.472135954999597, 10.297717981660224}, {30.92820323027553, 11.00000000000001}, {31.308363661140827, 11.746106855393611}, {31.60845213036125, 12.527864045000435}, {31.825180805870467, 13.33670647345794}, {31.95617516294621, 14.16377229385879}, {32.00000000000002, 15.00000000000002}, {31.95617516294621, 15.83622770614125}, {31.825180805870467, 16.6632935265421}, {31.60845213036125, 17.472135954999604}, {31.308363661140827, 18.25389314460643}, {30.92820323027553, 19.00000000000003}, {30.472135954999597, 19.702282018339815}, {29.94515860381917, 20.353044850870898}, {29.35304485087088, 20.945158603819188}, {28.702282018339798, 21.472135954999615}, {28.00000000000001, 21.928203230275546}, {27.253893144606412, 22.308363661140845}, {26.472135954999587, 22.608452130361268}, {25.66329352654208, 22.825180805870485}, {24.83622770614123, 22.956175162946227}, {24, 23.00000000000004}, {23.16377229385877, 22.956175162946227}, {22.33670647345792, 22.825180805870485}, {21.527864045000413, 22.608452130361268}, {20.746106855393588, 22.308363661140845}, {19.99999999999999, 21.928203230275546}, {19.297717981660202, 21.472135954999615}, {18.64695514912912, 20.945158603819188}, {18.05484139618083, 20.353044850870898}, {17.527864045000403, 19.702282018339815}, {17.07179676972447, 19.00000000000003}, {16.691636338859173, 18.25389314460643}, {16.39154786963875, 17.472135954999604}, {16.174819194129533, 16.6632935265421}, {16.04382483705379, 15.83622770614125}, {15.999999999999977, 15.00000000000002}, {16.04382483705379, 14.16377229385879}, {16.174819194129533, 13.33670647345794}, {16.39154786963875, 12.527864045000435}, {16.691636338859173, 11.746106855393611}, {17.07179676972447, 11.00000000000001}, {17.527864045000403, 10.297717981660224}, {18.05484139618083, 9.646955149129141}, {18.64695514912912, 9.054841396180851}, {19.297717981660202, 8.527864045000424}, {19.99999999999999, 8.071796769724493}, {20.746106855393588, 7.691636338859194}, {21.527864045000413, 7.391547869638772}, {22.33670647345792, 7.1748191941295545}, {23.16377229385877, 7.043824837053813}}}

expected := []struct {
op polyclip.Op
result polyclip.Polygon
}{
testCases{
{
polyclip.UNION,
polyclip.UNION, rect, circle,
polyclip.Polygon{{{36, 23}, {36, 7}, {24, 7}, {23.16377229385877, 7.043824837053813}, {22.33670647345792, 7.1748191941295545}, {21.527864045000413, 7.391547869638772}, {20.746106855393588, 7.691636338859194}, {19.99999999999999, 8.071796769724493}, {19.297717981660202, 8.527864045000424}, {18.64695514912912, 9.054841396180851}, {18.05484139618083, 9.646955149129141}, {17.527864045000403, 10.297717981660224}, {17.07179676972447, 11.00000000000001}, {16.691636338859173, 11.746106855393611}, {16.39154786963875, 12.527864045000435}, {16.174819194129533, 13.33670647345794}, {16.04382483705379, 14.16377229385879}, {15.999999999999977, 15.00000000000002}, {16.04382483705379, 15.83622770614125}, {16.174819194129533, 16.6632935265421}, {16.39154786963875, 17.472135954999604}, {16.691636338859173, 18.25389314460643}, {17.07179676972447, 19.00000000000003}, {17.527864045000403, 19.702282018339815}, {18.05484139618083, 20.353044850870898}, {18.64695514912912, 20.945158603819188}, {19.297717981660202, 21.472135954999615}, {19.99999999999999, 21.928203230275546}, {20.746106855393588, 22.308363661140845}, {21.527864045000413, 22.608452130361268}, {22.33670647345792, 22.825180805870485}, {23.16377229385877, 22.956175162946227}, {24, 23.00000000000004}, {24.000000000000746, 23}}},
},
{
polyclip.INTERSECTION,
polyclip.INTERSECTION, rect, circle,
polyclip.Polygon{{{31.95617516294621, 15.83622770614125}, {31.825180805870467, 16.6632935265421}, {31.60845213036125, 17.472135954999604}, {31.308363661140827, 18.25389314460643}, {30.92820323027553, 19.00000000000003}, {30.472135954999597, 19.702282018339815}, {29.94515860381917, 20.353044850870898}, {29.35304485087088, 20.945158603819188}, {28.702282018339798, 21.472135954999615}, {28.00000000000001, 21.928203230275546}, {27.253893144606412, 22.308363661140845}, {26.472135954999587, 22.608452130361268}, {25.66329352654208, 22.825180805870485}, {24.83622770614123, 22.956175162946227}, {24.000000000000746, 23}, {24, 23}, {24, 7}, {24.83622770614123, 7.043824837053814}, {25.66329352654208, 7.174819194129555}, {26.472135954999587, 7.391547869638773}, {27.253893144606412, 7.691636338859195}, {28.00000000000001, 8.071796769724493}, {28.702282018339798, 8.527864045000424}, {29.35304485087088, 9.054841396180851}, {29.94515860381917, 9.646955149129141}, {30.472135954999597, 10.297717981660224}, {30.92820323027553, 11.00000000000001}, {31.308363661140827, 11.746106855393611}, {31.60845213036125, 12.527864045000435}, {31.825180805870467, 13.33670647345794}, {31.95617516294621, 14.16377229385879}, {32.00000000000002, 15.00000000000002}}},
},
{
polyclip.DIFFERENCE,
polyclip.DIFFERENCE, rect, circle,
polyclip.Polygon{{{24.000000000000746, 23}, {24.83622770614123, 22.956175162946227}, {25.66329352654208, 22.825180805870485}, {26.472135954999587, 22.608452130361268}, {27.253893144606412, 22.308363661140845}, {28.00000000000001, 21.928203230275546}, {28.702282018339798, 21.472135954999615}, {29.35304485087088, 20.945158603819188}, {29.94515860381917, 20.353044850870898}, {30.472135954999597, 19.702282018339815}, {30.92820323027553, 19.00000000000003}, {31.308363661140827, 18.25389314460643}, {31.60845213036125, 17.472135954999604}, {31.825180805870467, 16.6632935265421}, {31.95617516294621, 15.83622770614125}, {32.00000000000002, 15.00000000000002}, {31.95617516294621, 14.16377229385879}, {31.825180805870467, 13.33670647345794}, {31.60845213036125, 12.527864045000435}, {31.308363661140827, 11.746106855393611}, {30.92820323027553, 11.00000000000001}, {30.472135954999597, 10.297717981660224}, {29.94515860381917, 9.646955149129141}, {29.35304485087088, 9.054841396180851}, {28.702282018339798, 8.527864045000424}, {28.00000000000001, 8.071796769724493}, {27.253893144606412, 7.691636338859195}, {26.472135954999587, 7.391547869638773}, {25.66329352654208, 7.174819194129555}, {24.83622770614123, 7.043824837053814}, {24, 7}, {36, 7}, {36, 23}}},
},
{
polyclip.XOR,
polyclip.XOR, rect, circle,
polyclip.Polygon{
{{24.000000000000746, 23}, {24, 23}, {24, 7}, {23.16377229385877, 7.043824837053813}, {22.33670647345792, 7.1748191941295545}, {21.527864045000413, 7.391547869638772}, {20.746106855393588, 7.691636338859194}, {19.99999999999999, 8.071796769724493}, {19.297717981660202, 8.527864045000424}, {18.64695514912912, 9.054841396180851}, {18.05484139618083, 9.646955149129141}, {17.527864045000403, 10.297717981660224}, {17.07179676972447, 11.00000000000001}, {16.691636338859173, 11.746106855393611}, {16.39154786963875, 12.527864045000435}, {16.174819194129533, 13.33670647345794}, {16.04382483705379, 14.16377229385879}, {15.999999999999977, 15.00000000000002}, {16.04382483705379, 15.83622770614125}, {16.174819194129533, 16.6632935265421}, {16.39154786963875, 17.472135954999604}, {16.691636338859173, 18.25389314460643}, {17.07179676972447, 19.00000000000003}, {17.527864045000403, 19.702282018339815}, {18.05484139618083, 20.353044850870898}, {18.64695514912912, 20.945158603819188}, {19.297717981660202, 21.472135954999615}, {19.99999999999999, 21.928203230275546}, {20.746106855393588, 22.308363661140845}, {21.527864045000413, 22.608452130361268}, {22.33670647345792, 22.825180805870485}, {23.16377229385877, 22.956175162946227}, {24, 23.00000000000004}},
{{24.000000000000746, 23}, {24.83622770614123, 22.956175162946227}, {25.66329352654208, 22.825180805870485}, {26.472135954999587, 22.608452130361268}, {27.253893144606412, 22.308363661140845}, {28.00000000000001, 21.928203230275546}, {28.702282018339798, 21.472135954999615}, {29.35304485087088, 20.945158603819188}, {29.94515860381917, 20.353044850870898}, {30.472135954999597, 19.702282018339815}, {30.92820323027553, 19.00000000000003}, {31.308363661140827, 18.25389314460643}, {31.60845213036125, 17.472135954999604}, {31.825180805870467, 16.6632935265421}, {31.95617516294621, 15.83622770614125}, {32.00000000000002, 15.00000000000002}, {31.95617516294621, 14.16377229385879}, {31.825180805870467, 13.33670647345794}, {31.60845213036125, 12.527864045000435}, {31.308363661140827, 11.746106855393611}, {30.92820323027553, 11.00000000000001}, {30.472135954999597, 10.297717981660224}, {29.94515860381917, 9.646955149129141}, {29.35304485087088, 9.054841396180851}, {28.702282018339798, 8.527864045000424}, {28.00000000000001, 8.071796769724493}, {27.253893144606412, 7.691636338859195}, {26.472135954999587, 7.391547869638773}, {25.66329352654208, 7.174819194129555}, {24.83622770614123, 7.043824837053814}, {24, 7}, {36, 7}, {36, 23}},
},
},
}

for _, e := range expected {
result := rect.Construct(e.op, circle)
if dump(result) != dump(e.result) {
t.Errorf("case %d expected:\n%v\ngot:\n%v", e.op, dump(e.result), dump(result))
}
}
}.verify(t)
}
21 changes: 13 additions & 8 deletions clipper.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ func (c *clipper) compute(operation Op) Polygon {
return connector.toPolygon()
}

func findIntersection(seg0, seg1 segment) (int, Point, Point) {
func findIntersection(seg0, seg1 segment, tryBothDirections bool) (int, Point, Point) {
var pi0, pi1 Point
p0 := seg0.start
d0 := Point{seg0.end.X - p0.X, seg0.end.Y - p0.Y}
Expand Down Expand Up @@ -332,13 +332,18 @@ func findIntersection(seg0, seg1 segment) (int, Point, Point) {
if imax > 0 {
pi0.X = p0.X + w[0]*d0.X
pi0.Y = p0.Y + w[0]*d0.Y

// [MC: commented fragment removed]

if imax > 1 {
pi1.X = p0.X + w[1]*d0.X
pi1.Y = p0.Y + w[1]*d0.Y
}
// For same-segment scenarios, this should be the case.
if imax > 1 {
pi1.X = p0.X + w[1]*d0.X
pi1.Y = p0.Y + w[1]*d0.Y

} else if tryBothDirections {
// However, findIntersection() is not symmetric and sometimes fails in one direction. Try the other.
if otherImax, otherPi0, otherPi1 := findIntersection(seg1, seg0, false); otherImax > imax {
return otherImax, otherPi0, otherPi1
}
_DBG(func() { fmt.Printf("WARNING: Could not find overlap in segments %v, %v\n", seg0, seg1) })
}

return imax, pi0, pi1
Expand Down Expand Up @@ -378,7 +383,7 @@ func findIntersection2(u0, u1, v0, v1 float64, w *[]float64) int {
func (c *clipper) possibleIntersection(e1, e2 *endpoint) {
// [MC]: commented fragment removed

numIntersections, ip1, _ := findIntersection(e1.segment(), e2.segment())
numIntersections, ip1, _ := findIntersection(e1.segment(), e2.segment(), true)

if numIntersections == 0 {
return
Expand Down
45 changes: 35 additions & 10 deletions intersection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,59 +6,84 @@ func TestFindIntersection(t *testing.T) {
cases := []struct {
s1, s2 segment
numIntersections int
ip1, ip2 Point
ip1 Point
}{
{
// Almost (but not) parallel lines
segment{Point{0, 0}, Point{100, 0.0001}},
segment{Point{1, 0}, Point{100, 0}},
0, Point{}, Point{},
0, Point{},
},
{
// Almost (but not) parallel lines
segment{Point{0, 0}, Point{100, 0.0000001}},
segment{Point{1, 0}, Point{100, 0}},
0, Point{}, Point{},
0, Point{},
},
{
// Cross
segment{Point{1, 0}, Point{1, 3}},
segment{Point{0, 1}, Point{3, 1}},
1, Point{1, 1}, Point{},
1, Point{1, 1},
},
{
// Rays
segment{Point{0, 1}, Point{1, 3}},
segment{Point{0, 1}, Point{3, 1}},
1, Point{0, 1}, Point{},
1, Point{0, 1},
},
{
// Colinear rays
segment{Point{2, 1}, Point{0, 1}},
segment{Point{2, 1}, Point{1, 1}},
2, Point{2, 1}, Point{}, // Why isn't this 2 intersections at {1,1} {2,1}?
2, Point{2, 1},
},
{
// Colinear rays
segment{Point{0, 3}, Point{0, 1}},
segment{Point{0, 3}, Point{0, 2}},
2, Point{0, 3}, Point{}, // Why isn't this 2 intersections at {0,2}, {0,3}?
2, Point{0, 3},
},
{
// Overlapping segments
segment{Point{0, 1}, Point{3, 1}},
segment{Point{1, 1}, Point{2, 1}},
1, Point{3, 1}, Point{}, // Why isn't this 2 intersections at {1,1} {2,1}?
2, Point{1, 1},
},
{
// Overlapping segments
segment{Point{0, 1}, Point{0, 4}},
segment{Point{0, 2}, Point{0, 3}},
1, Point{0, 4}, Point{}, // Why isn't this 2 intersections at {0,2}, {0,3}?
2, Point{0, 2},
},
{ // Overlapping segments
segment{Point{43.2635182233307, 170.15192246987792}, Point{41.57979856674331, 170.60307379214092}},
segment{Point{43.2635182233307, 170.15192246987792}, Point{42.78116786015871, 170.28116786015872}},
2, Point{43.2635182233307, 170.15192246987792},
},
{ // Overlapping segments
segment{Point{41.57979856674331, 170.60307379214092}, Point{43.2635182233307, 170.15192246987792}},
segment{Point{42.78116786015871, 170.28116786015872}, Point{43.2635182233307, 170.15192246987792}},
2, Point{42.78116786015871, 170.28116786015872},
},
{ // Overlapping segments
segment{Point{43.2635182233307, 170.15192246987792}, Point{41.57979856674331, 170.60307379214092}},
segment{Point{42.78116786015871, 170.28116786015872}, Point{43.2635182233307, 170.15192246987792}},
2, Point{43.2635182233307, 170.15192246987792},
},
{ // Overlapping segments
segment{Point{41.57979856674331, 170.60307379214092}, Point{43.2635182233307, 170.15192246987792}},
segment{Point{43.2635182233307, 170.15192246987792}, Point{42.78116786015871, 170.28116786015872}},
2, Point{43.2635182233307, 170.15192246987792},
},
{ // Identical segments
segment{Point{66, 160}, Point{67.1242262770966, 147.15003485264717}},
segment{Point{66, 160}, Point{67.1242262770966, 147.15003485264717}},
2, Point{66, 160},
},
}
for i, v := range cases {
num, ip1, _ := findIntersection(v.s1, v.s2)
num, ip1, _ := findIntersection(v.s1, v.s2, true)
verify(t, num == v.numIntersections, "Case %d: Expected numIntersections to be %d, but got %d", i, v.numIntersections, num)
verify(t, ip1.Equals(v.ip1), "Case %d: Expected ip1 to be %v, but got %v", i, v.ip1, ip1)
}
Expand Down

0 comments on commit 81284b8

Please sign in to comment.