github.com/richardwilkes/toolbox@v1.121.0/xmath/geom/poly/polygon_test.go (about)

     1  // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the Mozilla Public
     4  // License, version 2.0. If a copy of the MPL was not distributed with
     5  // this file, You can obtain one at http://mozilla.org/MPL/2.0/.
     6  //
     7  // This Source Code Form is "Incompatible With Secondary Licenses", as
     8  // defined by the Mozilla Public License, version 2.0.
     9  
    10  package poly_test
    11  
    12  import (
    13  	"testing"
    14  
    15  	"github.com/richardwilkes/toolbox/check"
    16  	"github.com/richardwilkes/toolbox/xmath/geom"
    17  	"github.com/richardwilkes/toolbox/xmath/geom/poly"
    18  	"golang.org/x/exp/constraints"
    19  )
    20  
    21  const floatsAreConsistentAcrossPlatforms = false
    22  
    23  type containsCase[T constraints.Float] struct {
    24  	geom.Point[T]
    25  	contains bool
    26  }
    27  
    28  func TestContains(t *testing.T) {
    29  	testContains[float32](t)
    30  	testContains[float64](t)
    31  }
    32  
    33  func testContains[T constraints.Float](t *testing.T) {
    34  	p := poly.Polygon[T]{
    35  		{{200, 20}, {300, 20}, {300, 120}, {200, 120}},
    36  		{{250, 50}, {280, 50}, {280, 80}, {250, 80}},
    37  		{{260, 60}, {290, 60}, {290, 90}, {260, 90}},
    38  		{{290, 110}, {320, 110}, {320, 140}, {290, 140}},
    39  	}
    40  	containsTests := []containsCase[T]{
    41  		{Point: geom.Point[T]{X: 199, Y: 20}, contains: false},
    42  		{Point: geom.Point[T]{X: 200, Y: 20}, contains: true},
    43  		{Point: geom.Point[T]{X: 300, Y: 120}, contains: true},
    44  		{Point: geom.Point[T]{X: 250, Y: 50}, contains: true},
    45  		{Point: geom.Point[T]{X: 260, Y: 60}, contains: true},
    46  		{Point: geom.Point[T]{X: 290, Y: 110}, contains: true},
    47  		{Point: geom.Point[T]{X: 319, Y: 139}, contains: true},
    48  		{Point: geom.Point[T]{X: 321, Y: 140}, contains: false},
    49  		{Point: geom.Point[T]{X: 320, Y: 141}, contains: false},
    50  	}
    51  	for i, test := range containsTests {
    52  		check.Equal(t, test.contains, p.Contains(test.Point), "%T contains case #%d", p, i)
    53  	}
    54  	containsEvenOddTests := []containsCase[T]{
    55  		{Point: geom.Point[T]{X: 199, Y: 20}, contains: false},
    56  		{Point: geom.Point[T]{X: 200, Y: 20}, contains: true},
    57  		{Point: geom.Point[T]{X: 300, Y: 120}, contains: true},
    58  		{Point: geom.Point[T]{X: 250, Y: 50}, contains: false},
    59  		{Point: geom.Point[T]{X: 260, Y: 60}, contains: true},
    60  		{Point: geom.Point[T]{X: 290, Y: 110}, contains: false},
    61  		{Point: geom.Point[T]{X: 319, Y: 139}, contains: true},
    62  		{Point: geom.Point[T]{X: 321, Y: 140}, contains: false},
    63  		{Point: geom.Point[T]{X: 320, Y: 141}, contains: false},
    64  	}
    65  	for i, test := range containsEvenOddTests {
    66  		check.Equal(t, test.contains, p.ContainsEvenOdd(test.Point), "%T contains even/odd case #%d", p, i)
    67  	}
    68  }
    69  
    70  type testCase[T constraints.Float] struct {
    71  	name     string
    72  	subject  poly.Polygon[T]
    73  	clipping poly.Polygon[T]
    74  	expected poly.Polygon[T]
    75  }
    76  
    77  func TestUnion(t *testing.T) {
    78  	testUnion[float32](t)
    79  	if floatsAreConsistentAcrossPlatforms {
    80  		testUnion[float64](t)
    81  	}
    82  }
    83  
    84  func testUnion[T constraints.Float](t *testing.T) {
    85  	tests := []testCase[T]{
    86  		{
    87  			name:    "union #1",
    88  			subject: poly.Polygon[T]{{{1, 1}, {1, 2}, {2, 2}, {2, 1}}},
    89  			clipping: poly.Polygon[T]{
    90  				{{2, 1}, {2, 2}, {3, 2}, {3, 1}},
    91  				{{1, 2}, {1, 3}, {2, 3}, {2, 2}},
    92  				{{2, 2}, {2, 3}, {3, 3}, {3, 2}},
    93  			},
    94  			expected: poly.Polygon[T]{{{3, 2}, {3, 1}, {1, 1}, {1, 2}, {1, 3}, {3, 3}}},
    95  		},
    96  		{
    97  			name:     "union #2",
    98  			subject:  poly.Polygon[T]{{{2, 1}, {1, 2}, {2, 2}}},
    99  			clipping: poly.Polygon[T]{{{1, 2}, {2, 2}, {2, 3}, {1, 2}, {2, 2}, {2, 3}}},
   100  			expected: poly.Polygon[T]{{{2, 1}, {1, 2}, {2, 2}}},
   101  		},
   102  		{
   103  			name:    "union #3",
   104  			subject: poly.Polygon[T]{{{1, 2}, {2, 2}, {2, 1}}},
   105  			clipping: poly.Polygon[T]{
   106  				{{2, 1}, {2, 2}, {3, 2}},
   107  				{{1, 2}, {2, 3}, {2, 2}},
   108  				{{2, 2}, {2, 3}, {3, 2}},
   109  			},
   110  			expected: poly.Polygon[T]{{{3, 2}, {2, 1}, {1, 2}, {2, 3}}},
   111  		},
   112  		{
   113  			name:    "union #4",
   114  			subject: poly.Polygon[T]{{{1, 2}, {2, 2}, {2, 1}}},
   115  			clipping: poly.Polygon[T]{
   116  				{{1, 2}, {2, 3}, {2, 2}},
   117  				{{2, 2}, {2, 3}, {3, 2}},
   118  			},
   119  			expected: poly.Polygon[T]{{{3, 2}, {2, 2}, {2, 1}, {1, 2}, {2, 3}}},
   120  		},
   121  		{
   122  			name:     "union #5",
   123  			subject:  poly.Polygon[T]{{{1, 2}, {2, 2}, {2, 1}}},
   124  			clipping: poly.Polygon[T]{{{1, 2}, {2, 3}, {2, 2}, {2, 3}, {3, 2}}},
   125  			expected: poly.Polygon[T]{{{3, 2}, {2, 2}, {2, 1}, {1, 2}, {2, 3}}},
   126  		},
   127  		{
   128  			name:    "union #6",
   129  			subject: poly.Polygon[T]{{{1, 2}, {2, 2}, {2, 1}}},
   130  			clipping: poly.Polygon[T]{
   131  				{{2, 1}, {2, 2}, {2, 3}, {3, 2}},
   132  				{{1, 2}, {2, 3}, {2, 2}},
   133  			},
   134  			expected: poly.Polygon[T]{{{3, 2}, {2, 1}, {1, 2}, {2, 3}}},
   135  		},
   136  		{
   137  			name:     "union #7",
   138  			subject:  poly.Polygon[T]{{{1, 2}, {2, 2}, {2, 1}}},
   139  			clipping: poly.Polygon[T]{{{1, 2}, {2, 2}, {2, 3}, {1, 2}, {2, 2}, {2, 3}}},
   140  			expected: poly.Polygon[T]{{{2, 1}, {1, 2}, {2, 2}}},
   141  		},
   142  		{
   143  			name:     "union #8",
   144  			subject:  poly.Polygon[T]{{{160.094495, 200.379924}, {90.094495, 200.379924}, {55.094495, 139.758145}, {90.094495, 79.136367}}},
   145  			clipping: poly.Polygon[T]{{{82.846611, 131.518814}, {66.592063, 159.672517}, {90.094495, 200.379924}, {160.094495, 200.379924}}},
   146  			expected: poly.Polygon[T]{{{90.0945, 79.13637}, {55.094494, 139.75815}, {66.592064, 159.67252}, {90.0945, 200.37993}, {160.0945, 200.37993}}},
   147  		},
   148  		{
   149  			name:     "union #9",
   150  			subject:  poly.Polygon[T]{{{70.784326, -7.668842}, {42.500055, -19.384571}, {22.504998, -11.102347}, {14.215784, -7.668842}, {2.500055, 20.615429}, {4.16327, 24.630785}, {-16.38653, 33.14279}, {-28.102259, 61.427062}}},
   151  			clipping: poly.Polygon[T]{{{22.504998, -11.102347}, {14.215784, -7.668842}, {2.500055, 20.615429}, {4.16327, 24.630785}, {-16.38653, 33.14279}, {-18.453791, 38.1336}, {-23.270337, 26.505431}, {16.729663, -13.494569}, {45.013935, -1.778841}, {22.504998, -11.102347}}},
   152  			expected: poly.Polygon[T]{{{70.784325, -7.668842}, {42.500053, -19.384571}, {22.504997, -11.102347}, {14.215784, -7.668842}, {22.504997, -11.102347}, {16.729664, -13.494569}, {-23.270336, 26.50543}, {-18.45379, 38.1336}, {-28.102259, 61.427063}}},
   153  		},
   154  		{
   155  			name:     "union #10",
   156  			subject:  poly.Polygon[T]{{{24, 7}, {36, 7}, {36, 23}, {24, 23}}},
   157  			clipping: poly.Polygon[T]{{{24, 7}, {24.836228, 7.043825}, {25.663294, 7.174819}, {26.472136, 7.391548}, {27.253893, 7.691636}, {28, 8.071797}, {28.702282, 8.527864}, {29.353045, 9.054841}, {29.945159, 9.646955}, {30.472136, 10.297718}, {30.928203, 11}, {31.308364, 11.746107}, {31.608452, 12.527864}, {31.825181, 13.336706}, {31.956175, 14.163772}, {32, 15}, {31.956175, 15.836228}, {31.825181, 16.663294}, {31.608452, 17.472136}, {31.308364, 18.253893}, {30.928203, 19}, {30.472136, 19.702282}, {29.945159, 20.353045}, {29.353045, 20.945159}, {28.702282, 21.472136}, {28, 21.928203}, {27.253893, 22.308364}, {26.472136, 22.608452}, {25.663294, 22.825181}, {24.836228, 22.956175}, {24, 23}, {23.163772, 22.956175}, {22.336706, 22.825181}, {21.527864, 22.608452}, {20.746107, 22.308364}, {20, 21.928203}, {19.297718, 21.472136}, {18.646955, 20.945159}, {18.054841, 20.353045}, {17.527864, 19.702282}, {17.071797, 19}, {16.691636, 18.253893}, {16.391548, 17.472136}, {16.174819, 16.663294}, {16.043825, 15.836228}, {16, 15}, {16.043825, 14.163772}, {16.174819, 13.336706}, {16.391548, 12.527864}, {16.691636, 11.746107}, {17.071797, 11}, {17.527864, 10.297718}, {18.054841, 9.646955}, {18.646955, 9.054841}, {19.297718, 8.527864}, {20, 8.071797}, {20.746107, 7.691636}, {21.527864, 7.391548}, {22.336706, 7.174819}, {23.163772, 7.043825}}},
   158  			expected: poly.Polygon[T]{{{36, 7}, {24, 7}, {23.163772, 7.043825}, {22.336706, 7.174819}, {21.527864, 7.391548}, {20.746107, 7.691636}, {20, 8.071797}, {19.297718, 8.527864}, {18.646955, 9.054841}, {18.054841, 9.646955}, {17.527864, 10.297718}, {17.071797, 11}, {16.691636, 11.746107}, {16.391548, 12.527864}, {16.174819, 13.336706}, {16.043825, 14.163772}, {16, 15}, {16.043825, 15.836228}, {16.174819, 16.663294}, {16.391548, 17.472136}, {16.691636, 18.253893}, {17.071797, 19}, {17.527864, 19.702282}, {18.054841, 20.353045}, {18.646955, 20.945159}, {19.297718, 21.472136}, {20, 21.928203}, {20.746107, 22.308364}, {21.527864, 22.608452}, {22.336706, 22.825181}, {23.163772, 22.956175}, {24, 23}, {36, 23}}},
   159  		},
   160  		{
   161  			name:     "union #11",
   162  			subject:  poly.Polygon[T]{{{160.094495, 200.379924}, {90.094495, 200.379924}, {55.094495, 139.758146}, {90.094495, 79.136368}}},
   163  			clipping: poly.Polygon[T]{{{82.846611, 131.518814}, {66.592063, 159.672518}, {90.094495, 200.379924}, {160.094495, 200.379924}}},
   164  			expected: poly.Polygon[T]{{{90.0945, 79.13637}, {55.094494, 139.75815}, {66.592064, 159.67252}, {90.0945, 200.37993}, {160.0945, 200.37993}}},
   165  		},
   166  		{
   167  			name:     "union #12",
   168  			subject:  poly.Polygon[T]{{{2.500055, 20.615429}, {42.500055, -19.384571}, {82.500055, 20.615429}, {42.500055, 60.615429}}},
   169  			clipping: poly.Polygon[T]{{{7.604714, 25.720088}, {32.55986, 50.675234}, {36.852887, 46.382207}, {11.897741, 21.427062}}},
   170  			expected: poly.Polygon[T]{{{82.50005, 20.615429}, {42.500053, -19.384571}, {2.500055, 20.615429}, {7.6047134, 25.720087}, {7.604714, 25.720087}, {32.55986, 50.675236}, {42.500053, 60.61543}}},
   171  		},
   172  		{
   173  			name:     "union overlapping segments #1",
   174  			subject:  poly.Polygon[T]{{{0, 0}, {10, 0}, {0, 10}}},
   175  			clipping: poly.Polygon[T]{{{6, 5}, {6, 4}, {5, 5}}},
   176  			expected: poly.Polygon[T]{{{5, 5}, {6, 5}, {6, 4}, {10, 0}, {0, 0}, {0, 10}}},
   177  		},
   178  		{
   179  			name:     "union overlapping segments #2",
   180  			subject:  poly.Polygon[T]{{{0, 0}, {10, 0}, {0, 10}}},
   181  			clipping: poly.Polygon[T]{{{6, 4}, {4, 5}, {5, 5}}},
   182  			expected: poly.Polygon[T]{{{5, 5}, {6, 4}, {10, 0}, {0, 0}, {0, 10}}},
   183  		},
   184  		{
   185  			name:     "union overlapping segments #3",
   186  			subject:  poly.Polygon[T]{{{0, 10}, {10, 10}, {0, 0}}},
   187  			clipping: poly.Polygon[T]{{{6, 5}, {6, 6}, {5, 5}}},
   188  			expected: poly.Polygon[T]{{{6, 6}, {6, 5}, {5, 5}, {0, 0}, {0, 10}, {10, 10}}},
   189  		},
   190  		{
   191  			name:     "union overlapping segments #4",
   192  			subject:  poly.Polygon[T]{{{0, 10}, {10, 10}, {0, 0}}},
   193  			clipping: poly.Polygon[T]{{{5, 5}, {5, 6}, {6, 6}}},
   194  			expected: poly.Polygon[T]{{{6, 6}, {5, 5}, {0, 0}, {0, 10}, {10, 10}}},
   195  		},
   196  		{
   197  			name:     "union overlapping segments #5",
   198  			subject:  poly.Polygon[T]{{{40131, 8372}, {40127, 8374}, {40126, 8375}, {40125, 8375}, {40122, 8377}, {40143, 8369}}},
   199  			clipping: poly.Polygon[T]{{{40106, 8376}, {40128, 8373}, {40126, 8375}}},
   200  			expected: poly.Polygon[T]{{{40143, 8369}, {40131, 8372}, {40127, 8374}, {40128, 8373}, {40106, 8376}, {40124.918919, 8375.054054}, {40122, 8377}}},
   201  		},
   202  		{
   203  			name: "union shared vertex #1",
   204  			subject: poly.Polygon[T]{
   205  				{{0, 6}, {5, 5}, {2, 10}},
   206  				{{4, 0}, {10, 4}, {5, 5}},
   207  			},
   208  			clipping: poly.Polygon[T]{{{5, 5}, {10, 8}, {8, 10}}},
   209  			expected: poly.Polygon[T]{
   210  				{{10, 8}, {5, 5}, {8, 10}},
   211  				{{5, 5}, {0, 6}, {2, 10}},
   212  				{{10, 4}, {4, 0}, {5, 5}},
   213  			},
   214  		},
   215  		{
   216  			name: "union shared vertex #2",
   217  			subject: poly.Polygon[T]{
   218  				{{0, 6}, {5, 5}, {5, 10}},
   219  				{{5, 0}, {10, 4}, {5, 5}},
   220  			},
   221  			clipping: poly.Polygon[T]{{{5, 5}, {10, 8}, {8, 10}}},
   222  			expected: poly.Polygon[T]{
   223  				{{10, 8}, {5, 5}, {8, 10}},
   224  				{{5, 5}, {10, 4}, {5, 0}, {5, 5}, {0, 6}, {5, 10}},
   225  			},
   226  		},
   227  		{
   228  			name: "union shared vertex #3",
   229  			subject: poly.Polygon[T]{
   230  				{{0, 6}, {5, 5}, {2, 10}},
   231  				{{6, 0}, {10, 4}, {5, 5}},
   232  			},
   233  			clipping: poly.Polygon[T]{{{5, 5}, {10, 8}, {8, 10}}},
   234  			expected: poly.Polygon[T]{
   235  				{{10, 8}, {5, 5}, {8, 10}},
   236  				{{5, 5}, {0, 6}, {2, 10}},
   237  				{{10, 4}, {6, 0}, {5, 5}},
   238  			},
   239  		},
   240  		{
   241  			name: "union self-intersection",
   242  			subject: poly.Polygon[T]{
   243  				{{0, 0}, {1, 0}, {1, 1}, {0, 1}},
   244  				{{1, 0}, {2, 0}, {2, 1}, {1, 1}},
   245  			},
   246  			clipping: poly.Polygon[T]{{{0, 0.25}, {3, 0.25}, {3, 0.75}, {0, 0.75}}},
   247  			expected: poly.Polygon[T]{{{2, 0.75}, {3, 0.75}, {3, 0.25}, {2, 0.25}, {2, 0}, {0, 0}, {0, 0.25}, {0, 0.75}, {0, 1}, {2, 1}}},
   248  		},
   249  	}
   250  	for i, test := range tests {
   251  		result := test.subject.Union(test.clipping)
   252  		check.True(t, matchPolys(test.expected, result), "%T test case %d (%s): %v", test.subject, i, test.name, result)
   253  	}
   254  }
   255  
   256  func TestIntersect(t *testing.T) {
   257  	testIntersect[float32](t)
   258  	if floatsAreConsistentAcrossPlatforms {
   259  		testIntersect[float64](t)
   260  	}
   261  }
   262  
   263  func testIntersect[T constraints.Float](t *testing.T) {
   264  	tests := []testCase[T]{
   265  		{
   266  			name:     "intersect #1",
   267  			subject:  poly.Polygon[T]{{{160.094495, 200.379924}, {90.094495, 200.379924}, {55.094495, 139.758146}, {90.094495, 79.136368}}},
   268  			clipping: poly.Polygon[T]{{{82.846611, 131.518814}, {66.592063, 159.672518}, {90.094495, 200.379924}, {160.094495, 200.379924}}},
   269  			expected: poly.Polygon[T]{{{82.846611, 131.518814}, {66.592063, 159.672518}, {90.094495, 200.379924}, {160.094495, 200.379924}}},
   270  		},
   271  		{
   272  			name:     "intersect #2",
   273  			subject:  poly.Polygon[T]{{{131.595971, 287.938524}, {100, 273.205081}, {71.442478, 253.208889}, {71.442478, -53.208889}, {100, -73.205081}, {131.595971, -87.938524}}},
   274  			clipping: poly.Polygon[T]{{{128.557522, -53.208889}, {100, -73.205081}, {100, -73.205081}, {71.442478, -53.208889}, {71.442478, 253.208889}, {100, 273.205081}, {128.557522, 253.208889}}},
   275  			expected: poly.Polygon[T]{{{128.557522, 253.208889}, {128.557522, -53.208889}, {100, -73.205081}, {71.442478, -53.208889}, {71.442478, 253.208889}, {100, 273.205081}}},
   276  		},
   277  		{
   278  			name:     "intersect #3",
   279  			subject:  poly.Polygon[T]{{{100, 100}, {98.480775, 117.364818}, {98.480775, 82.635182}}},
   280  			clipping: poly.Polygon[T]{{{100, 100}, {100, 99.999999}, {100, 100}, {100, 100.000001}, {100, 100}}},
   281  			expected: poly.Polygon[T]{},
   282  		},
   283  		{
   284  			name:     "intersect #4",
   285  			subject:  poly.Polygon[T]{{{100, 100}, {98.480775, 117.364818}, {98.480775, 82.635182}}},
   286  			clipping: poly.Polygon[T]{{{100, 100}, {100, 100}, {100, 100}, {100, 100}, {100, 100}}},
   287  			expected: poly.Polygon[T]{},
   288  		},
   289  		{
   290  			name:     "intersect #5",
   291  			subject:  poly.Polygon[T]{{{2.500055, 20.615429}, {42.500055, -19.384571}, {82.500055, 20.615429}, {42.500055, 60.615429}}},
   292  			clipping: poly.Polygon[T]{{{7.604714, 25.720088}, {11.897741, 21.427062}, {36.852887, 46.382207}, {32.55986, 50.675234}}},
   293  			expected: poly.Polygon[T]{{{36.852887, 46.382207}, {11.897741, 21.427062}, {7.604714, 25.720088}, {32.55986, 50.675234}}},
   294  		},
   295  		{
   296  			name:     "intersect #6",
   297  			subject:  poly.Polygon[T]{{{300, -100}, {259.807621, 50}, {-259.807621, 50}, {-300, -100}}},
   298  			clipping: poly.Polygon[T]{{{273.205081, 0}, {259.807621, -50}, {-259.807621, -50}, {-273.205081, -0}, {-259.807621, 50}, {259.807621, 50}}},
   299  			expected: poly.Polygon[T]{{{273.205081, -0.000001}, {259.807621, -50}, {-259.807621, -50}, {-273.205081, -0.000001}, {-259.807621, 50}, {259.807621, 50}}},
   300  		},
   301  		{
   302  			name:     "intersect #7",
   303  			subject:  poly.Polygon[T]{{{300, -100}, {277.16386, 14.80503}, {-277.16386, 14.80503}, {-300, -100}}},
   304  			clipping: poly.Polygon[T]{{{280.108763, 0}, {277.16386, -14.80503}, {-277.16386, -14.80503}, {-280.108763, -0}, {-277.16386, 14.80503}, {277.16386, 14.80503}, {280.108763, 0}}},
   305  			expected: poly.Polygon[T]{{{280.108763, 0}, {277.16386, -14.80503}, {-277.16386, -14.80503}, {-280.108763, -0}, {-277.16386, 14.80503}, {277.16386, 14.80503}}},
   306  		},
   307  		{
   308  			name:     "intersect #8",
   309  			subject:  poly.Polygon[T]{{{-196.961551, 65.270364}, {-187.938524, 31.595971}, {-173.205081, 0}, {-153.208889, -28.557522}, {153.208889, -28.557522}, {173.205081, -0}, {187.938524, 31.595971}, {196.961551, 65.270364}}},
   310  			clipping: poly.Polygon[T]{{{196.961551, -65.270364}, {187.938524, -31.595971}, {173.205081, -0}, {153.208889, 28.557522}, {-153.208889, 28.557522}, {-173.205081, -0}, {-187.938524, -31.595971}, {-196.961551, -65.270364}}},
   311  			expected: poly.Polygon[T]{{{173.205081, 0}, {153.208889, -28.557522}, {-153.208889, -28.557522}, {-173.205081, 0}, {-153.208889, 28.557522}, {153.208889, 28.557522}}},
   312  		},
   313  		{
   314  			name:     "intersect #9",
   315  			subject:  poly.Polygon[T]{{{128.557522, 253.208889}, {100, 273.205081}, {68.404029, 287.938524}, {68.404029, -87.938524}, {100, -73.205081}, {128.557522, -53.208889}}},
   316  			clipping: poly.Polygon[T]{{{131.595971, 287.938524}, {100, 273.205081}, {71.442478, 253.208889}, {71.442478, -53.208889}, {100, -73.205081}, {131.595971, -87.938524}}},
   317  			expected: poly.Polygon[T]{{{100, 273.205081}, {128.557522, 253.208889}, {128.557522, -53.208889}, {100, -73.205081}, {71.442478, -53.208889}, {71.442478, 253.208889}, {100, 273.205081}}},
   318  		},
   319  		{
   320  			name:     "intersect #10",
   321  			subject:  poly.Polygon[T]{{{24, 7}, {36, 7}, {36, 23}, {24, 23}}},
   322  			clipping: poly.Polygon[T]{{{24, 7}, {24.836228, 7.043825}, {25.663294, 7.174819}, {26.472136, 7.391548}, {27.253893, 7.691636}, {28, 8.071797}, {28.702282, 8.527864}, {29.353045, 9.054841}, {29.945159, 9.646955}, {30.472136, 10.297718}, {30.928203, 11}, {31.308364, 11.746107}, {31.608452, 12.527864}, {31.825181, 13.336706}, {31.956175, 14.163772}, {32, 15}, {31.956175, 15.836228}, {31.825181, 16.663294}, {31.608452, 17.472136}, {31.308364, 18.253893}, {30.928203, 19}, {30.472136, 19.702282}, {29.945159, 20.353045}, {29.353045, 20.945159}, {28.702282, 21.472136}, {28, 21.928203}, {27.253893, 22.308364}, {26.472136, 22.608452}, {25.663294, 22.825181}, {24.836228, 22.956175}, {24, 23}, {23.163772, 22.956175}, {22.336706, 22.825181}, {21.527864, 22.608452}, {20.746107, 22.308364}, {20, 21.928203}, {19.297718, 21.472136}, {18.646955, 20.945159}, {18.054841, 20.353045}, {17.527864, 19.702282}, {17.071797, 19}, {16.691636, 18.253893}, {16.391548, 17.472136}, {16.174819, 16.663294}, {16.043825, 15.836228}, {16, 15}, {16.043825, 14.163772}, {16.174819, 13.336706}, {16.391548, 12.527864}, {16.691636, 11.746107}, {17.071797, 11}, {17.527864, 10.297718}, {18.054841, 9.646955}, {18.646955, 9.054841}, {19.297718, 8.527864}, {20, 8.071797}, {20.746107, 7.691636}, {21.527864, 7.391548}, {22.336706, 7.174819}, {23.163772, 7.043825}}},
   323  			expected: poly.Polygon[T]{{{24.836228, 22.956175}, {25.663294, 22.825181}, {26.472136, 22.608452}, {27.253893, 22.308364}, {28, 21.928203}, {28.702282, 21.472136}, {29.353045, 20.945159}, {29.945159, 20.353045}, {30.472136, 19.702282}, {30.928203, 19}, {31.308364, 18.253893}, {31.608452, 17.472136}, {31.825181, 16.663294}, {31.956175, 15.836228}, {32, 15}, {31.956175, 14.163772}, {31.825181, 13.336706}, {31.608452, 12.527864}, {31.308364, 11.746107}, {30.928203, 11}, {30.472136, 10.297718}, {29.945159, 9.646955}, {29.353045, 9.054841}, {28.702282, 8.527864}, {28, 8.071797}, {27.253893, 7.691636}, {26.472136, 7.391548}, {25.663294, 7.174819}, {24.836228, 7.043825}, {24, 7}, {24, 23}}},
   324  		},
   325  		{
   326  			name:     "intersect #11",
   327  			subject:  poly.Polygon[T]{{{1568, 360.5}, {1567.2041, 392.66965}, {1564.8181, 424.76056}, {1560.8481, 456.69424}, {1555.3038, 488.3925}, {1548.1987, 519.7778}, {1539.55, 550.7733}, {1529.3793, 581.30316}, {1517.711, 611.2927}, {1504.574, 640.6685}, {1490.0002, 669.3587}, {1474.0255, 697.2931}, {1456.689, 724.4032}, {1438.0328, 750.6228}, {1418.1029, 775.8878}, {1396.9479, 800.1361}, {1374.6196, 823.3087}, {1351.1727, 845.34863}, {1326.6646, 866.20215}, {1301.155, 885.81805}, {1274.7067, 904.14844}, {1247.3842, 921.14844}, {1219.2544, 936.7764}, {1190.3861, 950.9942}, {1160.8501, 963.7669}, {1130.7186, 975.06335}, {1100.0653, 984.8558}, {1068.9652, 993.12036}, {1037.4944, 999.83685}, {1005.73004, 1004.9888}, {973.7497, 1008.5634}, {941.6318, 1010.55225}, {909.4547, 1010.95026}, {877.2974, 1009.75653}, {845.23846, 1006.97394}, {813.3563, 1002.6093}, {781.7291, 996.67334}, {750.4341, 989.18054}, {719.548, 980.1493}, {689.14624, 969.6016}, {659.3033, 957.5634}, {630.09216, 944.064}, {601.58435, 929.1366}, {573.8496, 912.81757}, {546.95593, 895.1469}, {520.969, 876.1679}, {495.95242, 855.927}, {471.96744, 834.47363}, {449.07278, 811.86035}, {427.32446, 788.1426}, {406.77567, 763.3783}, {387.47675, 737.62805}, {369.4748, 710.95496}, {352.8141, 683.4242}, {337.53516, 655.10315}, {323.67554, 626.0613}, {311.26917, 596.3694}, {300.34625, 566.10034}, {290.93372, 535.3282}, {283.05444, 504.12817}, {276.72772, 472.57666}, {271.96918, 440.75092}, {268.79034, 408.72873}, {267.19897, 376.58856}, {267.19904, 344.409}, {268.79047, 312.26883}, {271.96948, 280.24667}, {276.72815, 248.42093}, {283.055, 216.86945}, {290.93433, 185.66945}, {300.34705, 154.89732}, {311.27002, 124.62831}, {323.67657, 94.93652}, {337.53625, 65.89465}, {352.81525, 37.5737}, {369.47614, 10.042999}, {387.47815, -16.630066}, {406.7772, -42.38022}, {427.32608, -67.14444}, {449.0745, -90.86218}, {471.96924, -113.47528}, {495.95428, -134.92856}, {520.9708, -155.16931}, {546.95764, -174.14813}, {573.8513, -191.8186}, {601.58594, -208.13745}, {630.0936, -223.06476}, {659.3047, -236.56403}, {689.1476, -248.60211}, {719.54926, -259.14972}, {750.4353, -268.18085}, {781.7303, -275.67358}, {813.3575, -281.6095}, {845.2395, -285.974}, {877.2984, -288.75653}, {909.4556, -289.95026}, {941.63257, -289.5522}, {973.7504, -287.56335}, {1005.73065, -283.98865}, {1037.495, -278.83673}, {1068.9657, -272.12024}, {1100.0657, -263.85565}, {1130.7189, -254.06323}, {1160.8503, -242.76678}, {1190.3864, -229.99414}, {1219.2544, -215.77643}, {1247.3843, -200.14844}, {1274.7067, -183.1485}, {1301.1549, -164.81818}, {1326.6643, -145.20227}, {1351.1725, -124.34885}, {1374.6194, -102.30896}, {1396.9476, -79.136505}, {1418.1025, -54.888153}, {1438.0325, -29.623291}, {1456.6886, -3.403748}, {1474.0251, 23.70633}, {1489.9999, 51.640564}, {1504.5736, 80.33066}, {1517.7107, 109.70636}, {1529.3789, 139.69579}, {1539.5498, 170.22556}, {1548.1984, 201.22096}, {1555.3036, 232.60616}, {1560.848, 264.30432}, {1564.8181, 296.23785}, {1567.204, 328.3287}}},
   328  			clipping: poly.Polygon[T]{{{449, 399}, {400, 350}, {267, 347}, {267, -290}, {267, -290}, {1567, -290}, {1567, -291}, {1567, 1010}, {1567, 1010}, {268, 1010}, {267, 1010}, {267, 415}}},
   329  			expected: poly.Polygon[T]{{{973.7497, 1008.5634}, {1005.73004, 1004.9888}, {1037.4944, 999.83685}, {1068.9652, 993.12036}, {1100.0653, 984.8558}, {1130.7186, 975.06335}, {1160.8501, 963.7669}, {1190.3861, 950.9942}, {1219.2544, 936.7764}, {1247.3842, 921.14844}, {1274.7067, 904.14844}, {1301.155, 885.81805}, {1326.6646, 866.20215}, {1351.1727, 845.34863}, {1374.6196, 823.3087}, {1396.9479, 800.1361}, {1418.1029, 775.8878}, {1438.0328, 750.6228}, {1456.689, 724.4032}, {1474.0255, 697.2931}, {1490.0002, 669.3587}, {1504.574, 640.6685}, {1517.711, 611.2927}, {1529.3793, 581.30316}, {1539.55, 550.7733}, {1548.1987, 519.7778}, {1555.3038, 488.3925}, {1560.8481, 456.69424}, {1564.8181, 424.76056}, {1567, 395.41455}, {1567, 325.58502}, {1564.8181, 296.23785}, {1560.848, 264.30432}, {1555.3036, 232.60616}, {1548.1984, 201.22096}, {1539.5498, 170.22556}, {1529.3789, 139.69579}, {1517.7107, 109.70636}, {1504.5736, 80.33066}, {1489.9999, 51.640564}, {1474.0251, 23.70633}, {1456.6886, -3.403748}, {1438.0325, -29.623291}, {1418.1025, -54.888153}, {1396.9476, -79.136505}, {1374.6194, -102.30896}, {1351.1725, -124.34885}, {1326.6643, -145.20227}, {1301.1549, -164.81818}, {1274.7067, -183.1485}, {1247.3843, -200.14844}, {1219.2544, -215.77643}, {1190.3864, -229.99414}, {1160.8503, -242.76678}, {1130.7189, -254.06323}, {1100.0657, -263.85565}, {1068.9657, -272.12024}, {1037.495, -278.83673}, {1005.73065, -283.98865}, {973.7504, -287.56335}, {941.63257, -289.5522}, {909.4556, -289.95026}, {877.2984, -288.75653}, {845.2395, -285.974}, {813.3575, -281.6095}, {781.7303, -275.67358}, {750.4353, -268.18085}, {719.54926, -259.14972}, {689.1476, -248.60211}, {659.3047, -236.56403}, {630.0936, -223.06476}, {601.58594, -208.13745}, {573.8513, -191.8186}, {546.95764, -174.14813}, {520.9708, -155.16931}, {495.95428, -134.92856}, {471.96924, -113.47528}, {449.0745, -90.86218}, {427.32608, -67.14444}, {406.7772, -42.38022}, {387.47815, -16.630066}, {369.47614, 10.042999}, {352.81525, 37.5737}, {337.53625, 65.89465}, {323.67657, 94.93652}, {311.27002, 124.62831}, {300.34705, 154.89732}, {290.93433, 185.66945}, {283.055, 216.86945}, {276.72815, 248.42093}, {271.96948, 280.24667}, {268.79047, 312.26883}, {267.19904, 344.409}, {267.19904, 347.0045}, {400, 350}, {449, 399}, {269.39203, 414.7897}, {271.96918, 440.75092}, {276.72772, 472.57666}, {283.05444, 504.12817}, {290.93372, 535.3282}, {300.34625, 566.10034}, {311.26917, 596.3694}, {323.67554, 626.0613}, {337.53516, 655.10315}, {352.8141, 683.4242}, {369.4748, 710.95496}, {387.47675, 737.62805}, {406.77567, 763.3783}, {427.32446, 788.1426}, {449.07278, 811.86035}, {471.96744, 834.47363}, {495.95242, 855.927}, {520.969, 876.1679}, {546.95593, 895.1469}, {573.8496, 912.81757}, {601.58435, 929.1366}, {630.09216, 944.064}, {659.3033, 957.5634}, {689.14624, 969.6016}, {719.548, 980.1493}, {750.4341, 989.18054}, {781.7291, 996.67334}, {813.3563, 1002.6093}, {845.23846, 1006.97394}, {877.2974, 1009.75653}, {883.85614, 1010}, {950.55005, 1010}}},
   330  		},
   331  		{
   332  			name:     "intersect #12",
   333  			subject:  poly.Polygon[T]{{{2.500055, 20.615429}, {42.500055, -19.384571}, {82.500055, 20.615429}, {42.500055, 60.615429}}},
   334  			clipping: poly.Polygon[T]{{{7.604714, 25.720088}, {11.897741, 21.427062}, {36.852887, 46.382207}, {32.55986, 50.675234}}},
   335  			expected: poly.Polygon[T]{{{36.852887, 46.382207}, {11.897741, 21.427062}, {7.604714, 25.720088}, {32.55986, 50.675234}}},
   336  		},
   337  		{
   338  			name:     "intersect #13",
   339  			subject:  poly.Polygon[T]{{{0, 10}, {0, 0}, {10, 0}, {10, 10}}},
   340  			clipping: poly.Polygon[T]{{{0, 5}, {5, 0}, {10, 5}, {5, 10}}},
   341  			expected: poly.Polygon[T]{{{10, 5}, {5, 0}, {0, 5}, {5, 10}}},
   342  		},
   343  		{
   344  			name: "intersect self-intersection",
   345  			subject: poly.Polygon[T]{
   346  				{{0, 0}, {1, 0}, {1, 1}, {0, 1}},
   347  				{{1, 0}, {2, 0}, {2, 1}, {1, 1}},
   348  			},
   349  			clipping: poly.Polygon[T]{{{0, 0.25}, {3, 0.25}, {3, 0.75}, {0, 0.75}}},
   350  			expected: poly.Polygon[T]{{{2, 0.25}, {0, 0.25}, {0, 0.75}, {2, 0.75}}},
   351  		},
   352  	}
   353  	for i, test := range tests {
   354  		result := test.subject.Intersect(test.clipping)
   355  		check.True(t, matchPolys(test.expected, result), "%T test case %d (%s): %v", test.subject, i, test.name, result)
   356  	}
   357  }
   358  
   359  func TestSubtract(t *testing.T) {
   360  	testSubtract[float32](t)
   361  	if floatsAreConsistentAcrossPlatforms {
   362  		testSubtract[float64](t)
   363  	}
   364  }
   365  
   366  func testSubtract[T constraints.Float](t *testing.T) {
   367  	tests := []testCase[T]{
   368  		{
   369  			name:     "subtract #1",
   370  			subject:  poly.Polygon[T]{{{160.094495, 200.379924}, {90.094495, 200.379924}, {55.094495, 139.758146}, {90.094495, 79.136368}}},
   371  			clipping: poly.Polygon[T]{{{82.846611, 131.518814}, {66.592063, 159.672518}, {90.094495, 200.379924}, {160.094495, 200.379924}}},
   372  			expected: poly.Polygon[T]{{{90.094495, 79.136368}, {55.094495, 139.758146}, {66.592063, 159.672518}, {82.846611, 131.518814}, {160.094495, 200.379924}}},
   373  		},
   374  		{
   375  			name:     "subtract #2",
   376  			subject:  poly.Polygon[T]{{{2.500055, 20.615429}, {42.500055, -19.384571}, {82.500055, 20.615429}, {42.500055, 60.615429}}},
   377  			clipping: poly.Polygon[T]{{{7.604714, 25.720088}, {32.55986, 50.675234}, {36.852887, 46.382207}, {11.897741, 21.427062}}},
   378  			expected: poly.Polygon[T]{{{82.50005, 20.615429}, {42.500053, -19.384571}, {2.500055, 20.615429}, {7.6047134, 25.720087}, {7.604714, 25.720087}, {11.897741, 21.427061}, {36.852886, 46.382206}, {32.55986, 50.675236}, {42.500053, 60.61543}}},
   379  		},
   380  		{
   381  			name:     "subtract #3",
   382  			subject:  poly.Polygon[T]{{{38.572124, 172.339556}, {40, 171.339746}, {41.579799, 170.603074}, {43.263518, 170.151922}, {45, 170}, {46.736482, 170.151922}, {48.420201, 170.603074}, {50, 171.339746}, {51.427876, 172.339556}}},
   383  			clipping: poly.Polygon[T]{{{51.427876, 172.339556}, {50, 171.339746}, {48.420201, 170.603074}, {46.736482, 170.151922}, {45, 170}, {43.263518, 170.151922}, {42.781168, 170.281168}, {42.651922, 170.763518}, {42.5, 172}}},
   384  			expected: poly.Polygon[T]{{{42.5, 172}, {42.651922, 170.763518}, {42.781168, 170.281168}, {43.263518, 170.151922}, {41.579799, 170.603074}, {40, 171.339746}, {38.572124, 172.339556}, {51.427876, 172.339556}}},
   385  		},
   386  		{
   387  			name:     "subtract #4",
   388  			subject:  poly.Polygon[T]{{{24, 7}, {36, 7}, {36, 23}, {24, 23}}},
   389  			clipping: poly.Polygon[T]{{{24, 7}, {24.836228, 7.043825}, {25.663294, 7.174819}, {26.472136, 7.391548}, {27.253893, 7.691636}, {28, 8.071797}, {28.702282, 8.527864}, {29.353045, 9.054841}, {29.945159, 9.646955}, {30.472136, 10.297718}, {30.928203, 11}, {31.308364, 11.746107}, {31.608452, 12.527864}, {31.825181, 13.336706}, {31.956175, 14.163772}, {32, 15}, {31.956175, 15.836228}, {31.825181, 16.663294}, {31.608452, 17.472136}, {31.308364, 18.253893}, {30.928203, 19}, {30.472136, 19.702282}, {29.945159, 20.353045}, {29.353045, 20.945159}, {28.702282, 21.472136}, {28, 21.928203}, {27.253893, 22.308364}, {26.472136, 22.608452}, {25.663294, 22.825181}, {24.836228, 22.956175}, {24, 23}, {23.163772, 22.956175}, {22.336706, 22.825181}, {21.527864, 22.608452}, {20.746107, 22.308364}, {20, 21.928203}, {19.297718, 21.472136}, {18.646955, 20.945159}, {18.054841, 20.353045}, {17.527864, 19.702282}, {17.071797, 19}, {16.691636, 18.253893}, {16.391548, 17.472136}, {16.174819, 16.663294}, {16.043825, 15.836228}, {16, 15}, {16.043825, 14.163772}, {16.174819, 13.336706}, {16.391548, 12.527864}, {16.691636, 11.746107}, {17.071797, 11}, {17.527864, 10.297718}, {18.054841, 9.646955}, {18.646955, 9.054841}, {19.297718, 8.527864}, {20, 8.071797}, {20.746107, 7.691636}, {21.527864, 7.391548}, {22.336706, 7.174819}, {23.163772, 7.043825}}},
   390  			expected: poly.Polygon[T]{{{36, 7}, {24, 7}, {24.836228, 7.043825}, {25.663294, 7.174819}, {26.472136, 7.391548}, {27.253893, 7.691636}, {28, 8.071797}, {28.702282, 8.527864}, {29.353045, 9.054841}, {29.945159, 9.646955}, {30.472136, 10.297718}, {30.928203, 11}, {31.308364, 11.746107}, {31.608452, 12.527864}, {31.825181, 13.336706}, {31.956175, 14.163772}, {32, 15}, {31.956175, 15.836228}, {31.825181, 16.663294}, {31.608452, 17.472136}, {31.308364, 18.253893}, {30.928203, 19}, {30.472136, 19.702282}, {29.945159, 20.353045}, {29.353045, 20.945159}, {28.702282, 21.472136}, {28, 21.928203}, {27.253893, 22.308364}, {26.472136, 22.608452}, {25.663294, 22.825181}, {24.836228, 22.956175}, {24, 23}, {36, 23}}},
   391  		},
   392  		{
   393  			name:     "subtract #5",
   394  			subject:  poly.Polygon[T]{{{114, 0}, {161, 0}, {114, 168}}},
   395  			clipping: poly.Polygon[T]{{{99, 164}, {114, 108}, {121, 164}}},
   396  			expected: poly.Polygon[T]{
   397  				{{115.11905, 164}, {114, 164}, {114, 168}},
   398  				{{161, 0}, {114, 0}, {114, 108}, {119.18382, 149.4706}},
   399  			},
   400  		},
   401  		{
   402  			name:     "subtract #6",
   403  			subject:  poly.Polygon[T]{{{426694.636527, -668547.161158}, {426714.57523, -668548.923865}, {426745.396481, -668550.465125}}},
   404  			clipping: poly.Polygon[T]{{{426714.57523, -668548.923865}, {426744.637187, -668550.05919}, {426745.396482, -668550.465224}}},
   405  			expected: poly.Polygon[T]{{{426732.4, -668549.6}, {426714.56, -668548.94}, {426694.62, -668547.2}}},
   406  		},
   407  		{
   408  			name:     "subtract #7",
   409  			subject:  poly.Polygon[T]{{{38.572124, 172.339556}, {40, 171.339746}, {41.579799, 170.603074}, {43.263518, 170.151922}, {45, 170}, {46.736482, 170.151922}, {48.420201, 170.603074}, {50, 171.339746}, {51.427876, 172.339556}}},
   410  			clipping: poly.Polygon[T]{{{51.427876, 172.339556}, {50, 171.339746}, {48.420201, 170.603074}, {46.736482, 170.151922}, {45, 170}, {43.263518, 170.151922}, {42.781168, 170.281168}, {42.651922, 170.763518}, {42.5, 172}}},
   411  			expected: poly.Polygon[T]{{{42.5, 172}, {42.651922, 170.763518}, {42.781168, 170.281168}, {43.263518, 170.151922}, {41.579799, 170.603074}, {40, 171.339746}, {38.572124, 172.339556}, {51.427876, 172.339556}}},
   412  		},
   413  		{
   414  			name: "subtract #8",
   415  			subject: poly.Polygon[T]{
   416  				{{0, 0}, {1, 0}, {1, 1}, {0, 1}},
   417  				{{1, 0}, {2, 0}, {2, 1}, {1, 1}},
   418  			},
   419  			clipping: poly.Polygon[T]{{{0, 0.25}, {3, 0.25}, {3, 0.75}, {0, 0.75}}},
   420  			expected: poly.Polygon[T]{
   421  				{{2, 0.75}, {0, 0.75}, {0, 1}, {2, 1}},
   422  				{{2, 0}, {0, 0}, {0, 0.25}, {2, 0.25}},
   423  			},
   424  		},
   425  		{
   426  			name: "subtract self-intersect",
   427  			subject: poly.Polygon[T]{
   428  				{{0, 0}, {1, 0}, {1, 1}, {0, 1}},
   429  				{{1, 0}, {2, 0}, {2, 1}, {1, 1}},
   430  			},
   431  			clipping: poly.Polygon[T]{{{0, 0.25}, {3, 0.25}, {3, 0.75}, {0, 0.75}}},
   432  			expected: poly.Polygon[T]{
   433  				{{2, 0.75}, {0, 0.75}, {0, 1}, {2, 1}},
   434  				{{2, 0}, {0, 0}, {0, 0.25}, {2, 0.25}},
   435  			},
   436  		},
   437  	}
   438  	for i, test := range tests {
   439  		result := test.subject.Sub(test.clipping)
   440  		check.True(t, matchPolys(test.expected, result), "%T test case %d (%s): %v", test.subject, i, test.name, result)
   441  	}
   442  }
   443  
   444  func TestXor(t *testing.T) {
   445  	testXor[float32](t)
   446  	testXor[float64](t)
   447  }
   448  
   449  func testXor[T constraints.Float](t *testing.T) {
   450  	tests := []testCase[T]{
   451  		{
   452  			name:     "xor #1",
   453  			subject:  poly.Polygon[T]{{{24, 7}, {36, 7}, {36, 23}, {24, 23}}},
   454  			clipping: poly.Polygon[T]{{{24, 7}, {24.836228, 7.043825}, {25.663294, 7.174819}, {26.472136, 7.391548}, {27.253893, 7.691636}, {28, 8.071797}, {28.702282, 8.527864}, {29.353045, 9.054841}, {29.945159, 9.646955}, {30.472136, 10.297718}, {30.928203, 11}, {31.308364, 11.746107}, {31.608452, 12.527864}, {31.825181, 13.336706}, {31.956175, 14.163772}, {32, 15}, {31.956175, 15.836228}, {31.825181, 16.663294}, {31.608452, 17.472136}, {31.308364, 18.253893}, {30.928203, 19}, {30.472136, 19.702282}, {29.945159, 20.353045}, {29.353045, 20.945159}, {28.702282, 21.472136}, {28, 21.928203}, {27.253893, 22.308364}, {26.472136, 22.608452}, {25.663294, 22.825181}, {24.836228, 22.956175}, {24, 23}, {23.163772, 22.956175}, {22.336706, 22.825181}, {21.527864, 22.608452}, {20.746107, 22.308364}, {20, 21.928203}, {19.297718, 21.472136}, {18.646955, 20.945159}, {18.054841, 20.353045}, {17.527864, 19.702282}, {17.071797, 19}, {16.691636, 18.253893}, {16.391548, 17.472136}, {16.174819, 16.663294}, {16.043825, 15.836228}, {16, 15}, {16.043825, 14.163772}, {16.174819, 13.336706}, {16.391548, 12.527864}, {16.691636, 11.746107}, {17.071797, 11}, {17.527864, 10.297718}, {18.054841, 9.646955}, {18.646955, 9.054841}, {19.297718, 8.527864}, {20, 8.071797}, {20.746107, 7.691636}, {21.527864, 7.391548}, {22.336706, 7.174819}, {23.163772, 7.043825}}},
   455  			expected: poly.Polygon[T]{
   456  				{{36, 7}, {24, 7}, {24.836228, 7.043825}, {25.663294, 7.174819}, {26.472136, 7.391548}, {27.253893, 7.691636}, {28, 8.071797}, {28.702282, 8.527864}, {29.353045, 9.054841}, {29.945159, 9.646955}, {30.472136, 10.297718}, {30.928203, 11}, {31.308364, 11.746107}, {31.608452, 12.527864}, {31.825181, 13.336706}, {31.956175, 14.163772}, {32, 15}, {31.956175, 15.836228}, {31.825181, 16.663294}, {31.608452, 17.472136}, {31.308364, 18.253893}, {30.928203, 19}, {30.472136, 19.702282}, {29.945159, 20.353045}, {29.353045, 20.945159}, {28.702282, 21.472136}, {28, 21.928203}, {27.253893, 22.308364}, {26.472136, 22.608452}, {25.663294, 22.825181}, {24.836228, 22.956175}, {24, 23}, {36, 23}},
   457  				{{24, 7}, {23.163772, 7.043825}, {22.336706, 7.174819}, {21.527864, 7.391548}, {20.746107, 7.691636}, {20, 8.071797}, {19.297718, 8.527864}, {18.646955, 9.054841}, {18.054841, 9.646955}, {17.527864, 10.297718}, {17.071797, 11}, {16.691636, 11.746107}, {16.391548, 12.527864}, {16.174819, 13.336706}, {16.043825, 14.163772}, {16, 15}, {16.043825, 15.836228}, {16.174819, 16.663294}, {16.391548, 17.472136}, {16.691636, 18.253893}, {17.071797, 19}, {17.527864, 19.702282}, {18.054841, 20.353045}, {18.646955, 20.945159}, {19.297718, 21.472136}, {20, 21.928203}, {20.746107, 22.308364}, {21.527864, 22.608452}, {22.336706, 22.825181}, {23.163772, 22.956175}, {24, 23}},
   458  			},
   459  		},
   460  		{
   461  			name: "xor self-intersect",
   462  			subject: poly.Polygon[T]{
   463  				{{0, 0}, {1, 0}, {1, 1}, {0, 1}},
   464  				{{1, 0}, {2, 0}, {2, 1}, {1, 1}},
   465  			},
   466  			clipping: poly.Polygon[T]{{{0, 0.25}, {3, 0.25}, {3, 0.75}, {0, 0.75}}},
   467  			expected: poly.Polygon[T]{
   468  				{{2, 0.75}, {3, 0.75}, {3, 0.25}, {2, 0.25}, {2, 0.75}, {0, 0.75}, {0, 1}, {2, 1}},
   469  				{{2, 0}, {0, 0}, {0, 0.25}, {2, 0.25}},
   470  			},
   471  		},
   472  	}
   473  	for i, test := range tests {
   474  		result := test.subject.Xor(test.clipping)
   475  		check.True(t, matchPolys(test.expected, result), "%T test case %d (%s): %v", test.subject, i, test.name, result)
   476  	}
   477  }
   478  
   479  func matchPolys[T constraints.Float](left, right poly.Polygon[T]) bool {
   480  	left = simplifyPoly(left)
   481  	right = simplifyPoly(right)
   482  	if len(left) != len(right) {
   483  		return false
   484  	}
   485  	for i, c := range left {
   486  		if len(c) != len(right[i]) {
   487  			return false
   488  		}
   489  		for j, pt := range c {
   490  			if !pt.EqualWithin(right[i][j], 0.0001) {
   491  				return false
   492  			}
   493  		}
   494  	}
   495  	return true
   496  }
   497  
   498  func simplifyPoly[T constraints.Float](p poly.Polygon[T]) poly.Polygon[T] {
   499  	var revised poly.Polygon[T]
   500  	for _, c := range p {
   501  		var nc poly.Contour[T]
   502  		for i, pt := range c {
   503  			if i == 0 || !pt.EqualWithin(c[i-1], 0.0001) {
   504  				nc = append(nc, pt)
   505  			}
   506  		}
   507  		revised = append(revised, nc)
   508  	}
   509  	return revised
   510  }