Ilya Laktyushin 02e06779ef Drawing
2022-12-03 21:57:32 +04:00

309 lines
9.6 KiB
Swift

import Foundation
private func intersect(seg1: [CGPoint], seg2: [CGPoint]) -> Bool {
func ccw(_ seg1: CGPoint, _ seg2: CGPoint, _ seg3: CGPoint) -> Bool {
let ccw = ((seg3.y - seg1.y) * (seg2.x - seg1.x)) - ((seg2.y - seg1.y) * (seg3.x - seg1.x))
return ccw > 0 ? true : ccw < 0 ? false : true
}
let segment1 = seg1[0]
let segment2 = seg1[1]
let segment3 = seg2[0]
let segment4 = seg2[1]
return ccw(segment1, segment3, segment4) != ccw(segment2, segment3, segment4)
&& ccw(segment1, segment2, segment3) != ccw(segment1, segment2, segment4)
}
private func convex(points: [CGPoint]) -> [CGPoint] {
func cross(_ o: CGPoint, _ a: CGPoint, _ b: CGPoint) -> Double {
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
}
func upperTangent(_ points: [CGPoint]) -> [CGPoint] {
var lower: [CGPoint] = []
for point in points {
while lower.count >= 2 && (cross(lower[lower.count - 2], lower[lower.count - 1], point) <= 0) {
_ = lower.popLast()
}
lower.append(point)
}
_ = lower.popLast()
return lower
}
func lowerTangent(_ points: [CGPoint]) -> [CGPoint] {
let reversed = points.reversed()
var upper: [CGPoint] = []
for point in reversed {
while upper.count >= 2 && (cross(upper[upper.count - 2], upper[upper.count - 1], point) <= 0) {
_ = upper.popLast()
}
upper.append(point)
}
_ = upper.popLast()
return upper
}
var convex: [CGPoint] = []
convex.append(contentsOf: upperTangent(points))
convex.append(contentsOf: lowerTangent(points))
return convex
}
private class Grid {
var cells = [Int: [Int: [CGPoint]]]()
var cellSize: Double = 0
init(_ points: [CGPoint], _ cellSize: Double) {
self.cellSize = cellSize
for point in points {
let cellXY = point2CellXY(point)
let x = cellXY[0]
let y = cellXY[1]
if self.cells[x] == nil {
self.cells[x] = [Int: [CGPoint]]()
}
if self.cells[x]![y] == nil {
self.cells[x]![y] = [CGPoint]()
}
self.cells[x]![y]!.append(point)
}
}
func point2CellXY(_ point: CGPoint) -> [Int] {
let x = Int(point.x / self.cellSize)
let y = Int(point.y / self.cellSize)
return [x, y]
}
func extendBbox(_ bbox: [Double], _ scaleFactor: Double) -> [Double] {
return [
bbox[0] - (scaleFactor * self.cellSize),
bbox[1] - (scaleFactor * self.cellSize),
bbox[2] + (scaleFactor * self.cellSize),
bbox[3] + (scaleFactor * self.cellSize)
]
}
func removePoint(_ point: CGPoint) {
let cellXY = point2CellXY(point)
let cell = self.cells[cellXY[0]]![cellXY[1]]!
var pointIdxInCell = 0
for idx in 0 ..< cell.count {
if cell[idx].x == point.x && cell[idx].y == point.y {
pointIdxInCell = idx
break
}
}
self.cells[cellXY[0]]![cellXY[1]]!.remove(at: pointIdxInCell)
}
func rangePoints(_ bbox: [Double]) -> [CGPoint] {
let tlCellXY = point2CellXY(CGPoint(x: bbox[0], y: bbox[1]))
let brCellXY = point2CellXY(CGPoint(x: bbox[2], y: bbox[3]))
var points: [CGPoint] = []
for x in tlCellXY[0]..<brCellXY[0]+1 {
for y in tlCellXY[1]..<brCellXY[1]+1 {
points += cellPoints(x, y)
}
}
return points
}
func cellPoints(_ xAbs: Int, _ yOrd: Int) -> [CGPoint] {
if let x = self.cells[xAbs] {
if let y = x[yOrd] {
return y
}
}
return []
}
}
private let maxConcaveAngleCos = cos(90.0 / (180.0 / Double.pi))
private func filterDuplicates(_ pointSet: [CGPoint]) -> [CGPoint] {
let sortedSet = sortByX(pointSet)
return sortedSet.filter { (point: CGPoint) -> Bool in
let index = pointSet.firstIndex(where: {(idx: CGPoint) -> Bool in
return idx.x == point.x && idx.y == point.y
})
if index == 0 {
return true
} else {
let prevEl = pointSet[index! - 1]
if prevEl.x != point.x || prevEl.y != point.y {
return true
}
return false
}
}
}
private func sortByX(_ pointSet: [CGPoint]) -> [CGPoint] {
return pointSet.sorted(by: { (lhs, rhs) -> Bool in
if lhs.x == rhs.x {
return lhs.y < rhs.y
} else {
return lhs.x < rhs.x
}
})
}
private func squaredLength(_ a: CGPoint, _ b: CGPoint) -> Double {
return pow(b.x - a.x, 2) + pow(b.y - a.y, 2)
}
private func cosFunc(_ o: CGPoint, _ a: CGPoint, _ b: CGPoint) -> Double {
let aShifted = [a.x - o.x, a.y - o.y]
let bShifted = [b.x - o.x, b.y - o.y]
let sqALen = squaredLength(o, a)
let sqBLen = squaredLength(o, b)
let dot = aShifted[0] * bShifted[0] + aShifted[1] * bShifted[1]
return dot / sqrt(sqALen * sqBLen)
}
private func intersectFunc(_ segment: [CGPoint], _ pointSet: [CGPoint]) -> Bool {
for idx in 0..<pointSet.count - 1 {
let seg = [pointSet[idx], pointSet[idx + 1]]
if segment[0].x == seg[0].x && segment[0].y == seg[0].y ||
segment[0].x == seg[1].x && segment[0].y == seg[1].y {
continue
}
if intersect(seg1: segment, seg2: seg) {
return true
}
}
return false
}
private func occupiedAreaFunc(_ points: [CGPoint]) -> CGPoint {
var minX = Double.infinity
var minY = Double.infinity
var maxX = -Double.infinity
var maxY = -Double.infinity
for idx in 0 ..< points.reversed().count {
if points[idx].x < minX {
minX = points[idx].x
}
if points[idx].y < minY {
minY = points[idx].y
}
if points[idx].x > maxX {
maxX = points[idx].x
}
if points[idx].y > maxY {
maxY = points[idx].y
}
}
return CGPoint(x: maxX - minX, y: maxY - minY)
}
private func bBoxAroundFunc(_ edge: [CGPoint]) -> [Double] {
return [min(edge[0].x, edge[1].x),
min(edge[0].y, edge[1].y),
max(edge[0].x, edge[1].x),
max(edge[0].y, edge[1].y)]
}
private func midPointFunc(_ edge: [CGPoint], _ innerPoints: [CGPoint], _ convex: [CGPoint]) -> CGPoint? {
var point: CGPoint?
var angle1Cos = maxConcaveAngleCos
var angle2Cos = maxConcaveAngleCos
var a1Cos: Double = 0
var a2Cos: Double = 0
for innerPoint in innerPoints {
a1Cos = cosFunc(edge[0], edge[1], innerPoint)
a2Cos = cosFunc(edge[1], edge[0], innerPoint)
if a1Cos > angle1Cos &&
a2Cos > angle2Cos &&
!intersectFunc([edge[0], innerPoint], convex) &&
!intersectFunc([edge[1], innerPoint], convex) {
angle1Cos = a1Cos
angle2Cos = a2Cos
point = innerPoint
}
}
return point
}
private func concaveFunc(_ convex: inout [CGPoint], _ maxSqEdgeLen: Double, _ maxSearchArea: [Double], _ grid: Grid, _ edgeSkipList: inout [String: Bool]) -> [CGPoint] {
var edge: [CGPoint]
var keyInSkipList: String = ""
var scaleFactor: Double
var midPoint: CGPoint?
var bBoxAround: [Double]
var bBoxWidth: Double = 0
var bBoxHeight: Double = 0
var midPointInserted: Bool = false
for idx in 0..<convex.count - 1 {
edge = [convex[idx], convex[idx+1]]
keyInSkipList = edge[0].key.appending(", ").appending(edge[1].key)
scaleFactor = 0
bBoxAround = bBoxAroundFunc(edge)
if squaredLength(edge[0], edge[1]) < maxSqEdgeLen || edgeSkipList[keyInSkipList] == true {
continue
}
repeat {
bBoxAround = grid.extendBbox(bBoxAround, scaleFactor)
bBoxWidth = bBoxAround[2] - bBoxAround[0]
bBoxHeight = bBoxAround[3] - bBoxAround[1]
midPoint = midPointFunc(edge, grid.rangePoints(bBoxAround), convex)
scaleFactor += 1
} while midPoint == nil && (maxSearchArea[0] > bBoxWidth || maxSearchArea[1] > bBoxHeight)
if bBoxWidth >= maxSearchArea[0] && bBoxHeight >= maxSearchArea[1] {
edgeSkipList[keyInSkipList] = true
}
if let midPoint = midPoint {
convex.insert(midPoint, at: idx + 1)
grid.removePoint(midPoint)
midPointInserted = true
}
}
if midPointInserted {
return concaveFunc(&convex, maxSqEdgeLen, maxSearchArea, grid, &edgeSkipList)
}
return convex
}
private extension CGPoint {
var key: String {
return "\(self.x),\(self.y)"
}
}
func getHull(_ points: [CGPoint], concavity: Double) -> [CGPoint] {
let points = filterDuplicates(points)
let occupiedArea = occupiedAreaFunc(points)
let maxSearchArea: [Double] = [
occupiedArea.x * 0.6,
occupiedArea.y * 0.6
]
var convex = convex(points: points)
var innerPoints = points.filter { (point: CGPoint) -> Bool in
let idx = convex.firstIndex(where: { (idx: CGPoint) -> Bool in
return idx.x == point.x && idx.y == point.y
})
return idx == nil
}
innerPoints.sort(by: { (lhs: CGPoint, rhs: CGPoint) -> Bool in
return lhs.x == rhs.x ? lhs.y > rhs.y : lhs.x > rhs.x
})
let cellSize = ceil(occupiedArea.x * occupiedArea.y / Double(points.count))
let grid = Grid(innerPoints, cellSize)
var skipList: [String: Bool] = [String: Bool]()
return concaveFunc(&convex, pow(concavity, 2), maxSearchArea, grid, &skipList)
}