Merge e9446269c5
into 687baee97f
This commit is contained in:
commit
3f55c423f2
|
@ -22,6 +22,7 @@ extension CGPoint {
|
|||
func rotate(_ angle: Angle, around origin: Self) -> Self {
|
||||
let cosAngle = CGFloat(cos(angle.radians))
|
||||
let sinAngle = CGFloat(sin(angle.radians))
|
||||
|
||||
return .init(
|
||||
x: cosAngle * (x - origin.x) - sinAngle * (y - origin.y) + origin.x,
|
||||
y: sinAngle * (x - origin.x) + cosAngle * (y - origin.y) + origin.y
|
||||
|
@ -36,16 +37,6 @@ extension CGPoint {
|
|||
}
|
||||
}
|
||||
|
||||
public extension CGAffineTransform {
|
||||
/// Transform the point into the transform's coordinate system.
|
||||
func transform(point: CGPoint) -> CGPoint {
|
||||
CGPoint(
|
||||
x: (a * point.x) + (c * point.y) + tx,
|
||||
y: (b * point.x) + (d * point.y) + ty
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#if !canImport(CoreGraphics)
|
||||
public enum CGLineCap {
|
||||
/// A line with a squared-off end. Extends to the endpoint of the Path.
|
||||
|
@ -63,149 +54,248 @@ public enum CGLineJoin {
|
|||
/// A join with a squared-off end. Extends past the endpoint of the Path.
|
||||
case bevel
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !canImport(CoreGraphics)
|
||||
// For cross-platform testing.
|
||||
@_spi(Tests)
|
||||
public struct _CGAffineTransform {
|
||||
// Internal for testing purposes.
|
||||
internal let _transform: AffineTransform
|
||||
}
|
||||
#else
|
||||
public struct _CGAffineTransform {
|
||||
// Internal for testing purposes.
|
||||
internal private(set) var _transform: AffineTransform
|
||||
}
|
||||
|
||||
/// An affine transformation matrix for use in drawing 2D graphics.
|
||||
///
|
||||
/// a b 0
|
||||
/// c d 0
|
||||
/// tx ty 1
|
||||
public struct CGAffineTransform: Equatable {
|
||||
public var a: CGFloat
|
||||
public var b: CGFloat
|
||||
public var c: CGFloat
|
||||
public var d: CGFloat
|
||||
public var tx: CGFloat
|
||||
public var ty: CGFloat
|
||||
public typealias CGAffineTransform = _CGAffineTransform
|
||||
#endif
|
||||
|
||||
/// The identity matrix
|
||||
public static let identity: Self = .init(
|
||||
a: 1,
|
||||
b: 0, // 0
|
||||
c: 0,
|
||||
d: 1, // 0
|
||||
tx: 0,
|
||||
ty: 0
|
||||
) // 1
|
||||
extension _CGAffineTransform: Equatable {}
|
||||
|
||||
public init(
|
||||
extension _CGAffineTransform: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
try self.init(
|
||||
_transform: AffineTransform(from: decoder)
|
||||
)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
try _transform.encode(to: encoder)
|
||||
}
|
||||
}
|
||||
|
||||
public extension _CGAffineTransform {
|
||||
/// The value at position [1,1] in the matrix.
|
||||
var a: CGFloat {
|
||||
get { _transform.m11 }
|
||||
set { _transform.m11 = newValue }
|
||||
}
|
||||
|
||||
/// The value at position [1,2] in the matrix.
|
||||
var b: CGFloat {
|
||||
get { _transform.m12 }
|
||||
set { _transform.m12 = newValue }
|
||||
}
|
||||
|
||||
/// The value at position [2,1] in the matrix.
|
||||
var c: CGFloat {
|
||||
get { _transform.m21 }
|
||||
set { _transform.m21 = newValue }
|
||||
}
|
||||
|
||||
/// The value at position [2,2] in the matrix.
|
||||
var d: CGFloat {
|
||||
get { _transform.m22 }
|
||||
set { _transform.m22 = newValue }
|
||||
}
|
||||
|
||||
/// The value at position [3,1] in the matrix.
|
||||
var tx: CGFloat {
|
||||
get { _transform.tX }
|
||||
set { _transform.tX = newValue }
|
||||
}
|
||||
|
||||
/// The value at position [3,2] in the matrix.
|
||||
var ty: CGFloat {
|
||||
get { _transform.tY }
|
||||
set { _transform.tY = newValue }
|
||||
}
|
||||
|
||||
/// Creates an affine transform with the given matrix values.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - a: The value at position [1,1] in the matrix.
|
||||
/// - b: The value at position [1,2] in the matrix.
|
||||
/// - c: The value at position [2,1] in the matrix.
|
||||
/// - d: The value at position [2,2] in the matrix.
|
||||
/// - tx: The value at position [3,1] in the matrix.
|
||||
/// - ty: The value at position [3,2] in the matrix.
|
||||
init(
|
||||
a: CGFloat, b: CGFloat,
|
||||
c: CGFloat, d: CGFloat,
|
||||
tx: CGFloat, ty: CGFloat
|
||||
) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
self.d = d
|
||||
self.tx = tx
|
||||
self.ty = ty
|
||||
self.init(_transform: AffineTransform(
|
||||
m11: a, m12: b,
|
||||
m21: c, m22: d,
|
||||
tX: tx, tY: ty
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
public extension _CGAffineTransform {
|
||||
/// The identity transformation matrix.
|
||||
static let identity = Self(_transform: .identity)
|
||||
|
||||
/// Creates the identity transformation matrix.
|
||||
init() {
|
||||
self.init(_transform: AffineTransform())
|
||||
}
|
||||
|
||||
/// Returns an affine transformation matrix constructed from a rotation value you provide.
|
||||
var isIdentity: Bool {
|
||||
self == .identity
|
||||
}
|
||||
}
|
||||
|
||||
public extension _CGAffineTransform {
|
||||
/// Creates an affine transformation matrix constructed from a rotation value you
|
||||
/// provide.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - angle: The angle, in radians, by which this matrix rotates the coordinate system axes.
|
||||
/// A positive value specifies clockwise rotation and anegative value specifies
|
||||
/// counterclockwise rotation.
|
||||
public init(rotationAngle angle: CGFloat) {
|
||||
self.init(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0)
|
||||
/// - angle: The angle, in radians, by which this matrix rotates the coordinate
|
||||
/// system axes. A positive value specifies clockwise rotation and a negative value
|
||||
/// specifies counterclockwise rotation.
|
||||
init(rotationAngle angle: CGFloat) {
|
||||
self.init(_transform: AffineTransform(rotationByRadians: angle))
|
||||
}
|
||||
|
||||
/// Returns an affine transformation matrix constructed from scaling values you provide.
|
||||
/// Creates an affine transformation matrix constructed from scaling values you provide.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - sx: The factor by which to scale the x-axis of the coordinate system.
|
||||
/// - sy: The factor by which to scale the y-axis of the coordinate system.
|
||||
public init(scaleX sx: CGFloat, y sy: CGFloat) {
|
||||
self.init(
|
||||
a: sx,
|
||||
b: 0,
|
||||
c: 0,
|
||||
d: sy,
|
||||
tx: 0,
|
||||
ty: 0
|
||||
)
|
||||
init(scaleX x: CGFloat, y: CGFloat) {
|
||||
self.init(_transform: AffineTransform(scaleByX: x, byY: y))
|
||||
}
|
||||
|
||||
/// Returns an affine transformation matrix constructed from translation values you provide.
|
||||
/// Creates an affine transformation matrix constructed from translation values you
|
||||
/// provide.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - tx: The value by which to move the x-axis of the coordinate system.
|
||||
/// - ty: The value by which to move the y-axis of the coordinate system.
|
||||
public init(translationX tx: CGFloat, y ty: CGFloat) {
|
||||
self.init(
|
||||
a: 1,
|
||||
b: 0,
|
||||
c: 0,
|
||||
d: 1,
|
||||
tx: tx,
|
||||
ty: ty
|
||||
)
|
||||
init(translationX x: CGFloat, y: CGFloat) {
|
||||
self.init(_transform: AffineTransform(translationByX: x, byY: y))
|
||||
}
|
||||
}
|
||||
|
||||
public extension _CGAffineTransform {
|
||||
private func withBackingTransform(
|
||||
_ modify: (inout AffineTransform) -> ()
|
||||
) -> Self {
|
||||
var newTransform = _transform
|
||||
modify(&newTransform)
|
||||
|
||||
return _CGAffineTransform(_transform: newTransform)
|
||||
}
|
||||
}
|
||||
|
||||
public extension _CGAffineTransform {
|
||||
/// Returns an affine transformation matrix constructed by combining two existing affine
|
||||
/// transforms.
|
||||
///
|
||||
/// Note that concatenation is not commutative, meaning that order is important. For
|
||||
/// instance, `t1.concatenating(t2)` != `t2.concatenating(t1)` — where
|
||||
/// `t1` and `t2` are`CGAffineTransform` instances.
|
||||
///
|
||||
/// - Postcondition: The returned transformation is invertible if both `self` and
|
||||
/// the given transformation (`t2`) are invertible.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - t2: The affine transform to concatenate to this affine transform.
|
||||
/// - Returns: A new affine transformation matrix. That is, `t’ = t1*t2`.
|
||||
public func concatenating(_ t2: Self) -> Self {
|
||||
let t1m = [[a, b, 0],
|
||||
[c, d, 0],
|
||||
[tx, ty, 1]]
|
||||
let t2m = [[t2.a, t2.b, 0],
|
||||
[t2.c, t2.d, 0],
|
||||
[t2.tx, t2.ty, 1]]
|
||||
var res: [[CGFloat]] = [[0, 0, 0],
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]]
|
||||
for i in 0..<3 {
|
||||
for j in 0..<3 {
|
||||
res[i][j] = 0
|
||||
for k in 0..<3 {
|
||||
res[i][j] += t1m[i][k] * t2m[k][j]
|
||||
}
|
||||
func concatenating(_ t2: Self) -> Self {
|
||||
withBackingTransform { $0.append(t2._transform) }
|
||||
}
|
||||
}
|
||||
|
||||
public extension _CGAffineTransform {
|
||||
/// Returns an affine transformation matrix constructed by inverting an existing affine
|
||||
/// transform.
|
||||
///
|
||||
/// - Postcondition: Invertibility is preserved, meaning that if `self` is
|
||||
/// invertible, so the returned transformation will also be invertible.
|
||||
///
|
||||
/// - Returns: A new affine transformation matrix. If `self` is not invertible, it's
|
||||
/// returned unchanged.
|
||||
func inverted() -> Self {
|
||||
withBackingTransform { _transform in
|
||||
guard let inverted = _transform.inverted() else {
|
||||
fatalError("This affine transform is non invertible.")
|
||||
}
|
||||
|
||||
_transform = inverted
|
||||
}
|
||||
return .init(
|
||||
a: res[0][0],
|
||||
b: res[0][1],
|
||||
c: res[1][0],
|
||||
d: res[1][1],
|
||||
tx: res[2][0],
|
||||
ty: res[2][1]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an affine transformation matrix constructed by inverting an existing affine transform.
|
||||
public func inverted() -> Self {
|
||||
.init(a: -a, b: -b, c: -c, d: -d, tx: -tx, ty: -ty)
|
||||
}
|
||||
|
||||
/// Returns an affine transformation matrix constructed by rotating an existing affine transform.
|
||||
public extension _CGAffineTransform {
|
||||
/// Returns an affine transformation matrix constructed by rotating an existing affine
|
||||
/// transform.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - angle: The angle, in radians, by which to rotate the affine transform.
|
||||
/// A positive value specifies clockwise rotation and a negative value specifies
|
||||
/// counterclockwise rotation.
|
||||
public func rotated(by angle: CGFloat) -> Self {
|
||||
Self(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0)
|
||||
func rotated(by angle: CGFloat) -> Self {
|
||||
withBackingTransform { $0.rotate(byRadians: angle) }
|
||||
}
|
||||
|
||||
/// Returns an affine transformation matrix constructed by scaling an existing affine
|
||||
/// transform.
|
||||
///
|
||||
/// - Postcondition: Invertibility is preserved if both `sx` and `sy` aren't `0`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - sx: The value by which to scale x values of the affine transform.
|
||||
/// - sy: The value by which to scale y values of the affine transform.
|
||||
func scaledBy(x: CGFloat, y: CGFloat) -> Self {
|
||||
withBackingTransform { $0.scale(x: x, y: y) }
|
||||
}
|
||||
|
||||
/// Returns an affine transformation matrix constructed by translating an existing
|
||||
/// affine transform.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - tx: The value by which to move x values with the affine transform.
|
||||
/// - ty: The value by which to move y values with the affine transform.
|
||||
public func translatedBy(x tx: CGFloat, y ty: CGFloat) -> Self {
|
||||
.init(a: a, b: b, c: c, d: d, tx: self.tx + tx, ty: self.ty + ty)
|
||||
}
|
||||
|
||||
/// Returns an affine transformation matrix constructed by scaling an existing affine transform.
|
||||
/// - Parameters:
|
||||
/// - sx: The value by which to scale x values of the affine transform.
|
||||
/// - sy: The value by which to scale y values of the affine transform.
|
||||
public func scaledBy(x sx: CGFloat, y sy: CGFloat) -> Self {
|
||||
.init(a: a + sx, b: b, c: c, d: d + sy, tx: tx, ty: ty)
|
||||
}
|
||||
|
||||
public var isIdentity: Bool {
|
||||
self == Self.identity
|
||||
func translatedBy(x: CGFloat, y: CGFloat) -> Self {
|
||||
withBackingTransform { $0.translate(x: x, y: y) }
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
internal extension _CGAffineTransform {
|
||||
func _transform(point: CGPoint) -> CGPoint {
|
||||
_transform.transform(point)
|
||||
}
|
||||
}
|
||||
|
||||
public extension CGAffineTransform {
|
||||
/// Transform the point into the transform's coordinate system.
|
||||
func transform(point: CGPoint) -> CGPoint {
|
||||
let transform = _CGAffineTransform(
|
||||
a: a, b: b,
|
||||
c: c, d: d,
|
||||
tx: tx, ty: ty
|
||||
)
|
||||
|
||||
return transform._transform(point: point)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,523 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
@_spi(Tests)
|
||||
@testable import TokamakCore
|
||||
|
||||
import XCTest
|
||||
|
||||
typealias CGAffineTransform = TokamakCore._CGAffineTransform
|
||||
|
||||
// MARK: - Tests
|
||||
|
||||
final class CGAffineTransformTest: XCTestCase {
|
||||
private let accuracyThreshold = 0.001
|
||||
|
||||
static var allTests: [(String, (CGAffineTransformTest) -> () throws -> ())] {
|
||||
[
|
||||
("testConstruction", testConstruction),
|
||||
("testVectorTransformations", testVectorTransformations),
|
||||
("testIdentityConstruction", testIdentityConstruction),
|
||||
("testIdentity", testIdentity),
|
||||
("testTranslationConstruction", testTranslationConstruction),
|
||||
("testTranslation", testTranslation),
|
||||
("testScalingConstruction", testScalingConstruction),
|
||||
("testScaling", testScaling),
|
||||
("testRotationConstruction", testRotationConstruction),
|
||||
("testRotation", testRotation),
|
||||
("testTranslationScaling", testTranslationScaling),
|
||||
("testTranslationRotation", testTranslationRotation),
|
||||
("testScalingRotation", testScalingRotation),
|
||||
("testInversion", testInversion),
|
||||
("testConcatenation", testConcatenation),
|
||||
("testCoding", testCoding),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
extension CGAffineTransformTest {
|
||||
func check(
|
||||
point: CGPoint,
|
||||
withTransform transform: CGAffineTransform,
|
||||
mapsTo expectedPoint: CGPoint,
|
||||
_ message: String = "",
|
||||
file: StaticString = #file, line: UInt = #line
|
||||
) {
|
||||
let newPoint = transform.transform(point: point)
|
||||
|
||||
XCTAssertEqual(
|
||||
newPoint.x, expectedPoint.x,
|
||||
accuracy: accuracyThreshold,
|
||||
"Invalid x: \(message)",
|
||||
file: file, line: line
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
newPoint.y, expectedPoint.y,
|
||||
accuracy: accuracyThreshold,
|
||||
"Invalid y: \(message)",
|
||||
file: file, line: line
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Construction
|
||||
|
||||
extension CGAffineTransformTest {
|
||||
func testConstruction() {
|
||||
let transform = CGAffineTransform(
|
||||
a: 1, b: 2,
|
||||
c: 3, d: 4,
|
||||
tx: 5, ty: 6
|
||||
)
|
||||
|
||||
XCTAssertEqual(transform.a, 1)
|
||||
XCTAssertEqual(transform.b, 2)
|
||||
XCTAssertEqual(transform.c, 3)
|
||||
XCTAssertEqual(transform.d, 4)
|
||||
XCTAssertEqual(transform.tx, 5)
|
||||
XCTAssertEqual(transform.ty, 6)
|
||||
|
||||
let mutatedTransform: CGAffineTransform = {
|
||||
var copy = transform
|
||||
|
||||
copy.a = -1
|
||||
copy.b = -2
|
||||
copy.c = -3
|
||||
copy.d = -4
|
||||
copy.tx = -5
|
||||
copy.ty = -6
|
||||
|
||||
return copy
|
||||
}()
|
||||
|
||||
XCTAssertEqual(mutatedTransform.a, -1)
|
||||
XCTAssertEqual(mutatedTransform.b, -2)
|
||||
XCTAssertEqual(mutatedTransform.c, -3)
|
||||
XCTAssertEqual(mutatedTransform.d, -4)
|
||||
XCTAssertEqual(mutatedTransform.tx, -5)
|
||||
XCTAssertEqual(mutatedTransform.ty, -6)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Vector Transformations
|
||||
|
||||
extension CGAffineTransformTest {
|
||||
func testVectorTransformations() {
|
||||
// To transform a given point with coordinates x and y,
|
||||
// we do:
|
||||
//
|
||||
// [ m11 m12 0 ]
|
||||
// [ w' h' 1 ] = [ x y 1 ] * [ m21 m22 0 ]
|
||||
// [ tx ty 1 ]
|
||||
//
|
||||
// [ w' h' 1 ] = [ x*m11+y*m21+1*tX x*m12+y*m22+1*tY ]
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 20),
|
||||
withTransform: CGAffineTransform(
|
||||
a: 1, b: 2,
|
||||
c: 3, d: 4,
|
||||
tx: 5, ty: 6
|
||||
),
|
||||
|
||||
// [ px*m11+py*m21+tX px*m12+py*m22+tY ]
|
||||
// [ 10*1+20*3+5 10*2+20*4+6 ]
|
||||
// [ 75 106 ]
|
||||
mapsTo: CGPoint(x: 75, y: 106)
|
||||
)
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 5, y: 25),
|
||||
withTransform: CGAffineTransform(
|
||||
a: 5, b: 4,
|
||||
c: 3, d: 2,
|
||||
tx: 1, ty: 0
|
||||
),
|
||||
|
||||
// [ px*m11+py*m21+tX px*m12+py*m22+tY ]
|
||||
// [ 5*5+25*3+1 5*4+25*2+0 ]
|
||||
// [ 101 70 ]
|
||||
mapsTo: CGPoint(x: 101, y: 70)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Identity
|
||||
|
||||
extension CGAffineTransformTest {
|
||||
func testIdentityConstruction() {
|
||||
// Check that the transform matrix is the identity:
|
||||
// [ 1 0 0 ]
|
||||
// [ 0 1 0 ]
|
||||
// [ 0 0 1 ]
|
||||
let identity = CGAffineTransform(
|
||||
a: 1, b: 0,
|
||||
c: 0, d: 1,
|
||||
tx: 0, ty: 0
|
||||
)
|
||||
|
||||
XCTAssertEqual(CGAffineTransform(), identity)
|
||||
XCTAssertEqual(CGAffineTransform.identity, identity)
|
||||
}
|
||||
|
||||
func testIdentity() {
|
||||
check(
|
||||
point: CGPoint(x: 25, y: 10),
|
||||
withTransform: .identity,
|
||||
mapsTo: CGPoint(x: 25, y: 10)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Translation
|
||||
|
||||
extension CGAffineTransformTest {
|
||||
func testTranslationConstruction() {
|
||||
let translatedIdentity = CGAffineTransform.identity
|
||||
.translatedBy(x: 15, y: 20)
|
||||
|
||||
let translation = CGAffineTransform(
|
||||
translationX: 15, y: 20
|
||||
)
|
||||
|
||||
XCTAssertEqual(translatedIdentity, translation)
|
||||
}
|
||||
|
||||
func testTranslation() {
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 10),
|
||||
withTransform: CGAffineTransform(
|
||||
translationX: 0, y: 0
|
||||
),
|
||||
mapsTo: CGPoint(x: 10, y: 10)
|
||||
)
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 10),
|
||||
withTransform: CGAffineTransform(
|
||||
translationX: 0, y: 5
|
||||
),
|
||||
mapsTo: CGPoint(x: 10, y: 15)
|
||||
)
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 10),
|
||||
withTransform: CGAffineTransform(
|
||||
translationX: 5, y: 5
|
||||
),
|
||||
mapsTo: CGPoint(x: 15, y: 15)
|
||||
)
|
||||
|
||||
check(
|
||||
point: CGPoint(x: -2, y: -3),
|
||||
// Translate by 5
|
||||
withTransform: CGAffineTransform.identity
|
||||
.translatedBy(x: 2, y: 3)
|
||||
.translatedBy(x: 3, y: 2),
|
||||
mapsTo: CGPoint(x: 3, y: 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Scaling
|
||||
|
||||
extension CGAffineTransformTest {
|
||||
func testScalingConstruction() {
|
||||
let scaledIdentity = CGAffineTransform.identity
|
||||
.scaledBy(x: 15, y: 20)
|
||||
|
||||
let scaling = CGAffineTransform(
|
||||
scaleX: 15, y: 20
|
||||
)
|
||||
|
||||
XCTAssertEqual(scaledIdentity, scaling)
|
||||
}
|
||||
|
||||
func testScaling() {
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 10),
|
||||
withTransform: CGAffineTransform(
|
||||
scaleX: 1, y: 0
|
||||
),
|
||||
mapsTo: CGPoint(x: 10, y: 0)
|
||||
)
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 10),
|
||||
withTransform: CGAffineTransform(
|
||||
scaleX: 0.5, y: 1
|
||||
),
|
||||
mapsTo: CGPoint(x: 5, y: 10)
|
||||
)
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 10),
|
||||
withTransform: CGAffineTransform(
|
||||
scaleX: 0, y: 2
|
||||
),
|
||||
mapsTo: CGPoint(x: 0, y: 20)
|
||||
)
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 10),
|
||||
// Scale by (2, 0)
|
||||
withTransform: CGAffineTransform.identity
|
||||
.scaledBy(x: 4, y: 0)
|
||||
.scaledBy(x: 0.5, y: 1),
|
||||
mapsTo: CGPoint(x: 20, y: 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Rotation
|
||||
|
||||
extension CGAffineTransformTest {
|
||||
func testRotationConstruction() {
|
||||
let baseRotation = CGAffineTransform(rotationAngle: .pi)
|
||||
|
||||
let point = CGPoint(x: 10, y: 15)
|
||||
let expectedPoint = baseRotation.transform(point: point)
|
||||
|
||||
check(
|
||||
point: point,
|
||||
withTransform: .identity.rotated(by: .pi),
|
||||
mapsTo: expectedPoint,
|
||||
"Rotation operation on identity doesn't work as identity init."
|
||||
)
|
||||
}
|
||||
|
||||
func testRotation() {
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 15),
|
||||
withTransform: CGAffineTransform(rotationAngle: 0),
|
||||
mapsTo: CGPoint(x: 10, y: 15)
|
||||
)
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 15),
|
||||
// Rotate by 3*360º
|
||||
withTransform: CGAffineTransform(rotationAngle: .pi * 6),
|
||||
mapsTo: CGPoint(x: 10, y: 15)
|
||||
)
|
||||
|
||||
// Counter-clockwise rotation
|
||||
check(
|
||||
point: CGPoint(x: 15, y: 10),
|
||||
// Rotate by 90º
|
||||
withTransform: CGAffineTransform(rotationAngle: .pi / 2),
|
||||
mapsTo: CGPoint(x: -10, y: 15)
|
||||
)
|
||||
|
||||
// Clockwise rotation
|
||||
check(
|
||||
point: CGPoint(x: 15, y: 10),
|
||||
// Rotate by -90º
|
||||
withTransform: CGAffineTransform(rotationAngle: .pi / -2),
|
||||
mapsTo: CGPoint(x: 10, y: -15)
|
||||
)
|
||||
|
||||
// Reflect about origin
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 15),
|
||||
// Rotate by 180º
|
||||
withTransform: CGAffineTransform(rotationAngle: .pi),
|
||||
mapsTo: CGPoint(x: -10, y: -15)
|
||||
)
|
||||
|
||||
// Composed reflection about origin
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 15),
|
||||
// Rotate by 180º
|
||||
withTransform: CGAffineTransform.identity
|
||||
.rotated(by: .pi / 2)
|
||||
.rotated(by: .pi / 2),
|
||||
mapsTo: CGPoint(x: -10, y: -15)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Permutations
|
||||
|
||||
extension CGAffineTransformTest {
|
||||
func testTranslationScaling() {
|
||||
check(
|
||||
point: CGPoint(x: 1, y: 3),
|
||||
// Translate by (2, 0) then scale by (5, -5)
|
||||
withTransform: CGAffineTransform.identity
|
||||
.translatedBy(x: 2, y: 0)
|
||||
.scaledBy(x: 5, y: -5),
|
||||
mapsTo: CGPoint(x: 15, y: -15)
|
||||
)
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 3, y: 1),
|
||||
// Scale by (-5, 5) then scale by (0, 10)
|
||||
withTransform: CGAffineTransform.identity
|
||||
.scaledBy(x: -5, y: 5)
|
||||
.translatedBy(x: 0, y: 10),
|
||||
mapsTo: CGPoint(x: -15, y: 15)
|
||||
)
|
||||
}
|
||||
|
||||
func testTranslationRotation() {
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 10),
|
||||
// Translate by (20, -5) then rotate by 90º
|
||||
withTransform: CGAffineTransform.identity
|
||||
.translatedBy(x: 20, y: -5)
|
||||
.rotated(by: .pi / 2),
|
||||
mapsTo: CGPoint(x: -5, y: 30)
|
||||
)
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 10),
|
||||
// Rotate by 180º and then translate by (20, 15)
|
||||
withTransform: CGAffineTransform.identity
|
||||
.rotated(by: .pi)
|
||||
.translatedBy(x: 20, y: 15),
|
||||
mapsTo: CGPoint(x: 10, y: 5)
|
||||
)
|
||||
}
|
||||
|
||||
func testScalingRotation() {
|
||||
check(
|
||||
point: CGPoint(x: 20, y: 5),
|
||||
// Scale by (0.5, 3) then rotate by -90º
|
||||
withTransform: CGAffineTransform.identity
|
||||
.scaledBy(x: 0.5, y: 3)
|
||||
.rotated(by: .pi / -2),
|
||||
mapsTo: CGPoint(x: 15, y: -10)
|
||||
)
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 20, y: 5),
|
||||
// Rotate by -90º the scale by (0.5, 3)
|
||||
withTransform: CGAffineTransform.identity
|
||||
.rotated(by: .pi / -2)
|
||||
.scaledBy(x: 3, y: -0.5),
|
||||
mapsTo: CGPoint(x: 15, y: 10)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Inversion
|
||||
|
||||
extension CGAffineTransformTest {
|
||||
func testInversion() {
|
||||
let transforms = [
|
||||
CGAffineTransform(translationX: -30, y: 40),
|
||||
CGAffineTransform(rotationAngle: .pi / 4),
|
||||
CGAffineTransform(scaleX: 20, y: -10),
|
||||
]
|
||||
|
||||
let composeTransform: CGAffineTransform = {
|
||||
var transform = CGAffineTransform.identity
|
||||
|
||||
for component in transforms {
|
||||
transform = transform.concatenating(component)
|
||||
}
|
||||
|
||||
return transform
|
||||
}()
|
||||
|
||||
let recoveredIdentity: CGAffineTransform = {
|
||||
var transform = composeTransform
|
||||
|
||||
// Append inverse transformations in reverse order
|
||||
for component in transforms.reversed() {
|
||||
transform = transform.concatenating(
|
||||
component.inverted()
|
||||
)
|
||||
}
|
||||
|
||||
return transform
|
||||
}()
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 10),
|
||||
withTransform: recoveredIdentity,
|
||||
mapsTo: CGPoint(x: 10, y: 10)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Concatenation
|
||||
|
||||
extension CGAffineTransformTest {
|
||||
func testConcatenation() {
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 15),
|
||||
withTransform: CGAffineTransform.identity
|
||||
.concatenating(.identity),
|
||||
mapsTo: CGPoint(x: 10, y: 15)
|
||||
)
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 15),
|
||||
// Scale by 2 then translate by (10, 0)
|
||||
withTransform: CGAffineTransform(scaleX: 2, y: 2)
|
||||
.concatenating(CGAffineTransform(
|
||||
translationX: 10, y: 0
|
||||
)),
|
||||
mapsTo: CGPoint(x: 30, y: 30)
|
||||
)
|
||||
|
||||
check(
|
||||
point: CGPoint(x: 10, y: 15),
|
||||
// Translate by (10, 0) then scale by 2
|
||||
withTransform: CGAffineTransform(translationX: 10, y: 0)
|
||||
.concatenating(CGAffineTransform(scaleX: 2, y: 2)),
|
||||
mapsTo: CGPoint(x: 40, y: 30)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Coding
|
||||
|
||||
extension CGAffineTransformTest {
|
||||
func testCoding() throws {
|
||||
let transform = CGAffineTransform(
|
||||
a: 1, b: 2,
|
||||
c: 3, d: 4,
|
||||
tx: 5, ty: 6
|
||||
)
|
||||
|
||||
let encodedData = try JSONEncoder().encode(transform)
|
||||
|
||||
let encodedString = String(
|
||||
data: encodedData, encoding: .utf8
|
||||
)
|
||||
|
||||
let commaSeparatedNumbers = (1...6)
|
||||
.map(String.init)
|
||||
.joined(separator: ",")
|
||||
|
||||
XCTAssertEqual(
|
||||
encodedString, "[\(commaSeparatedNumbers)]",
|
||||
"Invalid coding representation"
|
||||
)
|
||||
|
||||
let recovered = try JSONDecoder().decode(
|
||||
CGAffineTransform.self, from: encodedData
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
transform, recovered,
|
||||
"Encoded and then decoded transform does not equal original"
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue