github.com/richardwilkes/toolbox@v1.121.0/xmath/geom/visibility/visibility.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 visibility 11 12 import ( 13 "cmp" 14 "slices" 15 16 "github.com/richardwilkes/toolbox/collection/quadtree" 17 "github.com/richardwilkes/toolbox/xmath" 18 "github.com/richardwilkes/toolbox/xmath/geom" 19 "github.com/richardwilkes/toolbox/xmath/geom/poly" 20 "golang.org/x/exp/constraints" 21 ) 22 23 const epsilon = 0.01 24 25 // Visibility holds state for computing a visibility polygon. 26 type Visibility[T constraints.Float] struct { 27 top T 28 left T 29 bottom T 30 right T 31 segments []Segment[T] 32 } 33 34 // New creates a Visibility object. If the obstructions do not intersect each other, pass in true for hasNoIntersections 35 // to eliminate the costly pass to break the segments up into smaller parts. 36 func New[T constraints.Float](bounds geom.Rect[T], obstructions []Segment[T], hasNoIntersections bool) *Visibility[T] { 37 v := &Visibility[T]{ 38 top: bounds.Y, 39 left: bounds.X, 40 bottom: bounds.Bottom() - 1, 41 right: bounds.Right() - 1, 42 segments: make([]Segment[T], len(obstructions)), 43 } 44 copy(v.segments, obstructions) 45 if !hasNoIntersections { 46 v.breakIntersections() 47 } 48 return v 49 } 50 51 // SetViewPoint sets a view point and generates a polygon with the unobstructed visible area. 52 func (v *Visibility[T]) SetViewPoint(viewPt geom.Point[T]) poly.Polygon[T] { 53 // If the view point is not within the bounding area, there is no visible area 54 if !v.inViewport(viewPt) { 55 return nil 56 } 57 58 // Generate a revised segment list by clipping the segments against the viewport and throwing out any that aren't 59 // within the viewport. 60 segments := make([]Segment[T], 0, len(v.segments)*2) 61 viewport := []geom.Point[T]{ 62 geom.NewPoint(v.left, v.top), 63 geom.NewPoint(v.right, v.top), 64 geom.NewPoint(v.right, v.bottom), 65 geom.NewPoint(v.left, v.bottom), 66 } 67 for _, si := range v.segments { 68 if (si.Start.X < v.left && si.End.X < v.left) || 69 (si.Start.Y < v.top && si.End.Y < v.top) || 70 (si.Start.X > v.right && si.End.X > v.right) || 71 (si.Start.Y > v.bottom && si.End.Y > v.bottom) { 72 continue 73 } 74 intersections := make([]geom.Point[T], 0, len(viewport)) 75 for j := range viewport { 76 k := (j + 1) % len(viewport) 77 if hasIntersection(si.Start, si.End, viewport[j], viewport[k]) { 78 pt, intersects := intersectLines(si.Start, si.End, viewport[j], viewport[k]) 79 if intersects && !mostlyEqual(pt, si.Start) && !mostlyEqual(pt, si.End) { 80 intersections = append(intersections, pt) 81 } 82 } 83 } 84 segments = v.collectSegments(si, intersections, segments, true) 85 } 86 87 // Add the viewport bounds to the segment list 88 const eps = 10 * epsilon 89 topLeft := geom.Point[T]{X: v.left - eps, Y: v.top - eps} 90 topRight := geom.Point[T]{X: v.right + eps, Y: v.top - eps} 91 bottomLeft := geom.Point[T]{X: v.left - eps, Y: v.bottom + eps} 92 bottomRight := geom.Point[T]{X: v.right + eps, Y: v.bottom + eps} 93 segments = append(segments, 94 Segment[T]{Start: topLeft, End: topRight}, 95 Segment[T]{Start: topRight, End: bottomRight}, 96 Segment[T]{Start: bottomRight, End: bottomLeft}, 97 Segment[T]{Start: bottomLeft, End: topLeft}, 98 ) 99 100 return v.computePolygon(viewPt, segments) 101 } 102 103 func (v *Visibility[T]) computePolygon(viewPt geom.Point[T], segments []Segment[T]) poly.Polygon[T] { 104 // Sweep through the points to generate the visibility contour 105 sorted := sortPoints(viewPt, segments) 106 mapper := &array{data: make([]int, len(segments))} 107 for i := range mapper.data { 108 mapper.data[i] = -1 109 } 110 heap := &array{} 111 start := geom.Point[T]{X: viewPt.X + 1, Y: viewPt.Y} 112 for i := range segments { 113 a1 := angle(segments[i].Start, viewPt) 114 a2 := angle(segments[i].End, viewPt) 115 if (a1 >= -180 && a1 <= 0 && a2 <= 180 && a2 >= 0 && a2-a1 > 180) || 116 (a2 >= -180 && a2 <= 0 && a1 <= 180 && a1 >= 0 && a1-a2 > 180) { 117 insert(i, heap, mapper, segments, viewPt, start) 118 } 119 } 120 contour := make(poly.Contour[T], 0, len(sorted)*2) 121 i := 0 122 for i < len(sorted) { 123 extend := false 124 shorten := false 125 orig := i 126 vertex := sorted[i].pt(segments) 127 oldSeg := heap.elem(0) 128 for { 129 if mapper.elem(sorted[i].segmentIndex) != -1 { 130 if sorted[i].segmentIndex == oldSeg { 131 extend = true 132 vertex = sorted[i].pt(segments) 133 } 134 remove(mapper.elem(sorted[i].segmentIndex), heap, mapper, segments, viewPt, vertex) 135 } else { 136 insert(sorted[i].segmentIndex, heap, mapper, segments, viewPt, vertex) 137 if heap.elem(0) != oldSeg { 138 shorten = true 139 } 140 } 141 i++ 142 if i == len(sorted) || sorted[i].angle >= sorted[orig].angle+epsilon { 143 break 144 } 145 } 146 if extend { 147 contour = append(contour, geom.Point[T]{X: vertex.X, Y: vertex.Y}) 148 s := segments[heap.elem(0)] 149 if cur, intersects := intersectLines(s.Start, s.End, viewPt, vertex); intersects && !mostlyEqual(cur, vertex) { 150 contour = append(contour, geom.Point[T]{X: cur.X, Y: cur.Y}) 151 } 152 } else if shorten { 153 s := segments[oldSeg] 154 if cur, intersects := intersectLines(s.Start, s.End, viewPt, vertex); intersects { 155 contour = append(contour, geom.Point[T]{X: cur.X, Y: cur.Y}) 156 } 157 s = segments[heap.elem(0)] 158 if cur, intersects := intersectLines(s.Start, s.End, viewPt, vertex); intersects { 159 contour = append(contour, geom.Point[T]{X: cur.X, Y: cur.Y}) 160 } 161 } 162 } 163 if len(contour) == 0 { 164 return nil 165 } 166 return poly.Polygon[T]{contour} 167 } 168 169 func (v *Visibility[T]) inViewport(pt geom.Point[T]) bool { 170 return pt.X >= v.left-epsilon && 171 pt.Y >= v.top-epsilon && 172 pt.X <= v.right+epsilon && 173 pt.Y <= v.bottom+epsilon 174 } 175 176 func (v *Visibility[T]) breakIntersections() { 177 var qt quadtree.QuadTree[T, Segment[T]] 178 for _, si := range v.segments { 179 qt.Insert(si) 180 } 181 segments := make([]Segment[T], 0, len(v.segments)*2) 182 for _, si := range v.segments { 183 var intersections []geom.Point[T] 184 for _, sj := range qt.FindIntersects(si.Bounds()) { 185 if si == sj { 186 continue 187 } 188 if hasIntersection(si.Start, si.End, sj.Start, sj.End) { 189 pt, intersects := intersectLines(si.Start, si.End, sj.Start, sj.End) 190 if intersects && !mostlyEqual(pt, si.Start) && !mostlyEqual(pt, si.End) { 191 intersections = append(intersections, pt) 192 } 193 } 194 } 195 segments = v.collectSegments(si, intersections, segments, false) 196 } 197 v.segments = slices.Clip(segments) 198 } 199 200 func (v *Visibility[T]) collectSegments(s Segment[T], intersections []geom.Point[T], segments []Segment[T], onlyInViewPort bool) []Segment[T] { 201 start := s.Start 202 for len(intersections) > 0 { 203 endIndex := 0 204 endDis := distance(start, intersections[0]) 205 for i := 1; i < len(intersections); i++ { 206 if dis := distance(start, intersections[i]); dis < endDis { 207 endDis = dis 208 endIndex = i 209 } 210 } 211 if !onlyInViewPort || (v.inViewport(start) && v.inViewport(intersections[endIndex])) { 212 segments = append(segments, Segment[T]{Start: start, End: intersections[endIndex]}) 213 } 214 start = intersections[endIndex] 215 intersections = slices.Delete(intersections, endIndex, endIndex+1) 216 } 217 if !onlyInViewPort || (v.inViewport(start) && v.inViewport(s.End)) { 218 segments = append(segments, Segment[T]{Start: start, End: s.End}) 219 } 220 return segments 221 } 222 223 func mostlyEqual[T constraints.Float](a, b geom.Point[T]) bool { 224 return a.EqualWithin(b, epsilon) 225 } 226 227 func remove[T constraints.Float](index int, heap, mapper *array, segments []Segment[T], position, destination geom.Point[T]) { 228 mapper.set(heap.elem(index), -1) 229 if index == heap.size()-1 { 230 heap.pop() 231 return 232 } 233 heap.set(index, heap.pop()) 234 mapper.set(heap.elem(index), index) 235 cur := index 236 parent := (cur - 1) / 2 237 if cur != 0 && lessThan(heap.elem(cur), heap.elem(parent), segments, position, destination) { 238 for cur > 0 { 239 parent = (cur - 1) / 2 240 parentElem := heap.elem(parent) 241 curElem := heap.elem(cur) 242 if !lessThan(curElem, parentElem, segments, position, destination) { 243 break 244 } 245 mapper.set(parentElem, cur) 246 mapper.set(curElem, parent) 247 heap.set(cur, parentElem) 248 heap.set(parent, curElem) 249 cur = parent 250 } 251 return 252 } 253 loop: 254 for { 255 left := 2*cur + 1 256 right := left + 1 257 switch { 258 case left < heap.size() && lessThan(heap.elem(left), heap.elem(cur), segments, position, destination) && 259 (right == heap.size() || lessThan(heap.elem(left), heap.elem(right), segments, position, destination)): 260 leftElem := heap.elem(left) 261 curElem := heap.elem(cur) 262 mapper.set(leftElem, cur) 263 mapper.set(curElem, left) 264 heap.set(left, curElem) 265 heap.set(cur, leftElem) 266 cur = left 267 case right < heap.size() && lessThan(heap.elem(right), heap.elem(cur), segments, position, destination): 268 rightElem := heap.elem(right) 269 curElem := heap.elem(cur) 270 mapper.set(rightElem, cur) 271 mapper.set(curElem, right) 272 heap.set(right, curElem) 273 heap.set(cur, rightElem) 274 cur = right 275 default: 276 break loop 277 } 278 } 279 } 280 281 func insert[T constraints.Float](index int, heap, mapper *array, segments []Segment[T], position, destination geom.Point[T]) { 282 if _, intersects := intersectLines(segments[index].Start, segments[index].End, position, destination); !intersects { 283 return 284 } 285 cur := heap.size() 286 heap.push(index) 287 mapper.set(index, cur) 288 for cur > 0 { 289 parent := (cur - 1) / 2 290 parentElem := heap.elem(parent) 291 curElem := heap.elem(cur) 292 if !lessThan(curElem, parentElem, segments, position, destination) { 293 break 294 } 295 mapper.set(parentElem, cur) 296 mapper.set(curElem, parent) 297 heap.set(cur, parentElem) 298 heap.set(parent, curElem) 299 cur = parent 300 } 301 } 302 303 func lessThan[T constraints.Float](index1, index2 int, segments []Segment[T], position, destination geom.Point[T]) bool { 304 pt1, intersects1 := intersectLines(segments[index1].Start, segments[index1].End, position, destination) 305 if !intersects1 { 306 return false 307 } 308 pt2, intersects2 := intersectLines(segments[index2].Start, segments[index2].End, position, destination) 309 if !intersects2 { 310 return false 311 } 312 if !mostlyEqual(pt1, pt2) { 313 d1 := distance(pt1, position) 314 d2 := distance(pt2, position) 315 return d1 < d2 316 } 317 var a1 T 318 if mostlyEqual(pt1, segments[index1].Start) { 319 a1 = angle2(segments[index1].End, pt1, position) 320 } else { 321 a1 = angle2(segments[index1].Start, pt1, position) 322 } 323 var a2 T 324 if mostlyEqual(pt2, segments[index2].Start) { 325 a2 = angle2(segments[index2].End, pt2, position) 326 } else { 327 a2 = angle2(segments[index2].Start, pt2, position) 328 } 329 if a1 < 180 { 330 if a2 > 180 { 331 return true 332 } 333 return a2 < a1 334 } 335 return a1 < a2 336 } 337 338 func sortPoints[T constraints.Float](position geom.Point[T], segments []Segment[T]) []endPoint[T] { 339 points := make([]endPoint[T], len(segments)*2) 340 pos := 0 341 for i, s := range segments { 342 points[pos].segmentIndex = i 343 points[pos].angle = angle(s.Start, position) 344 points[pos].start = true 345 pos++ 346 points[pos].segmentIndex = i 347 points[pos].angle = angle(s.End, position) 348 points[pos].start = false 349 pos++ 350 } 351 slices.SortFunc(points, func(a, b endPoint[T]) int { 352 if result := cmp.Compare(a.angle, b.angle); result != 0 { 353 return result 354 } 355 if result := cmp.Compare(distance(a.pt(segments), position), distance(b.pt(segments), position)); result != 0 { 356 return result 357 } 358 if a.start == b.start { 359 return 0 360 } 361 if a.start { 362 return 1 363 } 364 return -1 365 }) 366 return points 367 } 368 369 func angle2[T constraints.Float](a, b, c geom.Point[T]) T { 370 a1 := angle(a, b) 371 a2 := angle(b, c) 372 a3 := a1 - a2 373 if a3 < 0 { 374 a3 += 360 375 } 376 if a3 > 360 { 377 a3 -= 360 378 } 379 return a3 380 } 381 382 func angle[T constraints.Float](a, b geom.Point[T]) T { 383 return xmath.Atan2(b.Y-a.Y, b.X-a.X) * 180 / xmath.Pi 384 } 385 386 func distance[T constraints.Float](a, b geom.Point[T]) T { 387 dx := a.X - b.X 388 dy := a.Y - b.Y 389 return dx*dx + dy*dy 390 } 391 392 func intersectLines[T constraints.Float](s1, e1, s2, e2 geom.Point[T]) (geom.Point[T], bool) { 393 dbx := e2.X - s2.X 394 dby := e2.Y - s2.Y 395 dax := e1.X - s1.X 396 day := e1.Y - s1.Y 397 ub := dby*dax - dbx*day 398 if ub == 0 { 399 return geom.Point[T]{}, false 400 } 401 ua := (dbx*(s1.Y-s2.Y) - dby*(s1.X-s2.X)) / ub 402 return geom.Point[T]{X: s1.X + ua*dax, Y: s1.Y + ua*day}, true 403 } 404 405 func hasIntersection[T constraints.Float](s1, e1, s2, e2 geom.Point[T]) bool { 406 d1 := direction(s2, e2, s1) 407 d2 := direction(s2, e2, e1) 408 d3 := direction(s1, e1, s2) 409 d4 := direction(s1, e1, e2) 410 return (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && 411 ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) || 412 (d1 == 0 && onSegment(s2, e2, s1)) || 413 (d2 == 0 && onSegment(s2, e2, e1)) || 414 (d3 == 0 && onSegment(s1, e1, s2)) || 415 (d4 == 0 && onSegment(s1, e1, e2)) 416 } 417 418 func direction[T constraints.Float](a, b, c geom.Point[T]) int { 419 return cmp.Compare((c.X-a.X)*(b.Y-a.Y), (b.X-a.X)*(c.Y-a.Y)) 420 } 421 422 func onSegment[T constraints.Float](a, b, c geom.Point[T]) bool { 423 return (a.X <= c.X || b.X <= c.X) && 424 (c.X <= a.X || c.X <= b.X) && 425 (a.Y <= c.Y || b.Y <= c.Y) && 426 (c.Y <= a.Y || c.Y <= b.Y) 427 }