From 584bdbbeb9ee6ca0f2e57daa15281487107b588c Mon Sep 17 00:00:00 2001 From: oceanful <35951082+oceanful@users.noreply.github.com> Date: Fri, 17 May 2019 15:03:11 -0700 Subject: [PATCH] Improve snapping behavior by preferring endpoints in the middle. (#8) Modify the snapping algorithm to: * Avoid snapping if the intersection point is equivalent to an existing endpoint. * Prefer snapping to an endpoint in the middle of the 4 endpoint candidates. * Increase the tolerance to cover more cases. This improves robustness against floating imprecision. --- bugs_test.go | 122 ++++++++++++++++++++++++++++++++++++++++++++++-- clipper.go | 43 ++++++++++++----- clipper_test.go | 71 ++++++++++++++++++++++++++-- endpoint.go | 2 +- geom.go | 6 +++ geom_test.go | 16 +++++++ 6 files changed, 238 insertions(+), 22 deletions(-) diff --git a/bugs_test.go b/bugs_test.go index 4cb3b7a..c5f14c3 100644 --- a/bugs_test.go +++ b/bugs_test.go @@ -358,6 +358,118 @@ func TestCorruptionResistanceFromFloatingPointImprecision(t *T) { {7.604714313123809, 25.720088264944764}, }}, }, + { + op: polyclip.INTERSECTION, + subject: polyclip.Polygon{{ + {300, -100}, + {259.8076211353316, 49.99999999999997}, + {-259.80762113533154, 50.000000000000114}, + {-300, -99.99999999999996}, + }}, + clipping: polyclip.Polygon{{ + {273.2050807568877, 7.815970093361102e-14}, + {259.8076211353315, -50.00000000000014}, + {-259.80762113533166, -49.99999999999994}, + {-273.20508075688775, -7.105427357601002e-14}, + {-259.80762113533154, 50.000000000000114}, + {259.8076211353316, 49.99999999999997}, + }}, + result: polyclip.Polygon{{ + {273.2050807568877, 7.815970093361102e-14}, + {259.8076211353315, -50.00000000000014}, + {-259.80762113533166, -49.99999999999994}, + {-273.20508075688775, -7.105427357601002e-14}, + {-259.80762113533154, 50.000000000000114}, + {259.8076211353316, 49.99999999999997}, + }}, + }, + { + op: polyclip.INTERSECTION, + subject: polyclip.Polygon{{ + {300, -100}, + {277.163859753386, 14.805029709526934}, + {-277.163859753386, 14.805029709526949}, + {-300, -99.99999999999996}, + }}, + clipping: polyclip.Polygon{{ + {280.1087632620342, 4.618527782440651e-14}, + {277.16385975338596, -14.805029709527119}, + {-277.1638597533861, -14.805029709526906}, + {-280.10876326203424, -1.1368683772161603e-13}, + {-277.163859753386, 14.805029709526949}, + {277.163859753386, 14.805029709526934}, + {280.1087632620342, 4.618527782440651e-14}, + }}, + result: polyclip.Polygon{{ + {280.1087632620342, 4.618527782440651e-14}, + {277.16385975338596, -14.805029709527119}, + {-277.1638597533861, -14.805029709526906}, + {-280.10876326203424, -1.1368683772161603e-13}, + {-277.163859753386, 14.805029709526949}, + {277.163859753386, 14.805029709526934}, + {280.1087632620342, 4.618527782440651e-14}, + }}, + }, + { + op: polyclip.INTERSECTION, + subject: polyclip.Polygon{{ + {-196.96155060244163, 65.270364466614}, + {-187.9385241571817, 31.595971334866263}, + {-173.20508075688775, 4.263256414560601e-14}, + {-153.20888862379562, -28.557521937307854}, + {153.2088886237956, -28.55752193730791}, + {173.20508075688767, -8.526512829121202e-14}, + {187.93852415718163, 31.59597133486612}, + {196.96155060244163, 65.27036446661393}, + }}, + clipping: polyclip.Polygon{{ + {196.96155060244163, -65.27036446661393}, + {187.9385241571817, -31.595971334866263}, + {173.20508075688775, -1.4210854715202004e-14}, + {153.20888862379562, 28.557521937307854}, + {-153.2088886237956, 28.557521937307882}, + {-173.20508075688775, -1.4210854715202004e-14}, + {-187.93852415718166, -31.59597133486622}, + {-196.9615506024416, -65.27036446661387}, + }}, + result: polyclip.Polygon{{ + {-173.20508075688775, 4.263256414560601e-14}, + {-153.20888862379562, -28.557521937307854}, + {153.2088886237956, -28.55752193730791}, + {173.20508075688767, -8.526512829121202e-14}, + {173.20508075688775, -1.4210854715202004e-14}, + {153.20888862379562, 28.557521937307854}, + {-153.2088886237956, 28.557521937307882}, + }}, + }, + { + op: polyclip.INTERSECTION, + subject: polyclip.Polygon{{ + {128.55752193730788, 253.20888862379562}, + {100.00000000000003, 273.2050807568877}, + {68.40402866513377, 287.9385241571817}, + {68.40402866513364, -87.93852415718172}, + {100, -73.20508075688772}, + {128.55752193730785, -53.208888623795616}, + }}, + clipping: polyclip.Polygon{{ + {131.59597133486625, 287.9385241571817}, + {100.00000000000004, 273.20508075688775}, + {71.44247806269215, 253.20888862379562}, + {71.44247806269209, -53.20888862379559}, + {99.99999999999991, -73.20508075688767}, + {131.59597133486614, -87.93852415718163}, + }}, + result: polyclip.Polygon{{ + {71.44247806269209, -53.20888862379559}, + {99.99999999999991, -73.20508075688767}, + {100, -73.20508075688772}, + {128.55752193730785, -53.208888623795616}, + {128.55752193730788, 253.20888862379562}, + {100.00000000000003, 273.2050807568877}, + {71.44247806269215, 253.20888862379562}, + }}, + }, }.verify(t) } @@ -590,21 +702,21 @@ func TestBug5(t *T) { testCases{ { 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.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}}}, }, { 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.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, 23.00000000000004}, {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, 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.Polygon{{{24, 23.00000000000004}, {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, 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}}, + {{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, 23.00000000000004}, {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}}, }, }, }.verify(t) diff --git a/clipper.go b/clipper.go index c490d99..0f9410f 100644 --- a/clipper.go +++ b/clipper.go @@ -26,6 +26,7 @@ package polyclip import ( "fmt" "math" + "sort" ) //func _DBG(f func()) { f() } @@ -400,9 +401,23 @@ func findIntersection2(u0, u1, v0, v1 float64, w *[]float64) int { // snaps the [pt] to one of [toPts] if they are equal within a tolerance factor. // If none of the points are within the tolerance, the original pt is returned. -func snap(pt Point, toPts ...Point) Point { - const tolerance = 3e-14 - for _, p := range toPts { +func snap(pt Point, e1, e2 *endpoint) Point { + pts := []Point{e1.p, e2.p, e1.other.p, e2.other.p} + for _, p := range pts { + if pt.Equals(p) { // Prefer strict equality over snapping. + return p + } + } + // Order the points in sweep-line ordering and test the middle points + // first (i.e. 2, 1, 0, 3) to avoid creating invalid divisions when + // two endpoints are within the tolerance. + sort.Slice(pts, func(i, j int) bool { + return pts[i].isBefore(pts[j]) + }) + pts[0], pts[2] = pts[2], pts[0] + + const tolerance = 8e-14 + for _, p := range pts { if pt.equalWithin(p, tolerance) { return p } @@ -414,25 +429,26 @@ func snap(pt Point, toPts ...Point) Point { func (c *clipper) possibleIntersection(e1, e2 *endpoint) []*endpoint { numIntersections, ip1, _ := findIntersection(e1.segment(), e2.segment(), true) - if numIntersections == 0 { + switch { + case numIntersections == 0: return nil + case numIntersections == 1 && (e1.p.Equals(e2.p) || e1.other.p.Equals(e2.other.p)): + return nil // the line segments intersect at an endpoint of both line segments } // Adjust for floating point imprecision when intersections are created at endpoints, which // otherwise has the tendency to corrupt the original polygons with new, almost-parallel segments. - ip1 = snap(ip1, e1.p, e2.p, e1.other.p, e2.other.p) + ip1 = snap(ip1, e1, e2) if numIntersections == 1 { switch { - case e1.p.Equals(e2.p) || e1.other.p.Equals(e2.other.p): - return nil // the line segments intersect at an endpoint of both line segments - case !isValidSingleIntersection(e1, e2, ip1): - _DBG(func() { fmt.Printf("Dropping invalid intersection %v between %v and %v\n", ip1, e1, e2) }) - return nil case e1.p.Equals(ip1) || e1.other.p.Equals(ip1): // e1 divides e2 return []*endpoint{c.divideSegment(e2, ip1)} case e2.p.Equals(ip1) || e2.other.p.Equals(ip1): // e2 divides e1 return []*endpoint{c.divideSegment(e1, ip1)} + case !isValidSingleIntersection(e1, e2, ip1): + _DBG(func() { fmt.Printf("Dropping invalid intersection %v between %v and %v\n", ip1, e1, e2) }) + return nil default: // e1 and e2 divide each other return []*endpoint{ c.divideSegment(e1, ip1), @@ -442,7 +458,12 @@ func (c *clipper) possibleIntersection(e1, e2 *endpoint) []*endpoint { } if numIntersections == 2 && e1.polygonType == e2.polygonType { - return nil // the line segments overlap, but they belong to the same polygon + _DBG(func() { + fmt.Printf("Dropping intersection %v from overlapping edges of the same polygon %v and %v\n", ip1, e1, e2) + }) + // Note: This case is technically not handled by the algorithm. The original C++ code + // outputs: "Sorry, edges of the same polygon overlap" and exits. + return nil } // The line segments overlap diff --git a/clipper_test.go b/clipper_test.go index a1bc4da..3cebd83 100644 --- a/clipper_test.go +++ b/clipper_test.go @@ -3,9 +3,70 @@ package polyclip import "testing" func TestSnap(t *testing.T) { - p := snap(Point{0, 0}, Point{0, 1e-9}, Point{1e-9, 0}, Point{1e-13, 1e-13}) - verify(t, p.Equals(Point{0, 0}), "Expected no snapping but snapped to %v", p) - - p = snap(Point{0, 0}, Point{0, 1e-9}, Point{1e-9, 0}, Point{1e-15, 1e-15}) - verify(t, p.Equals(Point{1e-15, 1e-15}), "Expected snapping to {1e-15, 1e-15}") + cases := []struct { + pt, l1, r1, l2, r2, snap Point + }{ + { + pt: Point{0, 0}, + l1: Point{0, 1e-9}, + r1: Point{1e-9, 0}, + l2: Point{1e-13, 1e-13}, + r2: Point{1e-13, 0}, + snap: Point{0, 0}, + }, + { + pt: Point{0, 0}, + l1: Point{0, 1e-9}, + r1: Point{1e-9, 0}, + l2: Point{1e-15, 1e-15}, + r2: Point{1e-13, 0}, + snap: Point{1e-15, 1e-15}, + }, + { + pt: Point{0, 0}, + l1: Point{1e-15, 1e-15}, + r1: Point{1e-9, 0}, + l2: Point{1e-15, 2e-15}, + r2: Point{1, 0}, + snap: Point{1e-15, 2e-15}, // Should choose l2 instead of l1. + }, + { + pt: Point{0, 0}, + l1: Point{1e-15, 2e-15}, + r1: Point{1e-9, 0}, + l2: Point{1e-15, 1e-15}, + r2: Point{1, 0}, + snap: Point{1e-15, 2e-15}, // Should choose l1 instead of l2. + }, + { + pt: Point{0, 0}, + l1: Point{1e-15, 1e-15}, + r1: Point{1e-9, 0}, + l2: Point{1e-15, 2e-15}, + r2: Point{1, 0}, + snap: Point{1e-15, 2e-15}, // Should choose l2 instead of l1. + }, + { + pt: Point{0, 0}, + l1: Point{-1, 0}, + r1: Point{1e-15, 3e-15}, + l2: Point{-1, -1}, + r2: Point{1e-15, 1e-15}, + snap: Point{1e-15, 1e-15}, // Should choose r1 instead of r2. + }, + { + pt: Point{0, 0}, + l1: Point{-1, 0}, + r1: Point{1e-15, 1e-15}, + l2: Point{-1, -1}, + r2: Point{1e-15, 3e-15}, + snap: Point{1e-15, 1e-15}, // Should choose r2 instead of r1. + }, + } + for i, v := range cases { + e1 := &endpoint{p: v.l1, left: true, other: &endpoint{p: v.r1, left: false}} + e2 := &endpoint{p: v.l2, left: true, other: &endpoint{p: v.r2, left: false}} + p := snap(v.pt, e1, e2) + verify(t, p.Equals(v.snap), "Case %d: Expected snap to return %v but returned %v", i, v.snap, p) + } } diff --git a/endpoint.go b/endpoint.go index ffdd882..1f0918f 100644 --- a/endpoint.go +++ b/endpoint.go @@ -91,7 +91,7 @@ func (se *endpoint) leftRight() (Point, Point) { // Note that segments of zero length have no direction and are thus not considered valid. func (se *endpoint) isValidDirection() bool { lp, rp := se.leftRight() - return lp.X < rp.X || (lp.X == rp.X && lp.Y < rp.Y) + return lp.isBefore(rp) } // Floating point imprecision in findIntersection() can create "non-reductive" diff --git a/geom.go b/geom.go index 6f2f24b..000d1ad 100644 --- a/geom.go +++ b/geom.go @@ -46,6 +46,12 @@ func (p1 Point) equalWithin(p2 Point, tol float64) bool { floats.EqualWithinAbsOrRel(p1.Y, p2.Y, tol, tol) } +// isBefore() returns true if this point is considered "left" of the +// other point according to the semantics of the sweep line. +func (p1 Point) isBefore(p2 Point) bool { + return p1.X < p2.X || p1.X == p2.X && p1.Y < p2.Y +} + // Length returns distance from p to point (0, 0). func (p Point) Length() float64 { return math.Sqrt(p.X*p.X + p.Y*p.Y) diff --git a/geom_test.go b/geom_test.go index b2027cb..3bb4a0d 100644 --- a/geom_test.go +++ b/geom_test.go @@ -50,6 +50,22 @@ func TestEqualWithin(t *T) { verify(t, p.equalWithin(Point{1e-11, 1e-11}, 1e-10), "Expected equal") } +func TestPointIsBefore(t *T) { + cases := []struct { + p1, p2 Point + before bool + }{ + {Point{0, 1}, Point{0, 1}, false}, + {Point{0, 1}, Point{1, 1}, true}, + {Point{0, 1}, Point{-1, 1}, false}, + {Point{0, 1}, Point{0, 2}, true}, + {Point{0, 1}, Point{0, -1}, false}, + } + for i, v := range cases { + verify(t, v.p1.isBefore(v.p2) == v.before, "Expected %v isBefore(%v)=%v (case %d)", v.p1, v.p2, v.before, i) + } +} + func rect(x, y, w, h float64) Rectangle { return Rectangle{Min: Point{x, y}, Max: Point{x + w, y + h}} }