From 9b1ab02ba6fd50109dc3271e1528fc12bb41744f Mon Sep 17 00:00:00 2001 From: Taco de Wolff Date: Sun, 5 Jan 2025 17:40:55 +0100 Subject: [PATCH] Path intersections: fix bugs related to line-line intersection code --- path_intersection_test.go | 20 +++-- path_intersection_util.go | 152 +++++++++++++++++++++++++------------- 2 files changed, 113 insertions(+), 59 deletions(-) diff --git a/path_intersection_test.go b/path_intersection_test.go index b7fcb99..2aa8a63 100644 --- a/path_intersection_test.go +++ b/path_intersection_test.go @@ -166,9 +166,9 @@ func TestIntersectionLineLineBentleyOttmann(t *testing.T) { // tangent {"M2 0L2 3", "M2 2L3 2", []Point{{2.0, 2.0}}}, {"M2 0L2 2", "M2 2L3 2", nil}, - {"L2 2", "M0 4L2 2", []Point{{2.0, 2.0}}}, - {"L10 5", "M0 10L10 5", []Point{{10.0, 5.0}}}, - {"M10 5L20 10", "M10 5L20 0", []Point{{10.0, 5.0}}}, + {"L2 2", "M0 4L2 2", nil}, + {"L10 5", "M0 10L10 5", nil}, + {"M10 5L20 10", "M10 5L20 0", nil}, // parallel {"L2 2", "L2 2", nil}, @@ -198,26 +198,32 @@ func TestIntersectionLineLineBentleyOttmann(t *testing.T) { // bugs {"M21.590990257669734 18.40900974233027L22.651650429449557 17.348349570550447", "M21.23743686707646 18.762563132923542L21.590990257669738 18.409009742330266", - []Point{{21.590990257669738, 18.40900974233027}}, + nil, //[]Point{{21.590990257669738, 18.40900974233027}}, }, // almost colinear {"M-0.1997406229376793 158.88740153238177L-0.1997406229376793 296.9999999925494", "M-0.1997406229376793 158.88740153238177L-0.19974062293732664 158.8874019079834", - []Point{{-0.1997406229376793, 158.88740153238177}}, + nil, }, // #287 {"M-0.1997406229376793 158.88740153238177L-0.1997406229376793 296.9999999925494", "M-0.19999999999964735 20.77454766193328L-0.19974062293732664 158.8874019079834", []Point{{-0.1997406229376793, 158.88740172019808}}, }, // #287 {"M-0.1997406229376793 158.88740153238177L-0.19974062293732664 158.8874019079834", "M-0.19999999999964735 20.77454766193328L-0.19974062293732664 158.8874019079834", - []Point{{-0.19974062293732664, 158.8874019079834}}, + nil, //[]Point{{-0.19974062293732664, 158.8874019079834}}, }, // almost colinear, #287 {"M162.43449681368278 -9.999996185876771L162.43449681368278 -9.99998551284069", "M162.2344968136828 -9.99998551284069L162.43449681368278 -9.999985512840682", nil, }, // almost colinear, #287 {"M0.7814851602550552,0.3987574923859699L0.7814861805182336,0.39875653588924026", "M0.7814852358775772,0.39875773815916654L0.7814861805182336,0.39875653588924026", - []Point{{0.7814861805182336, 0.39875653588924026}}, + nil, //[]Point{{0.7814861805182336, 0.39875653588924026}}, }, // almost colinear {"M0.6187340865555582,7.030136875251485L0.6203785454666688,7.0296030922171955", "M0.6187340865555582,7.030136875251485L0.6189178552813777,7.03007722485377", nil, }, // colinear + {"M3.937495009359532,6.968745009359532L3.937495009359532,6.968745009361169", "M3.93359874,6.96874501L3.937504990640468,6.968745009359532", + nil, + }, + {"M0.7099618031467739,7.781251248018358L0.7099618032015841,7.781251247660366", "M0.70995969,7.78125125L0.709962185160117,7.781251247660117", + []Point{{0.7099618031467739, 7.781251248018357}}, // weird! + }, } for _, tt := range tts { t.Run(fmt.Sprint(tt.line1, "x", tt.line2), func(t *testing.T) { diff --git a/path_intersection_util.go b/path_intersection_util.go index d333218..cd52f49 100644 --- a/path_intersection_util.go +++ b/path_intersection_util.go @@ -159,6 +159,38 @@ func (zs Intersections) add(pos Point, ta, tb, dira, dirb float64, tangent, same return append(zs, Intersection{pos, [2]float64{ta, tb}, [2]float64{dira, dirb}, tangent, same}) } +func correctIntersection(z, aMin, aMax, bMin, bMax Point) Point { + if z.X < aMin.X { + //fmt.Println("CORRECT 1:", a0, a1, "--", b0, b1) + z.X = aMin.X + } else if aMax.X < z.X { + //fmt.Println("CORRECT 2:", a0, a1, "--", b0, b1) + z.X = aMax.X + } + if z.X < bMin.X { + //fmt.Println("CORRECT 3:", a0, a1, "--", b0, b1) + z.X = bMin.X + } else if bMax.X < z.X { + //fmt.Println("CORRECT 4:", a0, a1, "--", b0, b1) + z.X = bMax.X + } + if z.Y < aMin.Y { + //fmt.Println("CORRECT 5:", a0, a1, "--", b0, b1) + z.Y = aMin.Y + } else if aMax.Y < z.Y { + //fmt.Println("CORRECT 6:", a0, a1, "--", b0, b1) + z.Y = aMax.Y + } + if z.Y < bMin.Y { + //fmt.Println("CORRECT 7:", a0, a1, "--", b0, b1) + z.Y = bMin.Y + } else if bMax.Y < z.Y { + //fmt.Println("CORRECT 8:", a0, a1, "--", b0, b1) + z.Y = bMax.Y + } + return z +} + // F. Antonio, "Faster Line Segment Intersection", Graphics Gems III, 1992 func intersectionLineLineBentleyOttmann(zs []Point, a0, a1, b0, b1 Point) []Point { // fast line-line intersection code, with additional constraints for the BentleyOttmann code: @@ -169,11 +201,17 @@ func intersectionLineLineBentleyOttmann(zs []Point, a0, a1, b0, b1 Point) []Poin if a1.X < b0.X || b1.X < a0.X { return zs } - aMinY, aMaxY := math.Min(a0.Y, a1.Y), math.Max(a0.Y, a1.Y) - bMinY, bMaxY := math.Min(b0.Y, b1.Y), math.Max(b0.Y, b1.Y) - if aMaxY < bMinY || bMaxY < aMinY { + + aMin, aMax, bMin, bMax := a0, a1, b0, b1 + if a1.Y < a0.Y { + aMin.Y, aMax.Y = aMax.Y, aMin.Y + } + if b1.Y < b0.Y { + bMin.Y, bMax.Y = bMax.Y, bMin.Y + } + if aMax.Y < bMin.Y || bMax.Y < aMin.Y { return zs - } else if (a1.X == b0.X || b1.X == a0.X) && (aMaxY == bMinY || bMaxY == aMinY) { + } else if (aMax.X == bMin.X || bMax.X == aMin.X) && (aMax.Y == bMin.Y || bMax.Y == aMin.Y) { return zs } @@ -205,60 +243,70 @@ func intersectionLineLineBentleyOttmann(zs []Point, a0, a1, b0, b1 Point) []Poin } } } - } else { - anum := C.PerpDot(B) - if 0.0 < denom { - if anum < Epsilon || denom+Epsilon < anum { - return zs - } - } else if anum < denom-Epsilon || Epsilon < anum { + return zs + } + + anum := C.PerpDot(B) + if 0.0 < denom { + if anum < 0.0 || denom < anum { return zs } + } else if anum < denom || 0.0 < anum { + return zs + } - bnum := A.PerpDot(C) - if 0.0 < denom { - if bnum < Epsilon || denom+Epsilon < bnum { - return zs - } - } else if bnum < denom-Epsilon || Epsilon < bnum { + bnum := A.PerpDot(C) + if 0.0 < denom { + if bnum < 0.0 || denom < bnum { return zs } + } else if bnum < denom || 0.0 < bnum { + return zs + } - // ta is snapped to 0.0 or 1.0 if very close - ta := anum / denom // in [0,1] - if Equal(ta, 0.0) || ta < 0.0 { - ta = 0.0 - } else if Equal(ta, 1.0) || 1.0 < ta { - ta = 1.0 - } - z := a0.Interpolate(a1, ta) - zs = append(zs, z) - } - - // correct for numerical errors, make sure that the intersection is within the limits of - // each segment, and also that all split segments (ie. (a0,z), (z,a1), (b0,z), (z,b1)) are - // increasing (ie. go left-to-right or if vertical bottom-to-top) - for i := range zs { - z := &zs[i] - if z.X < a0.X { - z.X = a0.X - } else if a1.X < z.X { - z.X = a1.X - } - if z.X < b0.X { - z.X = b0.X - } else if b1.X < z.X { - z.X = b1.X - } - if z.Y < aMinY { - z.Y = aMinY - } else if aMaxY < z.Y { - z.Y = aMaxY - } - if z.Y < bMinY { - z.Y = bMinY - } else if bMaxY < z.Y { - z.Y = bMaxY + // ta is snapped to 0.0 or 1.0 if very close + ta := anum / denom // in [0,1] + if Equal(ta, 0.0) { + ta = 0.0 + } else if Equal(ta, 1.0) { + ta = 1.0 + } + + z := a0.Interpolate(a1, ta) + z = correctIntersection(z, aMin, aMax, bMin, bMax) + if z != a0 && z != a1 || z != b0 && z != b1 { + // not at endpoints for both + if a0 != b0 && z != a0 && z != b0 && b0.Sub(z).PerpDot(z.Sub(a0)) == 0.0 { + a, c, m := a0.X, b0.X, z.X + if math.Abs(z.Sub(a0).X) < math.Abs(z.Sub(a0).Y) { + // mostly vertical + a, c, m = a0.Y, b0.Y, z.Y + } + + if a != c && (a < m) == (c < m) { + if a < m && a < c || m < a && c < a { + zs = append(zs, b0) + } else { + zs = append(zs, a0) + } + } + zs = append(zs, z) + } else if a1 != b1 && z != a1 && z != b1 && z.Sub(b1).PerpDot(a1.Sub(z)) == 0.0 { + b, d, m := a1.X, b1.X, z.X + if math.Abs(z.Sub(a1).X) < math.Abs(z.Sub(a1).Y) { + // mostly vertical + b, d, m = a1.Y, b1.Y, z.Y + } + + if b != d && (b < m) == (d < m) { + if b < m && b < d || m < b && d < b { + zs = append(zs, b1) + } else { + zs = append(zs, a1) + } + } + } else { + zs = append(zs, z) } } return zs