-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
173 additions
and
35 deletions.
There are no files selected for viewing
120 changes: 120 additions & 0 deletions
120
Modules/Capabilities/Geometry/Sources/MinimumAreaRectFinder.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
// Created by Geoff Pado on 6/17/24. | ||
// Copyright © 2024 Cocoatype, LLC. All rights reserved. | ||
|
||
import CoreGraphics | ||
import Foundation | ||
|
||
enum MinimumAreaRectFinder { | ||
static func minimumAreaShape(for points: [CGPoint]) -> Shape { | ||
let (rect, angle) = minimumAreaEnclosingRectangle(for: points)! | ||
let rotationTransform = CGAffineTransform(rotationAngle: angle) | ||
let translationTransform = CGAffineTransform(translationX: -rect.minX, y: -rect.maxY) | ||
let inverseTranslate = CGAffineTransform(translationX: rect.minX, y: rect.maxY) | ||
|
||
let finalTransform = translationTransform.concatenating(rotationTransform).concatenating(inverseTranslate) | ||
|
||
let points = [CGPoint(x: rect.minX, y: rect.maxY), CGPoint(x: rect.maxX, y: rect.maxY), CGPoint(x: rect.minX, y: rect.minY), CGPoint(x: rect.maxX, y: rect.minY)] | ||
let transformedPoints = points.map { $0.applying(finalTransform) } | ||
|
||
return Shape(bottomLeft: transformedPoints[0], bottomRight: transformedPoints[1], topLeft: transformedPoints[2], topRight: transformedPoints[3]) | ||
} | ||
|
||
private static func minimumAreaEnclosingRectangle(for points: [CGPoint]) -> (CGRect, Double)? { | ||
guard points.count > 2 else { return nil } | ||
|
||
var minArea = CGFloat.greatestFiniteMagnitude | ||
var bestRect = CGRect.zero | ||
var bestAngle = Double.zero | ||
|
||
for i in 0..<points.count { | ||
for j in i+1..<points.count { | ||
let p1 = points[i] | ||
let p2 = points[j] | ||
let angle = atan2(p2.y - p1.y, p2.x - p1.x) | ||
let rotatedPoints = points.map { rotate(point: $0, around: p1, by: -angle) } | ||
let rect = boundingRect(for: rotatedPoints) | ||
let area = rect.width * rect.height | ||
if area < minArea { | ||
minArea = area | ||
bestRect = rect | ||
bestAngle = angle | ||
} | ||
} | ||
} | ||
|
||
return (bestRect, bestAngle) | ||
} | ||
|
||
private static func rotate(point: CGPoint, around origin: CGPoint, by angle: CGFloat) -> CGPoint { | ||
let translatedX = point.x - origin.x | ||
let translatedY = point.y - origin.y | ||
let rotatedX = translatedX * cos(angle) - translatedY * sin(angle) | ||
let rotatedY = translatedX * sin(angle) + translatedY * cos(angle) | ||
return CGPoint(x: rotatedX + origin.x, y: rotatedY + origin.y) | ||
} | ||
|
||
private static func boundingRect(for points: [CGPoint]) -> CGRect { | ||
let minX = points.map { $0.x }.min() ?? 0 | ||
let maxX = points.map { $0.x }.max() ?? 0 | ||
let minY = points.map { $0.y }.min() ?? 0 | ||
let maxY = points.map { $0.y }.max() ?? 0 | ||
return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) | ||
} | ||
|
||
// Helper function to find the orientation of the triplet (p, q, r) | ||
// 0 -> p, q and r are collinear | ||
// 1 -> Clockwise | ||
// 2 -> Counterclockwise | ||
private static func orientation(_ p: CGPoint, _ q: CGPoint, _ r: CGPoint) -> Int { | ||
let val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y) | ||
if val == 0 { return 0 } | ||
return (val > 0) ? 1 : 2 | ||
} | ||
|
||
// Helper function to find the square of the distance between two points | ||
private static func distanceSquared(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat { | ||
return (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y) | ||
} | ||
|
||
// Function to find the convex hull using Graham Scan algorithm | ||
static func convexHull(for points: [CGPoint]) -> [CGPoint] { | ||
guard points.count >= 3 else { return points } | ||
|
||
// Find the point with the lowest y-coordinate, break ties by x-coordinate | ||
var minYPoint = points[0] | ||
var minYIndex = 0 | ||
for (index, point) in points.enumerated() { | ||
if (point.y < minYPoint.y) || (point.y == minYPoint.y && point.x < minYPoint.x) { | ||
minYPoint = point | ||
minYIndex = index | ||
} | ||
} | ||
|
||
// Place the bottom-most point at the first position | ||
var sortedPoints = points | ||
sortedPoints.swapAt(0, minYIndex) | ||
|
||
// Sort the remaining points based on the polar angle with the first point | ||
let p0 = sortedPoints[0] | ||
sortedPoints = sortedPoints.sorted { (p1, p2) -> Bool in | ||
let o = orientation(p0, p1, p2) | ||
if o == 0 { | ||
return distanceSquared(p0, p1) < distanceSquared(p0, p2) | ||
} | ||
return o == 2 | ||
} | ||
|
||
// Create an empty stack and push the first three points to it | ||
var stack: [CGPoint] = [sortedPoints[0], sortedPoints[1], sortedPoints[2]] | ||
|
||
// Process the remaining points | ||
for i in 3..<sortedPoints.count { | ||
while stack.count > 1 && orientation(stack[stack.count - 2], stack.last!, sortedPoints[i]) != 2 { | ||
stack.removeLast() | ||
} | ||
stack.append(sortedPoints[i]) | ||
} | ||
|
||
return stack | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 4 additions & 2 deletions
6
...iting/Sources/Editing View/Observation View/Debug/PhotoEditingObservationDebugLayer.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters