mirror of
https://github.com/zhigang1992/PerspectiveTransform.git
synced 2026-01-12 22:49:33 +08:00
227 lines
9.5 KiB
Swift
227 lines
9.5 KiB
Swift
import Quick
|
|
import Nimble
|
|
import simd
|
|
import GameKit
|
|
|
|
@testable import PerspectiveTransform
|
|
|
|
class ProjectionSpec: QuickSpec {
|
|
override func spec() {
|
|
describe("general2DProjection") {
|
|
context("2 random Quadrilateral") {
|
|
let source = GKRandomSource.sharedRandom()
|
|
var a: Perspective!
|
|
var b: Perspective!
|
|
|
|
beforeEach {
|
|
a = Perspective(source.nextFourPoints())
|
|
b = Perspective(source.nextFourPoints())
|
|
}
|
|
|
|
context("reverse projection") {
|
|
it("should be invert of original") {
|
|
let forward = a.projection(to: b)
|
|
let reverse = b.projection(to: a)
|
|
expect(forward.inverse) ≈ (reverse, 0.0001)
|
|
}
|
|
}
|
|
}
|
|
context("isosceles trapezoid") {
|
|
var from: Perspective!
|
|
var to: Perspective!
|
|
|
|
beforeEach {
|
|
from = Perspective(CGRect(origin: CGPoint.zero, size: CGSize(width: 1, height: 1)))
|
|
to = Perspective(
|
|
CGPoint(x: -1, y: 0), // shifted left by 1
|
|
CGPoint(x: 2, y: 0), // shifted right by 1
|
|
CGPoint(x: 1, y: 1),
|
|
CGPoint(x: 0, y: 1)
|
|
)
|
|
}
|
|
|
|
it("should be non affine and have 3D rotation") {
|
|
let transform = from.projectiveTransform(destination: to)
|
|
expect(CATransform3DIsIdentity(transform)) == false
|
|
expect(CATransform3DIsAffine(transform)) == false
|
|
}
|
|
}
|
|
context("to the same perspective") {
|
|
var from: Perspective!
|
|
var to: Perspective!
|
|
|
|
beforeEach {
|
|
from = Perspective(CGRect(origin: CGPoint.zero, size: CGSize(width: 10, height: 10)))
|
|
to = from
|
|
}
|
|
|
|
it("should be identity") {
|
|
let projection = from.projection(to: to)
|
|
let transform3D = CATransform3D(projection.to3d())
|
|
expect(CATransform3DIsIdentity(transform3D)) == true
|
|
|
|
expect(transform3D.component(for: .scale)) == Vector3(1, 1, 1)
|
|
expect(transform3D.component(for: .rotation)) == Vector3(0, 0, 0)
|
|
expect(transform3D.component(for: .translation)) == Vector3(0, 0, 0)
|
|
}
|
|
}
|
|
|
|
context("rotation") {
|
|
// Image showing 3D transform matrix values
|
|
// https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/Art/transform_manipulations_2x.png
|
|
var from: Perspective!
|
|
var to: Perspective!
|
|
|
|
beforeEach {
|
|
let points = Quadrilateral(CGRect(origin: CGPoint.zero, size: CGSize(width: 10, height: 10))).corners
|
|
from = Perspective(points)
|
|
let turnedRight = [points[1], points[3], points[0], points[2]]
|
|
to = Perspective(turnedRight)
|
|
}
|
|
|
|
it("should match rotate around 0,0 and translate back to 0") {
|
|
let rotate3D = CATransform3DMakeRotation(.pi / 2, 0, 0, 1)
|
|
let translate3D = CATransform3DMakeTranslation(10, 0, 0)
|
|
let combined = CATransform3DConcat(rotate3D, translate3D)
|
|
|
|
let projection = from.projection(to: to)
|
|
expect(CATransform3D(projection.to3d())) == combined
|
|
}
|
|
|
|
context("matrix values") {
|
|
var matrix: CATransform3D!
|
|
|
|
beforeEach {
|
|
let projection = from.projection(to: to)
|
|
matrix = CATransform3D(projection.to3d())
|
|
}
|
|
|
|
it("should contain rotation around z axis") {
|
|
let angle = CGFloat.pi / 2
|
|
expect(matrix.m11) ≈ cos(angle)
|
|
expect(matrix.m22) ≈ cos(angle)
|
|
expect(matrix.m33) == 1
|
|
expect(matrix.m12) ≈ sin(angle)
|
|
expect(matrix.m21) ≈ -sin(angle)
|
|
expect(matrix.m13) == 0
|
|
expect(matrix.m31) == 0
|
|
}
|
|
|
|
it("should have key path components of rotation and translation") {
|
|
expect(matrix.component(for: .scale)) == Vector3(1, 1, 1)
|
|
expect(matrix.component(for: .rotation)) == Vector3(0, 0, Double.pi / 2)
|
|
expect(matrix.component(for: .translation)) == Vector3(10, 0, 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
context("scale and translate") {
|
|
let expected = Matrix3x3([200.0/152, 0, 100,
|
|
0, 200.0/122, 100,
|
|
0, 0, 1])
|
|
var start: Perspective!
|
|
var destination: Perspective!
|
|
|
|
beforeEach {
|
|
start = Perspective(CGRect(origin: CGPoint.zero, size: CGSize(width: 152, height: 122)))
|
|
destination = Perspective(
|
|
CGRect(
|
|
origin: CGPoint(x: 100, y: 100),
|
|
size: CGSize(width: 200, height: 200)
|
|
)
|
|
)
|
|
}
|
|
|
|
it("should match expected") {
|
|
let projection = start.projection(to: destination)
|
|
expect(projection) ≈ expected
|
|
}
|
|
|
|
context("reverse") {
|
|
it("should be invert of original") {
|
|
let projection = start.projection(to: destination)
|
|
let reverse = destination.projection(to: start)
|
|
expect(projection.inverse) ≈ (reverse, 0.001)
|
|
}
|
|
}
|
|
|
|
context("concat") {
|
|
var expected3D: CATransform3D!
|
|
var translate3D: CATransform3D!
|
|
var scale3D: CATransform3D!
|
|
|
|
beforeEach {
|
|
expected3D = CATransform3D(expected.to3d())
|
|
translate3D = CATransform3DMakeTranslation(100, 100, 0)
|
|
scale3D = CATransform3DMakeScale(200.0/152, 200.0/122, 1)
|
|
}
|
|
|
|
it("should create 3D transformation scale + translate, in that order only") {
|
|
expect(CATransform3DConcat(scale3D, translate3D)) == expected3D
|
|
expect(CATransform3DConcat(translate3D, scale3D)) != expected3D
|
|
}
|
|
|
|
context("expected 3d matrix") {
|
|
it("should contain scale values on the diagonal") {
|
|
[expected3D, scale3D].forEach { transform3d in
|
|
let matrix = transform3d!
|
|
expect(matrix.m11) == 200.0/152
|
|
expect(matrix.m22) == 200.0/122
|
|
expect(matrix.m33) == 1
|
|
expect(matrix.m44) == 1
|
|
}
|
|
}
|
|
|
|
it("should contain translate values in row 4") {
|
|
[expected3D, translate3D].forEach { transform3d in
|
|
let matrix = transform3d!
|
|
expect(matrix.m41) == 100
|
|
expect(matrix.m42) == 100
|
|
expect(matrix.m43) == 0
|
|
expect(matrix.m44) == 1
|
|
}
|
|
}
|
|
|
|
context("without scale and translate") {
|
|
var matrix: CATransform3D!
|
|
|
|
beforeEach {
|
|
matrix = expected3D!
|
|
matrix.m11 = 1
|
|
matrix.m22 = 1
|
|
matrix.m41 = 0
|
|
matrix.m42 = 0
|
|
}
|
|
|
|
it("should have no other value and thus be identity") {
|
|
expect(CATransform3DIsIdentity(matrix)) == true
|
|
}
|
|
}
|
|
}
|
|
|
|
context("affine 2D") {
|
|
var scale2D: CGAffineTransform!
|
|
var translate2D: CGAffineTransform!
|
|
var expected2D: CGAffineTransform!
|
|
|
|
beforeEach {
|
|
scale2D = CATransform3DGetAffineTransform(scale3D)
|
|
translate2D = CATransform3DGetAffineTransform(translate3D)
|
|
expected2D = CATransform3DGetAffineTransform(expected3D)
|
|
}
|
|
|
|
it("should all be affine") {
|
|
let isAffine = [scale3D, translate3D, expected3D].map {CATransform3DIsAffine($0)}
|
|
expect(isAffine).to(allPass(beTrue()))
|
|
}
|
|
|
|
it("should create 2D affine tranformation") {
|
|
expect(scale2D.concatenating(translate2D)) == expected2D
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|