github.com/richardwilkes/toolbox@v1.121.0/xmath/geom/poly/polygon.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 11 12 import ( 13 "strings" 14 15 "github.com/richardwilkes/toolbox/xmath/geom" 16 "golang.org/x/exp/constraints" 17 ) 18 19 const ( 20 clipping = iota 21 subject 22 ) 23 24 type clipOp int 25 26 const ( 27 subtractOp clipOp = iota 28 intersectOp 29 xorOp 30 unionOp 31 ) 32 33 // Polygon holds one or more contour lines. The polygon may contain holes and may be self-intersecting. 34 type Polygon[T constraints.Float] []Contour[T] 35 36 // Clone returns a duplicate of this polygon. 37 func (p Polygon[T]) Clone() Polygon[T] { 38 if len(p) == 0 { 39 return nil 40 } 41 clone := Polygon[T](make([]Contour[T], len(p))) 42 for i := range p { 43 clone[i] = p[i].Clone() 44 } 45 return clone 46 } 47 48 // String implements fmt.Stringer. 49 func (p Polygon[T]) String() string { 50 var buffer strings.Builder 51 buffer.WriteByte('{') 52 for i, c := range p { 53 if i != 0 { 54 buffer.WriteByte(',') 55 } 56 buffer.WriteString(c.String()) 57 } 58 buffer.WriteByte('}') 59 return buffer.String() 60 } 61 62 // Bounds returns the bounding rectangle of this polygon. 63 func (p Polygon[T]) Bounds() geom.Rect[T] { 64 if len(p) == 0 { 65 return geom.Rect[T]{} 66 } 67 b := p[0].Bounds() 68 for _, c := range p[1:] { 69 b = b.Union(c.Bounds()) 70 } 71 return b 72 } 73 74 // Contains returns true if the point is contained by this polygon. 75 func (p Polygon[T]) Contains(pt geom.Point[T]) bool { 76 for i := range p { 77 if p[i].Contains(pt) { 78 return true 79 } 80 } 81 return false 82 } 83 84 // ContainsEvenOdd returns true if the point is contained by the polygon using the even-odd rule. 85 // https://en.wikipedia.org/wiki/Even-odd_rule 86 func (p Polygon[T]) ContainsEvenOdd(pt geom.Point[T]) bool { 87 var count int 88 for i := range p { 89 if p[i].Contains(pt) { 90 count++ 91 } 92 } 93 return count%2 == 1 94 } 95 96 // Transform returns the result of transforming this Polygon by the Matrix. 97 func (p Polygon[T]) Transform(m geom.Matrix[T]) Polygon[T] { 98 clone := p.Clone() 99 for _, c := range clone { 100 for i := range c { 101 c[i] = m.TransformPoint(c[i]) 102 } 103 } 104 return clone 105 } 106 107 // Union returns a new Polygon holding the union of both Polygons. 108 func (p Polygon[T]) Union(other Polygon[T]) Polygon[T] { 109 return p.construct(unionOp, other) 110 } 111 112 // Intersect returns a new Polygon holding the intersection of both Polygons. 113 func (p Polygon[T]) Intersect(other Polygon[T]) Polygon[T] { 114 return p.construct(intersectOp, other) 115 } 116 117 // Sub returns a new Polygon holding the result of removing the other Polygon from this Polygon. 118 func (p Polygon[T]) Sub(other Polygon[T]) Polygon[T] { 119 return p.construct(subtractOp, other) 120 } 121 122 // Xor returns a new Polygon holding the result of xor'ing this Polygon with the other Polygon. 123 func (p Polygon[T]) Xor(other Polygon[T]) Polygon[T] { 124 return p.construct(xorOp, other) 125 } 126 127 func (p Polygon[T]) construct(op clipOp, other Polygon[T]) Polygon[T] { 128 var result Polygon[T] 129 130 // Short-circuit the work if we can trivially determine the result is an empty polygon. 131 if (len(p) == 0 && len(other) == 0) || 132 (len(p) == 0 && (op == intersectOp || op == subtractOp)) || 133 (len(other) == 0 && op == intersectOp) { 134 return result 135 } 136 137 // Build the local minima table and the scan beam table 138 sbTree := &scanBeamTree[T]{} 139 subjNonContributing, clipNonContributing := p.identifyNonContributingContours(op, other) 140 lmt := buildLocalMinimaTable(nil, sbTree, p, subjNonContributing, subject, op) 141 if lmt = buildLocalMinimaTable(lmt, sbTree, other, clipNonContributing, clipping, op); lmt == nil { 142 return result 143 } 144 sbt := sbTree.buildScanBeamTable() 145 146 // Process each scan beam 147 var aet *edgeNode[T] 148 var outPoly *polygonNode[T] 149 localMin := lmt 150 i := 0 151 for i < len(sbt) { 152 153 // Set yb and yt to the bottom and top of the scanbeam 154 var yt, dy T 155 var bPt geom.Point[T] 156 bPt.Y = sbt[i] 157 i++ 158 if i < len(sbt) { 159 yt = sbt[i] 160 dy = yt - bPt.Y 161 } 162 163 // If LMT node corresponding to bPt.Y exists 164 if localMin != nil && localMin.y == bPt.Y { 165 // Add edges starting at this local minimum to the AET 166 for edge := localMin.firstBound; edge != nil; edge = edge.nextBound { 167 aet = edge.addEdgeToActiveEdgeTable(aet, nil) 168 } 169 localMin = localMin.next 170 } 171 if aet == nil { 172 continue 173 } 174 175 aet.bundleFields(bPt) 176 bPt, outPoly = aet.process(op, bPt, outPoly) 177 aet = aet.deleteTerminatingEdges(bPt, yt) 178 179 if i < len(sbt) { 180 // Process each node in the intersection table 181 for inter := aet.buildIntersections(dy); inter != nil; inter = inter.next { 182 outPoly = inter.process(op, bPt, outPoly) 183 aet = aet.swapIntersectingEdgeBundles(inter) 184 } 185 aet = aet.prepareForNextScanBeam(yt) 186 } 187 } 188 189 // Generate the resulting polygon 190 if outPoly != nil { 191 return outPoly.generate() 192 } 193 return Polygon[T]{} 194 } 195 196 func (p Polygon[T]) identifyNonContributingContours(op clipOp, clip Polygon[T]) (subjNC, clipNC []bool) { 197 subjNC = make([]bool, len(p)) 198 clipNC = make([]bool, len(clip)) 199 if (op == intersectOp || op == subtractOp) && len(p) > 0 && len(clip) > 0 { 200 201 // Check all subject contour bounding boxes against clip boxes 202 overlaps := make([]bool, len(p)*len(clip)) 203 boxes := make([]geom.Rect[T], len(clip)) 204 for i, c := range clip { 205 boxes[i] = c.Bounds() 206 } 207 for si := range p { 208 box := p[si].Bounds() 209 for ci := range clip { 210 overlaps[ci*len(p)+si] = box.Intersects(boxes[ci]) 211 } 212 } 213 214 // For each clip contour, search for any subject contour overlaps 215 for ci := range clip { 216 clipNC[ci] = true 217 for si := range p { 218 if overlaps[ci*len(p)+si] { 219 clipNC[ci] = false 220 break 221 } 222 } 223 } 224 225 if op == intersectOp { 226 // For each subject contour, search for any clip contour overlaps 227 for si := range p { 228 subjNC[si] = true 229 for ci := range clip { 230 if overlaps[ci*len(p)+si] { 231 subjNC[si] = false 232 break 233 } 234 } 235 } 236 } 237 } 238 return 239 }