From 6e8f8e585bfad4542df870f2eb2072b2b88df93e Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Fri, 23 Jul 2021 08:52:10 +0300 Subject: [PATCH 01/11] Fix `rotate(byRadians:)` implementation. --- Sources/Foundation/AffineTransform.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Foundation/AffineTransform.swift b/Sources/Foundation/AffineTransform.swift index 425c1fc330..420e2f8741 100644 --- a/Sources/Foundation/AffineTransform.swift +++ b/Sources/Foundation/AffineTransform.swift @@ -148,10 +148,10 @@ public struct AffineTransform : ReferenceConvertible, Hashable, CustomStringConv let sine = sin(angle) let cosine = cos(angle) - m11 = cosine - m12 = sine - m21 = -sine - m22 = cosine + m11 = cosine*m11 + sine*m21 + m12 = cosine*m12 + sine * m22 + m21 = -sine*m11 + cosine*m21 + m22 = -sine*m12 + cosine*m22 } /** From 0a3112a3801b246f7a2f5ada858ef46f70d77dc1 Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Sat, 28 Aug 2021 01:22:30 +0300 Subject: [PATCH 02/11] Restructure AffineTransform. --- Sources/Foundation/AffineTransform.swift | 705 ++++++++++++----------- 1 file changed, 366 insertions(+), 339 deletions(-) diff --git a/Sources/Foundation/AffineTransform.swift b/Sources/Foundation/AffineTransform.swift index 420e2f8741..8be7293e33 100644 --- a/Sources/Foundation/AffineTransform.swift +++ b/Sources/Foundation/AffineTransform.swift @@ -7,17 +7,14 @@ // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // -private let ε: CGFloat = CGFloat(2.22045e-16) - - -/** - AffineTransform represents an affine transformation matrix of the following form: - - [ m11 m12 0 ] - [ m21 m22 0 ] - [ tX tY 1 ] - */ -public struct AffineTransform : ReferenceConvertible, Hashable, CustomStringConvertible { +/// AffineTransform represents an affine transformation matrix of the following form: +/// +/// ```swift +/// [ m11 m12 0 ] +/// [ m21 m22 0 ] +/// [ tX tY 1 ] +/// ``` +public struct AffineTransform: ReferenceConvertible { public typealias ReferenceType = NSAffineTransform public var m11: CGFloat @@ -27,147 +24,131 @@ public struct AffineTransform : ReferenceConvertible, Hashable, CustomStringConv public var tX: CGFloat public var tY: CGFloat - /// Creates an affine transformation matrix with identity values. - public init() { - self.init(m11: 1.0, m12: 0.0, - m21: 0.0, m22: 1.0, - tX: 0.0, tY: 0.0) - } - /// Creates an affine transformation. - public init(m11: CGFloat, m12: CGFloat, m21: CGFloat, m22: CGFloat, tX: CGFloat, tY: CGFloat) { - (self.m11, self.m12, self.m21, self.m22) = (m11, m12, m21, m22) - (self.tX, self.tY) = (tX, tY) + public init( + m11: CGFloat, m12: CGFloat, + m21: CGFloat, m22: CGFloat, + tX: CGFloat, tY: CGFloat + ) { + self.m11 = m11 + self.m12 = m12 + self.m21 = m21 + self.m22 = m22 + self.tX = tX + self.tY = tY } +} - /** - Creates an affine transformation matrix from translation values. - The matrix takes the following form: - - [ 1 0 0 ] - [ 0 1 0 ] - [ x y 1 ] - */ - public init(translationByX x: CGFloat, byY y: CGFloat) { - self.init(m11: CGFloat(1.0), m12: CGFloat(0.0), - m21: CGFloat(0.0), m22: CGFloat(1.0), - tX: x, tY: y) +extension AffineTransform { + /// Creates an affine transformation matrix with identity values. + public init() { + self.init(m11: 1, m12: 0, + m21: 0, m22: 1, + tX: 0, tY: 0) } + + /// An identity affine transformation matrix + /// + /// ```swift + /// [ 1 0 0 ] + /// [ 0 1 0 ] + /// [ 0 0 1 ] + /// ``` + public static let identity = AffineTransform() +} - /** - Creates an affine transformation matrix from scaling values. - The matrix takes the following form: - - [ x 0 0 ] - [ 0 y 0 ] - [ 0 0 1 ] - */ +extension AffineTransform { + /// Creates an affine transformation matrix from translation values. + /// The matrix takes the following form: + /// + /// ```swift + /// [ 1 0 0 ] + /// [ 0 1 0 ] + /// [ x y 1 ] + /// ``` + public init(translationByX x: CGFloat, byY y: CGFloat) { + self.init(m11: 1, m12: 0, + m21: 0, m22: 1, + tX: x, tY: y) + } + + /// Creates an affine transformation matrix from scaling values. + /// The matrix takes the following form: + /// + /// ```swift + /// [ x 0 0 ] + /// [ 0 y 0 ] + /// [ 0 0 1 ] + /// ``` public init(scaleByX x: CGFloat, byY y: CGFloat) { - self.init(m11: x, m12: CGFloat(0.0), - m21: CGFloat(0.0), m22: y, - tX: CGFloat(0.0), tY: CGFloat(0.0)) - } - - /** - Creates an affine transformation matrix from scaling a single value. - The matrix takes the following form: - - [ f 0 0 ] - [ 0 f 0 ] - [ 0 0 1 ] - */ + self.init(m11: x, m12: 0, + m21: 0, m22: y, + tX: 0, tY: 0) + } + + /// Creates an affine transformation matrix from scaling a single value. + /// The matrix takes the following form: + /// + /// ```swift + /// [ f 0 0 ] + /// [ 0 f 0 ] + /// [ 0 0 1 ] + /// ``` public init(scale factor: CGFloat) { self.init(scaleByX: factor, byY: factor) } - /** - Creates an affine transformation matrix from rotation value (angle in radians). - The matrix takes the following form: - - [ cos α sin α 0 ] - [ -sin α cos α 0 ] - [ 0 0 1 ] - */ + /// Creates an affine transformation matrix from rotation value (angle in radians). + /// The matrix takes the following form: + /// + /// ```swift + /// [ cos α sin α 0 ] + /// [ -sin α cos α 0 ] + /// [ 0 0 1 ] + /// ``` public init(rotationByRadians angle: CGFloat) { - let sine = sin(angle) - let cosine = cos(angle) + let sinα = sin(angle) + let cosα = cos(angle) - self.init(m11: cosine, m12: sine, m21: -sine, m22: cosine, tX: CGFloat(0.0), tY: CGFloat(0.0)) + self.init( + m11: cosα, m12: sinα, + m21: -sinα, m22: cosα, + tX: 0, tY: 0 + ) } - /** - Creates an affine transformation matrix from a rotation value (angle in degrees). - The matrix takes the following form: - - [ cos α sin α 0 ] - [ -sin α cos α 0 ] - [ 0 0 1 ] - */ + /// Creates an affine transformation matrix from a rotation value (angle in degrees). + /// The matrix takes the following form: + /// + /// ```swift + /// [ cos α sin α 0 ] + /// [ -sin α cos α 0 ] + /// [ 0 0 1 ] + /// ``` public init(rotationByDegrees angle: CGFloat) { - let α = angle * .pi / 180.0 + let α = angle * .pi / 180 self.init(rotationByRadians: α) } +} - /** - An identity affine transformation matrix - - [ 1 0 0 ] - [ 0 1 0 ] - [ 0 0 1 ] - */ - public static let identity = AffineTransform(m11: CGFloat(1.0), m12: CGFloat(0.0), m21: CGFloat(0.0), m22: CGFloat(1.0), tX: CGFloat(0.0), tY: CGFloat(0.0)) - - // Translating - public mutating func translate(x: CGFloat, y: CGFloat) { - tX += m11 * x + m21 * y - tY += m12 * x + m22 * y - } - - /** - Mutates an affine transformation matrix from a rotation value (angle α in degrees). - The matrix takes the following form: - - [ cos α sin α 0 ] - [ -sin α cos α 0 ] - [ 0 0 1 ] - */ - public mutating func rotate(byDegrees angle: CGFloat) { - let α = angle * .pi / 180.0 - return rotate(byRadians: α) - } - - /** - Mutates an affine transformation matrix from a rotation value (angle α in radians). - The matrix takes the following form: - - [ cos α sin α 0 ] - [ -sin α cos α 0 ] - [ 0 0 1 ] - */ - public mutating func rotate(byRadians angle: CGFloat) { - let sine = sin(angle) - let cosine = cos(angle) - - m11 = cosine*m11 + sine*m21 - m12 = cosine*m12 + sine * m22 - m21 = -sine*m11 + cosine*m21 - m22 = -sine*m12 + cosine*m22 - } - - /** - Creates an affine transformation matrix by combining the receiver with `transformStruct`. - That is, it computes `T * M` and returns the result, where `T` is the receiver's and `M` is - the `transformStruct`'s affine transformation matrix. - The resulting matrix takes the following form: - - [ m11_T m12_T 0 ] [ m11_M m12_M 0 ] - T * M = [ m21_T m22_T 0 ] [ m21_M m22_M 0 ] - [ tX_T tY_T 1 ] [ tX_M tY_M 1 ] - - [ (m11_T*m11_M + m12_T*m21_M) (m11_T*m12_M + m12_T*m22_M) 0 ] - = [ (m21_T*m11_M + m22_T*m21_M) (m21_T*m12_M + m22_T*m22_M) 0 ] - [ (tX_T*m11_M + tY_T*m21_M + tX_M) (tX_T*m12_M + tY_T*m22_M + tY_M) 1 ] - */ +extension AffineTransform { + /// Creates an affine transformation matrix by combining the receiver with `transformStruct`. + /// That is, it computes `T * M` and returns the result, where `T` is the receiver's and `M` is + /// the `transformStruct`'s affine transformation matrix. + /// The resulting matrix takes the following form: + /// + /// ```swift + /// [ m11_T m12_T 0 ] [ m11_M m12_M 0 ] + /// T * M = [ m21_T m22_T 0 ] [ m21_M m22_M 0 ] + /// [ tX_T tY_T 1 ] [ tX_M tY_M 1 ] + /// ``` + /// + /// ```swift + /// [ (m11_T*m11_M + m12_T*m21_M) (m11_T*m12_M + m12_T*m22_M) 0 ] + /// = [ (m21_T*m11_M + m22_T*m21_M) (m21_T*m12_M + m22_T*m22_M) 0 ] + /// [ (tX_T*m11_M + tY_T*m21_M + tX_M) (tX_T*m12_M + tY_T*m22_M + tY_M) 1 ] + /// ``` + @inline(__always) internal func concatenated(_ other: AffineTransform) -> AffineTransform { let (t, m) = (self, other) @@ -180,88 +161,146 @@ public struct AffineTransform : ReferenceConvertible, Hashable, CustomStringConv ) } - /// Mutates an affine transformation matrix to perform the given scaling in both x and y dimensions. - public mutating func scale(_ scale: CGFloat) { - self.scale(x: scale, y: scale) + /// Mutates an affine transformation by appending the specified matrix. + public mutating func append(_ transform: AffineTransform) { + self = concatenated(transform) } + /// Mutates an affine transformation by prepending the specified matrix. + public mutating func prepend(_ transform: AffineTransform) { + self = transform.concatenated(self) + } +} + +extension AffineTransform { + // Translating + public mutating func translate(x: CGFloat, y: CGFloat) { + self = concatenated( + AffineTransform(translationByX: x, byY: y) + ) + } + /// Mutates an affine transformation matrix to perform a scaling in each of the x and y dimensions. public mutating func scale(x: CGFloat, y: CGFloat) { - m11 = CGFloat(m11.native * x.native) - m12 = CGFloat(m12.native * x.native) - m21 = CGFloat(m21.native * y.native) - m22 = CGFloat(m22.native * y.native) + self = concatenated( + AffineTransform(scaleByX: x, byY: y) + ) } - /** - Inverts the transformation matrix if possible. Matrices with a determinant that is less than - the smallest valid representation of a double value greater than zero are considered to be - invalid for representing as an inverse. If the input AffineTransform can potentially fall into - this case then the inverted() method is suggested to be used instead since that will return - an optional value that will be nil in the case that the matrix cannot be inverted. - - D = (m11 * m22) - (m12 * m21) + /// Mutates an affine transformation matrix to perform the given scaling in both x and y dimensions. + public mutating func scale(_ scale: CGFloat) { + self.scale(x: scale, y: scale) + } + + /// Mutates an affine transformation matrix from a rotation value (angle α in radians). + /// The matrix takes the following form: + /// + /// ```swift + /// [ cos α sin α 0 ] + /// [ -sin α cos α 0 ] + /// [ 0 0 1 ] + /// ``` + public mutating func rotate(byRadians angle: CGFloat) { + self = concatenated( + AffineTransform(rotationByRadians: angle) + ) + } - D < ε the inverse is undefined and will be nil - */ - public mutating func invert() { - guard let inverse = inverted() else { - fatalError("Transform has no inverse") - } - self = inverse + /// Mutates an affine transformation matrix from a rotation value (angle α in degrees). + /// The matrix takes the following form: + /// + /// ```swift + /// [ cos α sin α 0 ] + /// [ -sin α cos α 0 ] + /// [ 0 0 1 ] + /// ``` + public mutating func rotate(byDegrees angle: CGFloat) { + self = concatenated( + AffineTransform(rotationByDegrees: angle) + ) } +} +extension AffineTransform { /// Returns an inverted version of the matrix if possible, or nil if not. public func inverted() -> AffineTransform? { let determinant = (m11 * m22) - (m12 * m21) - if fabs(determinant.native) <= ε.native { + + if abs(determinant) <= CGFloat.zero.ulp { return nil } - var inverse = AffineTransform() - inverse.m11 = m22 / determinant - inverse.m12 = -m12 / determinant - inverse.m21 = -m21 / determinant - inverse.m22 = m11 / determinant - inverse.tX = (m21 * tY - m22 * tX) / determinant - inverse.tY = (m12 * tX - m11 * tY) / determinant - return inverse - } - - /// Mutates an affine transformation by appending the specified matrix. - public mutating func append(_ transform: AffineTransform) { - self = concatenated(transform) + + return AffineTransform( + m11: m22 / determinant, m12: -m12 / determinant, + m21: -m21 / determinant, m22: m11 / determinant, + tX: (m21 * tY - m22 * tX) / determinant, tY: (m12 * tX - m11 * tY) / determinant + ) } - - /// Mutates an affine transformation by prepending the specified matrix. - public mutating func prepend(_ transform: AffineTransform) { - self = transform.concatenated(self) + + /// Inverts the transformation matrix if possible. Matrices with a determinant that is less than + /// the smallest valid representation of a double value greater than zero are considered to be + /// invalid for representing as an inverse. If the input AffineTransform can potentially fall into + /// this case then the inverted() method is suggested to be used instead since that will return + /// an optional value that will be nil in the case that the matrix cannot be inverted. + /// + /// ```swift + /// D = (m11 * m22) - (m12 * m21) + /// ``` + /// + /// - Note: `D < ε` the inverse is undefined and will be nil + public mutating func invert() { + guard let inverse = inverted() else { + fatalError("Transform has no inverse") + } + + self = inverse } - +} + +extension AffineTransform { /// Applies the transform to the specified point and returns the result. - public func transform(_ point: NSPoint) -> NSPoint { - var newPoint = NSPoint() - newPoint.x = (m11 * point.x) + (m21 * point.y) + tX - newPoint.y = (m12 * point.x) + (m22 * point.y) + tY - return newPoint + public func transform(_ point: CGPoint) -> CGPoint { + CGPoint( + x: (m11 * point.x) + (m21 * point.y) + tX, + y: (m12 * point.x) + (m22 * point.y) + tY + ) } /// Applies the transform to the specified size and returns the result. - public func transform(_ size: NSSize) -> NSSize { - var newSize = NSSize() - newSize.width = (m11 * size.width) + (m21 * size.height) - newSize.height = (m12 * size.width) + (m22 * size.height) - return newSize + public func transform(_ size: CGSize) -> CGSize { + let newVector = transform(CGPoint(x: size.width, y: size.height)) + + return CGSize(width: newVector.x, height: newVector.y) } +} - public func hash(into hasher: inout Hasher) { - hasher.combine(m11) - hasher.combine(m12) - hasher.combine(m21) - hasher.combine(m22) - hasher.combine(tX) - hasher.combine(tY) +extension AffineTransform: Hashable {} + +extension AffineTransform: Codable { + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + m11 = try container.decode(CGFloat.self) + m12 = try container.decode(CGFloat.self) + m21 = try container.decode(CGFloat.self) + m22 = try container.decode(CGFloat.self) + tX = try container.decode(CGFloat.self) + tY = try container.decode(CGFloat.self) } + + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + + try container.encode(self.m11) + try container.encode(self.m12) + try container.encode(self.m21) + try container.encode(self.m22) + try container.encode(self.tX) + try container.encode(self.tY) + } +} +extension AffineTransform: CustomStringConvertible { /// A textual description of the transform. public var description: String { return "{m11:\(m11), m12:\(m12), m21:\(m21), m22:\(m22), tX:\(tX), tY:\(tY)}" @@ -271,12 +310,6 @@ public struct AffineTransform : ReferenceConvertible, Hashable, CustomStringConv public var debugDescription: String { return description } - - public static func ==(lhs: AffineTransform, rhs: AffineTransform) -> Bool { - return lhs.m11 == rhs.m11 && lhs.m12 == rhs.m12 && - lhs.m21 == rhs.m21 && lhs.m22 == rhs.m22 && - lhs.tX == rhs.tX && lhs.tY == rhs.tY - } } @@ -289,18 +322,12 @@ public struct NSAffineTransformStruct { public var tX: CGFloat public var tY: CGFloat - /// Initializes a zero-filled transformation matrix. - public init() { - m11 = 0.0 - m12 = 0.0 - m21 = 0.0 - m22 = 0.0 - tX = 0.0 - tY = 0.0 - } - /// Initializes a transformation matrix with the given values. - public init(m11: CGFloat, m12: CGFloat, m21: CGFloat, m22: CGFloat, tX: CGFloat, tY: CGFloat) { + public init( + m11: CGFloat, m12: CGFloat, + m21: CGFloat, m22: CGFloat, + tX: CGFloat, tY: CGFloat + ) { self.m11 = m11 self.m12 = m12 self.m21 = m21 @@ -308,69 +335,44 @@ public struct NSAffineTransformStruct { self.tX = tX self.tY = tY } -} - -open class NSAffineTransform : NSObject, NSCopying, NSSecureCoding { - - private var affineTransform: AffineTransform - - /// The matrix coefficients stored as the transformation matrix. - public var transformStruct: NSAffineTransformStruct { - get { - return NSAffineTransformStruct(m11: affineTransform.m11, - m12: affineTransform.m12, - m21: affineTransform.m21, - m22: affineTransform.m22, - tX: affineTransform.tX, - tY: affineTransform.tY) - } - set { - affineTransform.m11 = newValue.m11 - affineTransform.m12 = newValue.m12 - affineTransform.m21 = newValue.m21 - affineTransform.m22 = newValue.m22 - affineTransform.tX = newValue.tX - affineTransform.tY = newValue.tY - } + + /// Initializes a zero-filled transformation matrix. + public init() { + self.init(m11: 0, m12: 0, + m21: 0, m22: 0, + tX: 0, tY: 0) } +} - open func encode(with aCoder: NSCoder) { - guard aCoder.allowsKeyedCoding else { - preconditionFailure("Unkeyed coding is unsupported.") - } - - let array = [ - Float(transformStruct.m11), - Float(transformStruct.m12), - Float(transformStruct.m21), - Float(transformStruct.m22), - Float(transformStruct.tX), - Float(transformStruct.tY), - ] - - array.withUnsafeBytes { pointer in - aCoder.encodeValue(ofObjCType: "[6f]", at: UnsafeRawPointer(pointer.baseAddress!)) - } +open class NSAffineTransform: NSObject { + // Internal only for testing. + internal var affineTransform: AffineTransform + + /// Initializes an affine transform matrix to the identity matrix. + public override init() { + affineTransform = .identity } - open func copy(with zone: NSZone? = nil) -> Any { - return NSAffineTransform(transform: affineTransform) + /// Initializes an affine transform matrix using another transform object. + public convenience init(transform: AffineTransform) { + self.init() + affineTransform = transform } // Necessary because `NSObject.copy()` returns `self`. open override func copy() -> Any { - return copy(with: nil) + copy(with: nil) } public required init?(coder aDecoder: NSCoder) { - guard aDecoder.allowsKeyedCoding else { - preconditionFailure("Unkeyed coding is unsupported.") - } + precondition(aDecoder.allowsKeyedCoding, "Unkeyed coding is unsupported.") + + let pointer = UnsafeMutableRawPointer.allocate( + byteCount: MemoryLayout.stride * 6, + alignment: 1 + ) + defer { pointer.deallocate() } - let pointer = UnsafeMutableRawPointer.allocate(byteCount: MemoryLayout.stride * 6, alignment: 1) - defer { - pointer.deallocate() - } aDecoder.decodeValue(ofObjCType: "[6f]", at: pointer) let floatPointer = pointer.bindMemory(to: Float.self, capacity: 6) @@ -388,151 +390,176 @@ open class NSAffineTransform : NSObject, NSCopying, NSSecureCoding { open override func isEqual(_ object: Any?) -> Bool { guard let other = object as? NSAffineTransform else { return false } + return other === self || (other.affineTransform == self.affineTransform) } open override var hash: Int { - return affineTransform.hashValue - } - - public static var supportsSecureCoding: Bool { - return true + affineTransform.hashValue } - - /// Initializes an affine transform matrix using another transform object. - public convenience init(transform: AffineTransform) { - self.init() - affineTransform = transform +} + +extension NSAffineTransform { + /// The matrix coefficients stored as the transformation matrix. + public var transformStruct: NSAffineTransformStruct { + get { + NSAffineTransformStruct( + m11: affineTransform.m11, m12: affineTransform.m12, + m21: affineTransform.m21, m22: affineTransform.m22, + tX: affineTransform.tX, tY: affineTransform.tY + ) + } + _modify { + var transformStruct = self.transformStruct + defer { self.transformStruct = transformStruct } + + yield &transformStruct + } + set { + affineTransform.m11 = newValue.m11 + affineTransform.m12 = newValue.m12 + affineTransform.m21 = newValue.m21 + affineTransform.m22 = newValue.m22 + affineTransform.tX = newValue.tX + affineTransform.tY = newValue.tY + } } +} - /// Initializes an affine transform matrix to the identity matrix. - public override init() { - affineTransform = AffineTransform( - m11: CGFloat(1.0), m12: CGFloat(), - m21: CGFloat(), m22: CGFloat(1.0), - tX: CGFloat(), tY: CGFloat() - ) +extension NSAffineTransform: NSCopying { + open func copy(with zone: NSZone? = nil) -> Any { + NSAffineTransform(transform: affineTransform) } +} - /// Applies the specified translation factors to the transformation matrix. - open func translateX(by deltaX: CGFloat, yBy deltaY: CGFloat) { - let translation = AffineTransform(translationByX: deltaX, byY: deltaY) - affineTransform = translation.concatenated(affineTransform) +extension NSAffineTransform: NSSecureCoding { + public static let supportsSecureCoding = true + + open func encode(with aCoder: NSCoder) { + precondition(aCoder.allowsKeyedCoding, "Unkeyed coding is unsupported.") + + let array = [ + Float(transformStruct.m11), + Float(transformStruct.m12), + Float(transformStruct.m21), + Float(transformStruct.m22), + Float(transformStruct.tX), + Float(transformStruct.tY), + ] + + array.withUnsafeBytes { pointer in + aCoder.encodeValue( + ofObjCType: "[6f]", + at: UnsafeRawPointer(pointer.baseAddress!) + ) + } } +} - /// Applies a rotation factor (measured in degrees) to the transformation matrix. - open func rotate(byDegrees angle: CGFloat) { - let rotation = AffineTransform(rotationByDegrees: angle) - affineTransform = rotation.concatenated(affineTransform) +extension NSAffineTransform { + /// Applies the specified translation factors to the transformation matrix. + open func translateX(by deltaX: CGFloat, yBy deltaY: CGFloat) { + affineTransform.translate(x: deltaX, y: deltaY) } - /// Applies a rotation factor (measured in radians) to the transformation matrix. - open func rotate(byRadians angle: CGFloat) { - let rotation = AffineTransform(rotationByRadians: angle) - affineTransform = rotation.concatenated(affineTransform) + /// Applies scaling factors to each axis of the transformation matrix. + open func scaleX(by scaleX: CGFloat, yBy scaleY: CGFloat) { + affineTransform.scale(x: scaleX, y: scaleY) } /// Applies the specified scaling factor along both x and y axes to the transformation matrix. open func scale(by scale: CGFloat) { - scaleX(by: scale, yBy: scale) + affineTransform.scale(scale) + } + + /// Applies a rotation factor (measured in degrees) to the transformation matrix. + open func rotate(byDegrees angle: CGFloat) { + affineTransform.rotate(byDegrees: angle) } - /// Applies scaling factors to each axis of the transformation matrix. - open func scaleX(by scaleX: CGFloat, yBy scaleY: CGFloat) { - let scale = AffineTransform(scaleByX: scaleX, byY: scaleY) - affineTransform = scale.concatenated(affineTransform) + /// Applies a rotation factor (measured in radians) to the transformation matrix. + open func rotate(byRadians angle: CGFloat) { + affineTransform.rotate(byRadians: angle) } /// Replaces the matrix with its inverse matrix. open func invert() { - if let inverse = affineTransform.inverted() { - affineTransform = inverse - } - else { - preconditionFailure("NSAffineTransform: Transform has no inverse") + guard let inverse = affineTransform.inverted() else { + fatalError("NSAffineTransform: Transform has no inverse") } + + affineTransform = inverse } /// Appends the specified matrix. open func append(_ transform: AffineTransform) { - affineTransform = affineTransform.concatenated(transform) + affineTransform.append(transform) } /// Prepends the specified matrix. open func prepend(_ transform: AffineTransform) { - affineTransform = transform.concatenated(affineTransform) + affineTransform.prepend(transform) } /// Applies the transform to the specified point and returns the result. - open func transform(_ aPoint: NSPoint) -> NSPoint { - return affineTransform.transform(aPoint) + open func transform(_ aPoint: CGPoint) -> CGPoint { + affineTransform.transform(aPoint) } /// Applies the transform to the specified size and returns the result. - open func transform(_ aSize: NSSize) -> NSSize { - return affineTransform.transform(aSize) + open func transform(_ aSize: CGSize) -> CGSize { + affineTransform.transform(aSize) } } -extension AffineTransform : _ObjectiveCBridgeable { +extension AffineTransform: _ObjectiveCBridgeable { public static func _isBridgedToObjectiveC() -> Bool { - return true + true } public static func _getObjectiveCType() -> Any.Type { - return NSAffineTransform.self + NSAffineTransform.self } @_semantics("convertToObjectiveC") public func _bridgeToObjectiveC() -> NSAffineTransform { - return NSAffineTransform(transform: self) + NSAffineTransform(transform: self) } - public static func _forceBridgeFromObjectiveC(_ x: NSAffineTransform, result: inout AffineTransform?) { - if !_conditionallyBridgeFromObjectiveC(x, result: &result) { - fatalError("Unable to bridge type") - } + public static func _forceBridgeFromObjectiveC( + _ x: NSAffineTransform, + result: inout AffineTransform? + ) { + precondition(_conditionallyBridgeFromObjectiveC(x, result: &result), + "Unable to bridge type") } - public static func _conditionallyBridgeFromObjectiveC(_ x: NSAffineTransform, result: inout AffineTransform?) -> Bool { + public static func _conditionallyBridgeFromObjectiveC( + _ x: NSAffineTransform, + result: inout AffineTransform? + ) -> Bool { let ts = x.transformStruct - result = AffineTransform(m11: ts.m11, m12: ts.m12, m21: ts.m21, m22: ts.m22, tX: ts.tX, tY: ts.tY) + + result = AffineTransform(m11: ts.m11, m12: ts.m12, + m21: ts.m21, m22: ts.m22, + tX: ts.tX, tY: ts.tY) + return true // Can't fail } - public static func _unconditionallyBridgeFromObjectiveC(_ x: NSAffineTransform?) -> AffineTransform { + public static func _unconditionallyBridgeFromObjectiveC( + _ x: NSAffineTransform? + ) -> AffineTransform { var result: AffineTransform? _forceBridgeFromObjectiveC(x!, result: &result) return result! } } -extension NSAffineTransform : _StructTypeBridgeable { +extension NSAffineTransform: _StructTypeBridgeable { public typealias _StructType = AffineTransform public func _bridgeToSwift() -> AffineTransform { return AffineTransform._unconditionallyBridgeFromObjectiveC(self) } } - -extension AffineTransform : Codable { - public init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - m11 = try container.decode(CGFloat.self) - m12 = try container.decode(CGFloat.self) - m21 = try container.decode(CGFloat.self) - m22 = try container.decode(CGFloat.self) - tX = try container.decode(CGFloat.self) - tY = try container.decode(CGFloat.self) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.unkeyedContainer() - try container.encode(self.m11) - try container.encode(self.m12) - try container.encode(self.m21) - try container.encode(self.m22) - try container.encode(self.tX) - try container.encode(self.tY) - } -} From 5a80d809c16e73964eb69261eebc3fa09631b4c1 Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Sat, 28 Aug 2021 01:25:43 +0300 Subject: [PATCH 03/11] Expand and refine AffineTransform tests. --- .../Tests/TestAffineTransform.swift | 980 ++++++++++++------ 1 file changed, 665 insertions(+), 315 deletions(-) diff --git a/Tests/Foundation/Tests/TestAffineTransform.swift b/Tests/Foundation/Tests/TestAffineTransform.swift index ed8563df61..6d963f7659 100644 --- a/Tests/Foundation/Tests/TestAffineTransform.swift +++ b/Tests/Foundation/Tests/TestAffineTransform.swift @@ -7,383 +7,733 @@ // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // -class TestAffineTransform : XCTestCase { +// MARK: - Tests + +class TestAffineTransform: XCTestCase { private let accuracyThreshold = 0.001 static var allTests: [(String, (TestAffineTransform) -> () throws -> Void)] { return [ - ("test_BasicConstruction", test_BasicConstruction), - ("test_IdentityTransformation", test_IdentityTransformation), - ("test_Scale", test_Scale), - ("test_Scaling", test_Scaling), - ("test_TranslationScaling", test_TranslationScaling), - ("test_ScalingTranslation", test_ScalingTranslation), - ("test_Rotation_Degrees", test_Rotation_Degrees), - ("test_Rotation_Radians", test_Rotation_Radians), - ("test_Inversion", test_Inversion), - ("test_IdentityTransformation", test_IdentityTransformation), - ("test_Translation", test_Translation), - ("test_TranslationComposed", test_TranslationComposed), - ("test_AppendTransform", test_AppendTransform), - ("test_PrependTransform", test_PrependTransform), - ("test_TransformComposition", test_TransformComposition), - ("test_hashing", test_hashing), - ("test_rotation_compose", test_rotation_compose), - ("test_translation_and_rotation", test_translation_and_rotation), - ("test_Equal", test_Equal), - ("test_NSCoding", test_NSCoding), + ("testConstruction", testConstruction), + ("testBridging", testBridging), + ("testEqualityHashing", testEqualityHashing), + ("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), + ("testPrependTransform", testPrependTransform), + ("testAppendTransform", testAppendTransform), + ("testNSCoding", testNSCoding), ] } - - func checkPointTransformation(_ transform: NSAffineTransform, point: NSPoint, expectedPoint: NSPoint, _ message: String = "", file: StaticString = #file, line: UInt = #line) { +} + +// MARK: - Helper + +extension TestAffineTransform { + func assert( + point: CGPoint, + transformedBy transform: AffineTransform, + equals expectedPoint: CGPoint, + _ message: String = "", + file: StaticString = #file, line: UInt = #line + ) { + let size = CGSize(width: point.x, height: point.y) + let newPoint = transform.transform(point) - XCTAssertEqual(Double(newPoint.x), Double(expectedPoint.x), accuracy: accuracyThreshold, - "x (expected: \(expectedPoint.x), was: \(newPoint.x)): \(message)", file: file, line: line) - XCTAssertEqual(Double(newPoint.y), Double(expectedPoint.y), accuracy: accuracyThreshold, - "y (expected: \(expectedPoint.y), was: \(newPoint.y)): \(message)", file: file, line: line) - } - - func checkSizeTransformation(_ transform: NSAffineTransform, size: NSSize, expectedSize: NSSize, _ message: String = "", file: StaticString = #file, line: UInt = #line) { let newSize = transform.transform(size) - XCTAssertEqual(Double(newSize.width), Double(expectedSize.width), accuracy: accuracyThreshold, - "width (expected: \(expectedSize.width), was: \(newSize.width)): \(message)", file: file, line: line) - XCTAssertEqual(Double(newSize.height), Double(expectedSize.height), accuracy: accuracyThreshold, - "height (expected: \(expectedSize.height), was: \(newSize.height)): \(message)", file: file, line: line) - } - - func checkRectTransformation(_ transform: NSAffineTransform, rect: NSRect, expectedRect: NSRect, _ message: String = "", file: StaticString = #file, line: UInt = #line) { - let newRect = transform.transformRect(rect) - checkPointTransformation(transform, point: newRect.origin, expectedPoint: expectedRect.origin, - "origin (expected: \(expectedRect.origin), was: \(newRect.origin)): \(message)", file: file, line: line) - checkSizeTransformation(transform, size: newRect.size, expectedSize: expectedRect.size, - "size (expected: \(expectedRect.size), was: \(newRect.size)): \(message)", file: file, line: line) + XCTAssertEqual( + newPoint.x, newSize.width, + "Expected point x and size width to match", + file: file, line: line + ) + XCTAssertEqual( + newPoint.y, newSize.height, + "Expected point y and size height to match", + file: file, line: line + ) + + let nsTransform = transform as NSAffineTransform + XCTAssertEqual( + nsTransform.transform(point), newPoint, + "Expected NSAffineTransform to match AffineTransform's point-accepting transform(_:)", + file: file, line: line + ) + XCTAssertEqual( + nsTransform.transform(size), newSize, + "Expected NSAffineTransform to match AffineTransform's size-accepting transform(_:)", + file: file, line: line + ) + + 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 + ) } +} - func test_BasicConstruction() { - let identityTransform = NSAffineTransform() - let transformStruct = identityTransform.transformStruct - - // The diagonal entries (1,1) and (2,2) of the identity matrix are ones. The other entries are zeros. - // TODO: These should use DBL_MAX but it's not available as part of Glibc on Linux - XCTAssertEqual(Double(transformStruct.m11), Double(1), accuracy: accuracyThreshold) - XCTAssertEqual(Double(transformStruct.m22), Double(1), accuracy: accuracyThreshold) - - XCTAssertEqual(Double(transformStruct.m12), Double(0), accuracy: accuracyThreshold) - XCTAssertEqual(Double(transformStruct.m21), Double(0), accuracy: accuracyThreshold) - XCTAssertEqual(Double(transformStruct.tX), Double(0), accuracy: accuracyThreshold) - XCTAssertEqual(Double(transformStruct.tY), Double(0), accuracy: accuracyThreshold) +// MARK: - Construction + +extension TestAffineTransform { + func testConstruction() { + let transform = AffineTransform( + m11: 1, m12: 2, + m21: 3, m22: 4, + tX: 5, tY: 6 + ) + + XCTAssertEqual(transform.m11, 1) + XCTAssertEqual(transform.m12, 2) + XCTAssertEqual(transform.m21, 3) + XCTAssertEqual(transform.m22, 4) + XCTAssertEqual(transform.tX , 5) + XCTAssertEqual(transform.tY , 6) } +} - func test_IdentityTransformation() { - let identityTransform = NSAffineTransform() +// MARK: - Bridging - func checkIdentityPointTransformation(_ point: NSPoint) { - checkPointTransformation(identityTransform, point: point, expectedPoint: point) - } +extension TestAffineTransform { + func testBridging() { + let transform = AffineTransform( + m11: 1, m12: 2, + m21: 3, m22: 4, + tX: 5, tY: 6 + ) + + let nsTransform = NSAffineTransform(transform: transform) + XCTAssertEqual(transform, nsTransform.affineTransform) - checkIdentityPointTransformation(NSPoint.zero) - checkIdentityPointTransformation(NSPoint(x: 24.5, y: 10.0)) - checkIdentityPointTransformation(NSPoint(x: -7.5, y: 2.0)) + XCTAssertEqual(nsTransform as AffineTransform, transform) + } +} - func checkIdentitySizeTransformation(_ size: NSSize) { - checkSizeTransformation(identityTransform, size: size, expectedSize: size) +// MARK: Equality + +extension TestAffineTransform { + func testEqualityHashing() { + let samples = [ + AffineTransform(m11: 1.5, m12: 2.0, m21: 3.0, m22: 4.0, tX: 5.0, tY: 6.0), + AffineTransform(m11: 1.0, m12: 2.5, m21: 3.0, m22: 4.0, tX: 5.0, tY: 6.0), + AffineTransform(m11: 1.0, m12: 2.0, m21: 3.5, m22: 4.0, tX: 5.0, tY: 6.0), + AffineTransform(m11: 1.0, m12: 2.0, m21: 3.0, m22: 4.5, tX: 5.0, tY: 6.0), + AffineTransform(m11: 1.0, m12: 2.0, m21: 3.0, m22: 4.0, tX: 5.5, tY: 6.0), + AffineTransform(m11: 1.0, m12: 2.0, m21: 3.0, m22: 4.0, tX: 5.0, tY: 6.5), + ].map(NSAffineTransform.init) + + for (index, sample) in samples.enumerated() { + let otherSamples: [NSAffineTransform] = { + var samplesCopy = samples + samplesCopy.remove(at: index) + return samplesCopy + }() + + XCTAssertEqual(sample, sample) + XCTAssertEqual(sample.hashValue, sample.hashValue) + + for otherSample in otherSamples { + XCTAssert(sample != otherSample) + XCTAssert(sample.hashValue != otherSample.hashValue) + } } + } +} - checkIdentitySizeTransformation(NSSize.zero) - checkIdentitySizeTransformation(NSSize(width: 13.0, height: 12.5)) - checkIdentitySizeTransformation(NSSize(width: 100.0, height: -100.0)) +// MARK: - Vector Transformations + +extension TestAffineTransform { + func testVectorTransformations() { + + // To transform a given point with coordinated px and py, + // we do: + // + // [ x' y' ] + // + // = [ px py ] * [ m11 m12 ] + [ tX tY ] + // [ m21 m22 ] + // + // = [ px*m11+py*m21+tX px*m12+py*m22+tY ] + + assert( + point: CGPoint(x: 10, y: 20), + transformedBy: AffineTransform( + m11: 1, m12: 2, + m21: 3, m22: 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 ] + equals: CGPoint(x: 75, y: 106) + ) + + assert( + point: CGPoint(x: 5, y: 25), + transformedBy: AffineTransform( + m11: 5, m12: 4, + m21: 3, m22: 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 ] + equals: CGPoint(x: 101, y: 70) + ) } - - func test_Translation() { - let point = NSPoint.zero +} - let noop = NSAffineTransform() - noop.translateX(by: CGFloat(), yBy: CGFloat()) - checkPointTransformation(noop, point: point, expectedPoint: point) - - let translateH = NSAffineTransform() - translateH.translateX(by: CGFloat(10.0), yBy: CGFloat()) - checkPointTransformation(translateH, point: point, expectedPoint: NSPoint(x: 10.0, y: 0.0)) - - let translateV = NSAffineTransform() - translateV.translateX(by: CGFloat(), yBy: CGFloat(20.0)) - checkPointTransformation(translateV, point: point, expectedPoint: NSPoint(x: 0.0, y: 20.0)) - - let translate = NSAffineTransform() - translate.translateX(by: CGFloat(-30.0), yBy: CGFloat(40.0)) - checkPointTransformation(translate, point: point, expectedPoint: NSPoint(x: -30.0, y: 40.0)) +// MARK: - Identity + +extension TestAffineTransform { + func testIdentityConstruction() { + // Check that the transform matrix is the identity: + // [ 1 0 0 ] + // [ 0 1 0 ] + // [ 0 0 1 ] + let identity = AffineTransform( + m11: 1, m12: 0, + m21: 0, m22: 1, + tX: 0, tY: 0 + ) + + XCTAssertEqual(AffineTransform(), identity) + XCTAssertEqual(AffineTransform.identity, identity) + XCTAssertEqual(NSAffineTransform().affineTransform, identity) } - func test_Scale() { - let size = NSSize(width: 10.0, height: 10.0) - - let noop = NSAffineTransform() - noop.scale(by: CGFloat(1.0)) - checkSizeTransformation(noop, size: size, expectedSize: size) - - let shrink = NSAffineTransform() - shrink.scale(by: CGFloat(0.5)) - checkSizeTransformation(shrink, size: size, expectedSize: NSSize(width: 5.0, height: 5.0)) - - let grow = NSAffineTransform() - grow.scale(by: CGFloat(3.0)) - checkSizeTransformation(grow, size: size, expectedSize: NSSize(width: 30.0, height: 30.0)) - - let stretch = NSAffineTransform() - stretch.scaleX(by: CGFloat(2.0), yBy: CGFloat(0.5)) - checkSizeTransformation(stretch, size: size, expectedSize: NSSize(width: 20.0, height: 5.0)) + func testIdentity() { + assert( + point: CGPoint(x: 25, y: 10), + transformedBy: .identity, + equals: CGPoint(x: 25, y: 10) + ) } - - func test_Rotation_Degrees() { - let point = NSPoint(x: 10.0, y: 10.0) +} - let noop = NSAffineTransform() - noop.rotate(byDegrees: CGFloat()) - checkPointTransformation(noop, point: point, expectedPoint: point) - - let tenEighty = NSAffineTransform() - tenEighty.rotate(byDegrees: CGFloat(1080.0)) - checkPointTransformation(tenEighty, point: point, expectedPoint: point) +// MARK: - Translation + +extension TestAffineTransform { + func testTranslationConstruction() { + let translatedIdentity: AffineTransform = { + var transform = AffineTransform.identity + transform.translate(x: 15, y: 20) + return transform + }() - let rotateCounterClockwise = NSAffineTransform() - rotateCounterClockwise.rotate(byDegrees: CGFloat(90.0)) - checkPointTransformation(rotateCounterClockwise, point: point, expectedPoint: NSPoint(x: -10.0, y: 10.0)) + let translation = AffineTransform( + translationByX: 15, byY: 20 + ) - let rotateClockwise = NSAffineTransform() - rotateClockwise.rotate(byDegrees: CGFloat(-90.0)) - checkPointTransformation(rotateClockwise, point: point, expectedPoint: NSPoint(x: 10.0, y: -10.0)) + let nsTranslation: NSAffineTransform = { + let transform = NSAffineTransform() + transform.translateX(by: 15, yBy: 20) + return transform + }() - let reflectAboutOrigin = NSAffineTransform() - reflectAboutOrigin.rotate(byDegrees: CGFloat(180.0)) - checkPointTransformation(reflectAboutOrigin, point: point, expectedPoint: NSPoint(x: -10.0, y: -10.0)) + XCTAssertEqual(translatedIdentity, translation) + XCTAssertEqual(nsTranslation.affineTransform, translation) } - func test_Rotation_Radians() { - let point = NSPoint(x: 10.0, y: 10.0) + func testTranslation() { + assert( + point: CGPoint(x: 10, y: 10), + transformedBy: AffineTransform( + translationByX: 0, byY: 0 + ), + equals: CGPoint(x: 10, y: 10) + ) + + assert( + point: CGPoint(x: 10, y: 10), + transformedBy: AffineTransform( + translationByX: 0, byY: 5 + ), + equals: CGPoint(x: 10, y: 15) + ) + + assert( + point: CGPoint(x: 10, y: 10), + transformedBy: AffineTransform( + translationByX: 5, byY: 5 + ), + equals: CGPoint(x: 15, y: 15) + ) + + assert( + point: CGPoint(x: -2, y: -3), + // Translate by 5 + transformedBy: { + var transform = AffineTransform.identity + + transform.translate(x: 2, y: 3) + transform.translate(x: 3, y: 2) + + return transform + }(), + equals: CGPoint(x: 3, y: 2) + ) + } +} - let noop = NSAffineTransform() - noop.rotate(byRadians: CGFloat()) - checkPointTransformation(noop, point: point, expectedPoint: point) +// MARK: - Scaling + +extension TestAffineTransform { + func testScalingConstruction() { + // Distinct x/y Components - let tenEighty = NSAffineTransform() - tenEighty.rotate(byRadians: 6 * .pi) - checkPointTransformation(tenEighty, point: point, expectedPoint: point) + let scaledIdentity: AffineTransform = { + var transform = AffineTransform.identity + transform.scale(x: 15, y: 20) + return transform + }() - let rotateCounterClockwise = NSAffineTransform() - rotateCounterClockwise.rotate(byRadians: .pi / 2) - checkPointTransformation(rotateCounterClockwise, point: point, expectedPoint: NSPoint(x: -10.0, y: 10.0)) + let scaling = AffineTransform( + scaleByX: 15, byY: 20 + ) - let rotateClockwise = NSAffineTransform() - rotateClockwise.rotate(byRadians: -.pi / 2) - checkPointTransformation(rotateClockwise, point: point, expectedPoint: NSPoint(x: 10.0, y: -10.0)) + let nsScaling: NSAffineTransform = { + let transform = NSAffineTransform() + transform.scaleX(by: 15, yBy: 20) + return transform + }() - let reflectAboutOrigin = NSAffineTransform() - reflectAboutOrigin.rotate(byRadians: .pi) - checkPointTransformation(reflectAboutOrigin, point: point, expectedPoint: NSPoint(x: -10.0, y: -10.0)) - } - - func test_Inversion() { - let point = NSPoint(x: 10.0, y: 10.0) - - var translate = AffineTransform() - translate.translate(x: CGFloat(-30.0), y: CGFloat(40.0)) + XCTAssertEqual(scaledIdentity, scaling) + XCTAssertEqual(nsScaling.affineTransform, scaling) - var rotate = AffineTransform() - translate.rotate(byDegrees: CGFloat(30.0)) + // Same x/y Components - var scale = AffineTransform() - scale.scale(CGFloat(2.0)) + let differentScaledIdentity = AffineTransform( + scaleByX: 20, byY: 20 + ) - let identityTransform = NSAffineTransform() + let sameScaledIdentity: AffineTransform = { + var transform = AffineTransform.identity + transform.scale(20) + return transform + }() - // append transformations - identityTransform.append(translate) - identityTransform.append(rotate) - identityTransform.append(scale) + let sameScaling = AffineTransform( + scale: 20 + ) - // invert transformations - scale.invert() - rotate.invert() - translate.invert() + let sameNSScaling: NSAffineTransform = { + let transform = NSAffineTransform() + transform.scale(by: 20) + return transform + }() - // append inverse transformations in reverse order - identityTransform.append(scale) - identityTransform.append(rotate) - identityTransform.append(translate) + XCTAssertEqual(sameScaling, differentScaledIdentity) - checkPointTransformation(identityTransform, point: point, expectedPoint: point) - } - - func test_TranslationComposed() { - let xyPlus5 = NSAffineTransform() - xyPlus5.translateX(by: CGFloat(2.0), yBy: CGFloat(3.0)) - xyPlus5.translateX(by: CGFloat(3.0), yBy: CGFloat(2.0)) - - checkPointTransformation(xyPlus5, point: NSPoint(x: -2.0, y: -3.0), - expectedPoint: NSPoint(x: 3.0, y: 2.0)) + XCTAssertEqual(sameScaledIdentity, sameScaling) + XCTAssertEqual(sameNSScaling.affineTransform, sameScaling) } - func test_Scaling() { - let xyTimes5 = NSAffineTransform() - xyTimes5.scale(by: CGFloat(5.0)) - - checkPointTransformation(xyTimes5, point: NSPoint(x: -2.0, y: 3.0), - expectedPoint: NSPoint(x: -10.0, y: 15.0)) - - let xTimes2YTimes3 = NSAffineTransform() - xTimes2YTimes3.scaleX(by: CGFloat(2.0), yBy: CGFloat(-3.0)) - - checkPointTransformation(xTimes2YTimes3, point: NSPoint(x: -1.0, y: 3.5), - expectedPoint: NSPoint(x: -2.0, y: -10.5)) + func testScaling() { + assert( + point: CGPoint(x: 10, y: 10), + transformedBy: AffineTransform( + scaleByX: 1, byY: 0 + ), + equals: CGPoint(x: 10, y: 0) + ) + + assert( + point: CGPoint(x: 10, y: 10), + transformedBy: AffineTransform( + scaleByX: 0.5, byY: 1 + ), + equals: CGPoint(x: 5, y: 10) + ) + + assert( + point: CGPoint(x: 10, y: 10), + transformedBy: AffineTransform( + scaleByX: 0, byY: 2 + ), + equals: CGPoint(x: 0, y: 20) + ) + + assert( + point: CGPoint(x: 10, y: 10), + // Scale by (2, 0) + transformedBy: { + var transform = AffineTransform.identity + + transform.scale(x: 4, y: 0) + transform.scale(x: 0.5, y: 1) + + return transform + }(), + equals: CGPoint(x: 20, y: 0) + ) } +} - func test_TranslationScaling() { - let xPlus2XYTimes5 = NSAffineTransform() - xPlus2XYTimes5.translateX(by: CGFloat(2.0), yBy: CGFloat()) - xPlus2XYTimes5.scaleX(by: CGFloat(5.0), yBy: CGFloat(-5.0)) - - checkPointTransformation(xPlus2XYTimes5, point: NSPoint(x: 1.0, y: 2.0), - expectedPoint: NSPoint(x: 7.0, y: -10.0)) - } - - func test_ScalingTranslation() { - let xyTimes5XPlus3 = NSAffineTransform() - xyTimes5XPlus3.scale(by: CGFloat(5.0)) - xyTimes5XPlus3.translateX(by: CGFloat(3.0), yBy: CGFloat()) - - checkPointTransformation(xyTimes5XPlus3, point: NSPoint(x: 1.0, y: 2.0), - expectedPoint: NSPoint(x: 20.0, y: 10.0)) +// MARK: - Rotation + +extension TestAffineTransform { + func testRotationConstruction() { + let baseRotation = AffineTransform( + rotationByRadians: .pi + ) + + func assertPiRotation( + _ rotation: AffineTransform, + file: StaticString = #file, + line: UInt = #line + ) { + let point = CGPoint(x: 10, y: 15) + let newPoint = baseRotation.transform(point) + + self.assert( + point: point, transformedBy: rotation, + equals: newPoint, + file: file, line: line + ) + } + + // Radians + + assertPiRotation({ + var transform = AffineTransform.identity + transform.rotate(byRadians: .pi) + return transform + }()) + + assertPiRotation({ + let transform = NSAffineTransform() + transform.rotate(byRadians: .pi) + return transform + }() as NSAffineTransform as AffineTransform) + + // Degrees + + assertPiRotation({ + var transform = AffineTransform.identity + transform.rotate(byDegrees: 180) + return transform + }()) + + assertPiRotation(AffineTransform( + rotationByDegrees: 180 + )) + + assertPiRotation({ + let transform = NSAffineTransform() + transform.rotate(byDegrees: 180) + return transform + }() as NSAffineTransform as AffineTransform) } - func test_AppendTransform() { - let point = NSPoint(x: 10.0, y: 10.0) + func testRotation() { + assert( + point: CGPoint(x: 10, y: 15), + transformedBy: AffineTransform(rotationByDegrees: 0), + equals: CGPoint(x: 10, y: 15) + ) + + assert( + point: CGPoint(x: 10, y: 15), + transformedBy: AffineTransform(rotationByDegrees: 1080), + equals: CGPoint(x: 10, y: 15) + ) + + // Counter-clockwise rotation + assert( + point: CGPoint(x: 15, y: 10), + transformedBy: AffineTransform(rotationByRadians: .pi / 2), + equals: CGPoint(x: -10, y: 15) + ) + + // Clockwise rotation + assert( + point: CGPoint(x: 15, y: 10), + transformedBy: AffineTransform(rotationByDegrees: -90), + equals: CGPoint(x: 10, y: -15) + ) + + // Reflect about origin + assert( + point: CGPoint(x: 10, y: 15), + transformedBy: AffineTransform(rotationByRadians: .pi), + equals: CGPoint(x: -10, y: -15) + ) + + // Composed reflection about origin + assert( + point: CGPoint(x: 10, y: 15), + // Rotate by 180º + transformedBy: { + var transform = AffineTransform.identity + + transform.rotate(byDegrees: 90) + transform.rotate(byDegrees: 90) + + return transform + }(), + equals: CGPoint(x: -10, y: -15) + ) + } +} - var identityTransform = AffineTransform() - identityTransform.append(identityTransform) - checkPointTransformation(NSAffineTransform(transform: identityTransform), point: point, expectedPoint: point) - - var translate = AffineTransform() - translate.translate(x: CGFloat(10.0), y: CGFloat()) - - var scale = AffineTransform() - scale.scale(CGFloat(2.0)) - - let translateThenScale = NSAffineTransform(transform: translate) - translateThenScale.append(scale) - checkPointTransformation(translateThenScale, point: point, expectedPoint: NSPoint(x: 40.0, y: 20.0)) +// MARK: - Permutations + +extension TestAffineTransform { + func testTranslationScaling() { + assert( + point: CGPoint(x: 1, y: 3), + // Translate by (2, 0) then scale by (5, -5) + transformedBy: { + var transform = AffineTransform.identity + + transform.append(.init(translationByX: 2, byY: 0)) + transform.append(.init(scaleByX: 5, byY: -5)) + print(">>>>>>>>>>>>>>>>>", transform) + + transform = .identity + transform.translate(x: 2, y: 0) + transform.scale(x: 5, y: -5) + print(">>>>>>>>>>>>>>>>>", transform) + + return transform + }(), + equals: CGPoint(x: 15, y: -15) + ) + + assert( + point: CGPoint(x: 3, y: 1), + // Scale by (-5, 5) then scale by (0, 10) + transformedBy: { + var transform = AffineTransform.identity + + transform.scale(x: -5, y: 5) + transform.translate(x: 0, y: 10) + + return transform + }(), + equals: CGPoint(x: -15, y: 15) + ) } - func test_PrependTransform() { - let point = NSPoint(x: 10.0, y: 10.0) - - var identityTransform = AffineTransform() - identityTransform.prepend(identityTransform) - checkPointTransformation(NSAffineTransform(transform: identityTransform), point: point, expectedPoint: point) - - var translate = AffineTransform() - translate.translate(x: CGFloat(10.0), y: CGFloat()) - - var scale = AffineTransform() - scale.scale(CGFloat(2.0)) - - let scaleThenTranslate = NSAffineTransform(transform: translate) - scaleThenTranslate.prepend(scale) - checkPointTransformation(scaleThenTranslate, point: point, expectedPoint: NSPoint(x: 30.0, y: 20.0)) + func testTranslationRotation() { + assert( + point: CGPoint(x: 10, y: 10), + // Translate by (20, -5) then rotate by 90º + transformedBy: { + var transform = AffineTransform.identity + + transform.translate(x: 20, y: -5) + transform.rotate(byDegrees: 90) + + return transform + }(), + equals: CGPoint(x: -5, y: 30) + ) + + assert( + point: CGPoint(x: 10, y: 10), + // Rotate by 180º and then translate by (20, 15) + transformedBy: { + var transform = AffineTransform.identity + + transform.rotate(byDegrees: 180) + transform.translate(x: 20, y: 15) + + return transform + }(), + equals: CGPoint(x: 10, y: 5) + ) } - - func test_TransformComposition() { - let origin = NSPoint(x: 10.0, y: 10.0) - let size = NSSize(width: 40.0, height: 20.0) - let rect = NSRect(origin: origin, size: size) - let center = NSPoint(x: rect.midX, y: rect.midY) - - let rotate = NSAffineTransform() - rotate.rotate(byDegrees: CGFloat(90.0)) - - var moveOrigin = AffineTransform() - moveOrigin.translate(x: -center.x, y: -center.y) - - var moveBack = moveOrigin - moveBack.invert() - - let rotateAboutCenter = rotate - rotateAboutCenter.prepend(moveOrigin) - rotateAboutCenter.append(moveBack) - - // center of rect shouldn't move as its the rotation anchor - checkPointTransformation(rotateAboutCenter, point: center, expectedPoint: center) + func testScalingRotation() { + assert( + point: CGPoint(x: 20, y: 5), + // Scale by (0.5, 3) then rotate by -90º + transformedBy: { + var transform = AffineTransform.identity + + transform.scale(x: 0.5, y: 3) + transform.rotate(byDegrees: -90) + + return transform + }(), + equals: CGPoint(x: 15, y: -10) + ) + + assert( + point: CGPoint(x: 20, y: 5), + // Rotate by -90º the scale by (0.5, 3) + transformedBy: { + var transform = AffineTransform.identity + + transform.rotate(byDegrees: -90) + transform.scale(x: 3, y: -0.5) + + return transform + }(), + equals: CGPoint(x: 15, y: 10) + ) } +} - func test_hashing() { - let a = AffineTransform(m11: 1.0, m12: 2.5, m21: 66.2, m22: 40.2, tX: -5.5, tY: 3.7) - let b = AffineTransform(m11: -55.66, m12: 22.7, m21: 1.5, m22: 0.0, tX: -22, tY: -33) - let c = AffineTransform(m11: 4.5, m12: 1.1, m21: 0.025, m22: 0.077, tX: -0.55, tY: 33.2) - let d = AffineTransform(m11: 7.0, m12: -2.3, m21: 6.7, m22: 0.25, tX: 0.556, tY: 0.99) - let e = AffineTransform(m11: 0.498, m12: -0.284, m21: -0.742, m22: 0.3248, tX: 12, tY: 44) - - // Samples testing that every component is properly hashed - let x1 = AffineTransform(m11: 1.0, m12: 2.0, m21: 3.0, m22: 4.0, tX: 5.0, tY: 6.0) - let x2 = AffineTransform(m11: 1.5, m12: 2.0, m21: 3.0, m22: 4.0, tX: 5.0, tY: 6.0) - let x3 = AffineTransform(m11: 1.0, m12: 2.5, m21: 3.0, m22: 4.0, tX: 5.0, tY: 6.0) - let x4 = AffineTransform(m11: 1.0, m12: 2.0, m21: 3.5, m22: 4.0, tX: 5.0, tY: 6.0) - let x5 = AffineTransform(m11: 1.0, m12: 2.0, m21: 3.0, m22: 4.5, tX: 5.0, tY: 6.0) - let x6 = AffineTransform(m11: 1.0, m12: 2.0, m21: 3.0, m22: 4.0, tX: 5.5, tY: 6.0) - let x7 = AffineTransform(m11: 1.0, m12: 2.0, m21: 3.0, m22: 4.0, tX: 5.0, tY: 6.5) - - @inline(never) - func bridged(_ t: AffineTransform) -> NSAffineTransform { - return t as NSAffineTransform - } +// MARK: - Inversion - let values: [[AffineTransform]] = [ - [AffineTransform.identity, NSAffineTransform() as AffineTransform], - [a, bridged(a) as AffineTransform], - [b, bridged(b) as AffineTransform], - [c, bridged(c) as AffineTransform], - [d, bridged(d) as AffineTransform], - [e, bridged(e) as AffineTransform], - [x1], [x2], [x3], [x4], [x5], [x6], [x7] +extension TestAffineTransform { + func testInversion() { + let transforms = [ + AffineTransform(translationByX: -30, byY: 40), + AffineTransform(rotationByDegrees: 30), + AffineTransform(scaleByX: 20, byY: -10), ] - checkHashableGroups(values) - } - - func test_rotation_compose() { - var t = AffineTransform.identity - t.translate(x: 1.0, y: 1.0) - t.rotate(byDegrees: 90) - t.translate(x: -1.0, y: -1.0) - let result = t.transform(NSPoint(x: 1.0, y: 2.0)) - XCTAssertEqual(0.0, Double(result.x), accuracy: accuracyThreshold) - XCTAssertEqual(1.0, Double(result.y), accuracy: accuracyThreshold) + + let composeTransform: AffineTransform = { + var transform = AffineTransform.identity + + for component in transforms { + transform.append(component) + } + + return transform + }() + + let recoveredIdentity: AffineTransform = { + var transform = composeTransform + + // Append inverse transformations in reverse order + for component in transforms.reversed() { + transform.append(component.inverted()!) + } + + return transform + }() + + assert( + point: CGPoint(x: 10, y: 10), + transformedBy: recoveredIdentity, + equals: CGPoint(x: 10, y: 10) + ) } +} - func test_translation_and_rotation() { - let point = NSPoint(x: 10, y: 10) - var translateThenRotate = AffineTransform(translationByX: 20, byY: -30) - translateThenRotate.rotate(byRadians: .pi / 2) - checkPointTransformation(NSAffineTransform(transform: translateThenRotate), point: point, expectedPoint: NSPoint(x: 10, y: -20)) - } - - func test_Equal() { - let transform = NSAffineTransform() - let transform1 = NSAffineTransform() - - XCTAssertEqual(transform1, transform) - XCTAssertFalse(transform === transform1) +// MARK: - Concatenation + +extension TestAffineTransform { + func testPrependTransform() { + assert( + point: CGPoint(x: 10, y: 15), + transformedBy: { + var transform = AffineTransform.identity + transform.prepend(.identity) + return transform + }(), + equals: CGPoint(x: 10, y: 15) + ) + + assert( + point: CGPoint(x: 10, y: 15), + // Scale by 2 then translate by (10, 0) + transformedBy: { + let scale = AffineTransform(scale: 2) + + var transform = AffineTransform( + translationByX: 10, byY: 0 + ) + transform.prepend(scale) + + return transform + }(), + equals: CGPoint(x: 30, y: 30) + ) } - func test_NSCoding() { - let transformA = NSAffineTransform() - transformA.scale(by: 2) - let transformB = NSKeyedUnarchiver.unarchiveObject(with: NSKeyedArchiver.archivedData(withRootObject: transformA)) as! NSAffineTransform - XCTAssertEqual(transformA, transformB, "Archived then unarchived `NSAffineTransform` must be equal.") + func testAppendTransform() { + assert( + point: CGPoint(x: 10, y: 15), + transformedBy: { + var transform = AffineTransform.identity + transform.append(.identity) + return transform + }(), + equals: CGPoint(x: 10, y: 15) + ) + + assert( + point: CGPoint(x: 10, y: 15), + // Translate by (10, 0) then scale by 2 + transformedBy: { + let scale = AffineTransform(scale: 2) + + var transform = AffineTransform( + translationByX: 10, byY: 0 + ) + transform.append(scale) + + return transform + }(), + equals: CGPoint(x: 40, y: 30) + ) } } -extension NSAffineTransform { - func transformRect(_ aRect: NSRect) -> NSRect { - return NSRect(origin: transform(aRect.origin), size: transform(aRect.size)) +// MARK: - Coding + +extension TestAffineTransform { + func testNSCoding() throws { + let transform = AffineTransform( + m11: 1, m12: 2, + m21: 3, m22: 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( + AffineTransform.self, from: encodedData + ) + + XCTAssertEqual( + transform, recovered, + "Encoded and then decoded transform does not equal original" + ) + + let nsTransform = transform as NSAffineTransform + let nsRecoveredTransform = NSKeyedUnarchiver.unarchiveObject(with: NSKeyedArchiver.archivedData(withRootObject: nsTransform)) as! NSAffineTransform + + XCTAssertEqual( + nsTransform, nsRecoveredTransform, + "Archived then unarchived `NSAffineTransform` must be equal." + ) } } From 372202858758a1e56f026f61a1041829c27942a7 Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Sat, 28 Aug 2021 01:26:09 +0300 Subject: [PATCH 04/11] Update AffineTransform implementation status. --- Docs/Status.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docs/Status.md b/Docs/Status.md index ce1f70e2ca..6668b46d90 100644 --- a/Docs/Status.md +++ b/Docs/Status.md @@ -227,7 +227,7 @@ There is no _Complete_ status for test coverage because there are always additio | `NSEdgeInsets` | Complete | Substantial | | | `NSGeometry` | Mostly Complete | Substantial | `NSIntegralRectWithOptions` `.AlignRectFlipped` support remains unimplemented | | `CGFloat` | Complete | Substantial | | - | `AffineTransform` | Complete | None | | + | `AffineTransform` | Complete | Substantial | | | `NSAffineTransform` | Complete | Substantial | | | `NSNumber` | Complete | Incomplete | | | `NSConcreteValue` | N/A | N/A | For internal use only | From b965b148dd2e877980fb4fbc09989d1630ea1427 Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Sat, 28 Aug 2021 01:37:59 +0300 Subject: [PATCH 05/11] [AffineTransform] Add Foundation testable import. --- Tests/Foundation/Tests/TestAffineTransform.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tests/Foundation/Tests/TestAffineTransform.swift b/Tests/Foundation/Tests/TestAffineTransform.swift index 6d963f7659..83c0c87458 100644 --- a/Tests/Foundation/Tests/TestAffineTransform.swift +++ b/Tests/Foundation/Tests/TestAffineTransform.swift @@ -7,6 +7,14 @@ // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // +#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT + #if canImport(SwiftFoundation) && !DEPLOYMENT_RUNTIME_OBJC + @testable import SwiftFoundation + #else + @testable import Foundation + #endif +#endif + // MARK: - Tests class TestAffineTransform: XCTestCase { From 4ea13f416faa60045ece79fe08e0263aad66ca56 Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Sun, 29 Aug 2021 14:44:54 +0300 Subject: [PATCH 06/11] Apply code-review fixes. --- .../Tests/TestAffineTransform.swift | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/Tests/Foundation/Tests/TestAffineTransform.swift b/Tests/Foundation/Tests/TestAffineTransform.swift index 83c0c87458..11c0732190 100644 --- a/Tests/Foundation/Tests/TestAffineTransform.swift +++ b/Tests/Foundation/Tests/TestAffineTransform.swift @@ -48,7 +48,7 @@ class TestAffineTransform: XCTestCase { // MARK: - Helper extension TestAffineTransform { - func assert( + func check( point: CGPoint, transformedBy transform: AffineTransform, equals expectedPoint: CGPoint, @@ -128,8 +128,10 @@ extension TestAffineTransform { tX: 5, tY: 6 ) + #if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT let nsTransform = NSAffineTransform(transform: transform) XCTAssertEqual(transform, nsTransform.affineTransform) + #endif XCTAssertEqual(nsTransform as AffineTransform, transform) } @@ -159,8 +161,8 @@ extension TestAffineTransform { XCTAssertEqual(sample.hashValue, sample.hashValue) for otherSample in otherSamples { - XCTAssert(sample != otherSample) - XCTAssert(sample.hashValue != otherSample.hashValue) + XCTAssertNotEqual(sample, otherSample) + XCTAssertNotEqual(sample.hashValue, otherSample.hashValue) } } } @@ -181,7 +183,7 @@ extension TestAffineTransform { // // = [ px*m11+py*m21+tX px*m12+py*m22+tY ] - assert( + check( point: CGPoint(x: 10, y: 20), transformedBy: AffineTransform( m11: 1, m12: 2, @@ -194,7 +196,7 @@ extension TestAffineTransform { equals: CGPoint(x: 75, y: 106) ) - assert( + check( point: CGPoint(x: 5, y: 25), transformedBy: AffineTransform( m11: 5, m12: 4, @@ -229,7 +231,7 @@ extension TestAffineTransform { } func testIdentity() { - assert( + check( point: CGPoint(x: 25, y: 10), transformedBy: .identity, equals: CGPoint(x: 25, y: 10) @@ -262,7 +264,7 @@ extension TestAffineTransform { } func testTranslation() { - assert( + check( point: CGPoint(x: 10, y: 10), transformedBy: AffineTransform( translationByX: 0, byY: 0 @@ -270,7 +272,7 @@ extension TestAffineTransform { equals: CGPoint(x: 10, y: 10) ) - assert( + check( point: CGPoint(x: 10, y: 10), transformedBy: AffineTransform( translationByX: 0, byY: 5 @@ -278,7 +280,7 @@ extension TestAffineTransform { equals: CGPoint(x: 10, y: 15) ) - assert( + check( point: CGPoint(x: 10, y: 10), transformedBy: AffineTransform( translationByX: 5, byY: 5 @@ -286,7 +288,7 @@ extension TestAffineTransform { equals: CGPoint(x: 15, y: 15) ) - assert( + check( point: CGPoint(x: -2, y: -3), // Translate by 5 transformedBy: { @@ -356,7 +358,7 @@ extension TestAffineTransform { } func testScaling() { - assert( + check( point: CGPoint(x: 10, y: 10), transformedBy: AffineTransform( scaleByX: 1, byY: 0 @@ -364,7 +366,7 @@ extension TestAffineTransform { equals: CGPoint(x: 10, y: 0) ) - assert( + check( point: CGPoint(x: 10, y: 10), transformedBy: AffineTransform( scaleByX: 0.5, byY: 1 @@ -372,7 +374,7 @@ extension TestAffineTransform { equals: CGPoint(x: 5, y: 10) ) - assert( + check( point: CGPoint(x: 10, y: 10), transformedBy: AffineTransform( scaleByX: 0, byY: 2 @@ -380,7 +382,7 @@ extension TestAffineTransform { equals: CGPoint(x: 0, y: 20) ) - assert( + check( point: CGPoint(x: 10, y: 10), // Scale by (2, 0) transformedBy: { @@ -412,7 +414,7 @@ extension TestAffineTransform { let point = CGPoint(x: 10, y: 15) let newPoint = baseRotation.transform(point) - self.assert( + self.check( point: point, transformedBy: rotation, equals: newPoint, file: file, line: line @@ -453,41 +455,41 @@ extension TestAffineTransform { } func testRotation() { - assert( + check( point: CGPoint(x: 10, y: 15), transformedBy: AffineTransform(rotationByDegrees: 0), equals: CGPoint(x: 10, y: 15) ) - assert( + check( point: CGPoint(x: 10, y: 15), transformedBy: AffineTransform(rotationByDegrees: 1080), equals: CGPoint(x: 10, y: 15) ) // Counter-clockwise rotation - assert( + check( point: CGPoint(x: 15, y: 10), transformedBy: AffineTransform(rotationByRadians: .pi / 2), equals: CGPoint(x: -10, y: 15) ) // Clockwise rotation - assert( + check( point: CGPoint(x: 15, y: 10), transformedBy: AffineTransform(rotationByDegrees: -90), equals: CGPoint(x: 10, y: -15) ) // Reflect about origin - assert( + check( point: CGPoint(x: 10, y: 15), transformedBy: AffineTransform(rotationByRadians: .pi), equals: CGPoint(x: -10, y: -15) ) // Composed reflection about origin - assert( + check( point: CGPoint(x: 10, y: 15), // Rotate by 180º transformedBy: { @@ -507,7 +509,7 @@ extension TestAffineTransform { extension TestAffineTransform { func testTranslationScaling() { - assert( + check( point: CGPoint(x: 1, y: 3), // Translate by (2, 0) then scale by (5, -5) transformedBy: { @@ -515,19 +517,17 @@ extension TestAffineTransform { transform.append(.init(translationByX: 2, byY: 0)) transform.append(.init(scaleByX: 5, byY: -5)) - print(">>>>>>>>>>>>>>>>>", transform) transform = .identity transform.translate(x: 2, y: 0) transform.scale(x: 5, y: -5) - print(">>>>>>>>>>>>>>>>>", transform) return transform }(), equals: CGPoint(x: 15, y: -15) ) - assert( + check( point: CGPoint(x: 3, y: 1), // Scale by (-5, 5) then scale by (0, 10) transformedBy: { @@ -543,7 +543,7 @@ extension TestAffineTransform { } func testTranslationRotation() { - assert( + check( point: CGPoint(x: 10, y: 10), // Translate by (20, -5) then rotate by 90º transformedBy: { @@ -557,7 +557,7 @@ extension TestAffineTransform { equals: CGPoint(x: -5, y: 30) ) - assert( + check( point: CGPoint(x: 10, y: 10), // Rotate by 180º and then translate by (20, 15) transformedBy: { @@ -573,7 +573,7 @@ extension TestAffineTransform { } func testScalingRotation() { - assert( + check( point: CGPoint(x: 20, y: 5), // Scale by (0.5, 3) then rotate by -90º transformedBy: { @@ -587,7 +587,7 @@ extension TestAffineTransform { equals: CGPoint(x: 15, y: -10) ) - assert( + check( point: CGPoint(x: 20, y: 5), // Rotate by -90º the scale by (0.5, 3) transformedBy: { @@ -634,7 +634,7 @@ extension TestAffineTransform { return transform }() - assert( + check( point: CGPoint(x: 10, y: 10), transformedBy: recoveredIdentity, equals: CGPoint(x: 10, y: 10) @@ -646,7 +646,7 @@ extension TestAffineTransform { extension TestAffineTransform { func testPrependTransform() { - assert( + check( point: CGPoint(x: 10, y: 15), transformedBy: { var transform = AffineTransform.identity @@ -656,7 +656,7 @@ extension TestAffineTransform { equals: CGPoint(x: 10, y: 15) ) - assert( + check( point: CGPoint(x: 10, y: 15), // Scale by 2 then translate by (10, 0) transformedBy: { @@ -674,7 +674,7 @@ extension TestAffineTransform { } func testAppendTransform() { - assert( + check( point: CGPoint(x: 10, y: 15), transformedBy: { var transform = AffineTransform.identity @@ -684,7 +684,7 @@ extension TestAffineTransform { equals: CGPoint(x: 10, y: 15) ) - assert( + check( point: CGPoint(x: 10, y: 15), // Translate by (10, 0) then scale by 2 transformedBy: { From db4707369cb7f0f1f570a9406c9cd748128c313c Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Sun, 29 Aug 2021 15:19:54 +0300 Subject: [PATCH 07/11] Remove debug code from `testTranslationScaling()`. --- Tests/Foundation/Tests/TestAffineTransform.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Tests/Foundation/Tests/TestAffineTransform.swift b/Tests/Foundation/Tests/TestAffineTransform.swift index 11c0732190..35760d7129 100644 --- a/Tests/Foundation/Tests/TestAffineTransform.swift +++ b/Tests/Foundation/Tests/TestAffineTransform.swift @@ -515,10 +515,6 @@ extension TestAffineTransform { transformedBy: { var transform = AffineTransform.identity - transform.append(.init(translationByX: 2, byY: 0)) - transform.append(.init(scaleByX: 5, byY: -5)) - - transform = .identity transform.translate(x: 2, y: 0) transform.scale(x: 5, y: -5) From 127f75f07b0cb3c7bfcdc939a48f60c609a85806 Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Sun, 29 Aug 2021 15:25:20 +0300 Subject: [PATCH 08/11] Fix ns-testable-import `#if`-directive code. --- Tests/Foundation/Tests/TestAffineTransform.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/Foundation/Tests/TestAffineTransform.swift b/Tests/Foundation/Tests/TestAffineTransform.swift index 35760d7129..fc961807ce 100644 --- a/Tests/Foundation/Tests/TestAffineTransform.swift +++ b/Tests/Foundation/Tests/TestAffineTransform.swift @@ -128,8 +128,9 @@ extension TestAffineTransform { tX: 5, tY: 6 ) - #if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT let nsTransform = NSAffineTransform(transform: transform) + + #if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT XCTAssertEqual(transform, nsTransform.affineTransform) #endif From 3a31d341ac122c6f266e22df88cb4bd21c96ff40 Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Tue, 31 Aug 2021 14:40:23 +0300 Subject: [PATCH 09/11] [AffineTransform] Fix size transformation. --- Sources/Foundation/AffineTransform.swift | 7 +- .../Tests/TestAffineTransform.swift | 274 +++++++++++------- 2 files changed, 168 insertions(+), 113 deletions(-) diff --git a/Sources/Foundation/AffineTransform.swift b/Sources/Foundation/AffineTransform.swift index 8be7293e33..8119111721 100644 --- a/Sources/Foundation/AffineTransform.swift +++ b/Sources/Foundation/AffineTransform.swift @@ -268,9 +268,10 @@ extension AffineTransform { /// Applies the transform to the specified size and returns the result. public func transform(_ size: CGSize) -> CGSize { - let newVector = transform(CGPoint(x: size.width, y: size.height)) - - return CGSize(width: newVector.x, height: newVector.y) + CGSize( + width : (m11 * size.width) + (m21 * size.height), + height: (m12 * size.width) + (m22 * size.height) + ) } } diff --git a/Tests/Foundation/Tests/TestAffineTransform.swift b/Tests/Foundation/Tests/TestAffineTransform.swift index fc961807ce..eab1ec57ed 100644 --- a/Tests/Foundation/Tests/TestAffineTransform.swift +++ b/Tests/Foundation/Tests/TestAffineTransform.swift @@ -49,28 +49,19 @@ class TestAffineTransform: XCTestCase { extension TestAffineTransform { func check( - point: CGPoint, - transformedBy transform: AffineTransform, - equals expectedPoint: CGPoint, + vector: CGVector, + withTransform transform: AffineTransform, + mapsToPoint expectedPoint: CGPoint, + mapsToSize expectedSize: CGSize, _ message: String = "", file: StaticString = #file, line: UInt = #line ) { - let size = CGSize(width: point.x, height: point.y) + let point = CGPoint(x: vector.dx, y: vector.dy) + let size = CGSize(width: vector.dx, height: vector.dy) let newPoint = transform.transform(point) let newSize = transform.transform(size) - XCTAssertEqual( - newPoint.x, newSize.width, - "Expected point x and size width to match", - file: file, line: line - ) - XCTAssertEqual( - newPoint.y, newSize.height, - "Expected point y and size height to match", - file: file, line: line - ) - let nsTransform = transform as NSAffineTransform XCTAssertEqual( nsTransform.transform(point), newPoint, @@ -96,6 +87,19 @@ extension TestAffineTransform { "Invalid y: \(message)", file: file, line: line ) + + XCTAssertEqual( + newSize.width, expectedSize.width, + accuracy: accuracyThreshold, + "Invalid width: \(message)", + file: file, line: line + ) + XCTAssertEqual( + newSize.height, expectedSize.height, + accuracy: accuracyThreshold, + "Invalid height: \(message)", + file: file, line: line + ) } } @@ -174,40 +178,57 @@ extension TestAffineTransform { extension TestAffineTransform { func testVectorTransformations() { - // To transform a given point with coordinated px and py, + // To transform a given size with coordinates w and h, // we do: // - // [ x' y' ] + // [ w' h' ] = [ w h ] * [ m11 m12 ] + // [ m21 m22 ] // - // = [ px py ] * [ m11 m12 ] + [ tX tY ] - // [ m21 m22 ] + // = [ w*m11+h*m21 w*m12+h*m22 ] // - // = [ px*m11+py*m21+tX px*m12+py*m22+tY ] + // To find the transformed point with coordinates x, y + // where x=w and y=h, we simply add the translation vector + // [tX, tX] to our previous result: + // + // [ p' y' ] = [ w' h' ] + [ tX tY ] + // = [ x*m11+y*m21+tX x*m12+y*m22+tY ] check( - point: CGPoint(x: 10, y: 20), - transformedBy: AffineTransform( + vector: CGVector(dx: 10, dy: 20), + withTransform: AffineTransform( m11: 1, m12: 2, m21: 3, m22: 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 ] - equals: CGPoint(x: 75, y: 106) + mapsToPoint: CGPoint(x: 75, y: 106), + + // [ px*m11+py*m21 px*m12+py*m22 ] + // [ 10*1+20*3 10*2+20*4 ] + // [ 70 100 ] + mapsToSize: CGSize(width: 70, height: 100) ) check( - point: CGPoint(x: 5, y: 25), - transformedBy: AffineTransform( + vector: CGVector(dx: 5, dy: 25), + withTransform: AffineTransform( m11: 5, m12: 4, m21: 3, m22: 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 ] - equals: CGPoint(x: 101, y: 70) + // [ 5*5+25*3+1 5*4+25*2+0 ] + // [ 101 70 ] + mapsToPoint: CGPoint(x: 101, y: 70), + + // [ px*m11+py*m21 px*m12+py*m22 ] + // [ 5*5+25*3 5*4+25*2 ] + // [ 100 70 ] + mapsToSize: CGSize(width: 100, height: 70) ) } } @@ -233,9 +254,10 @@ extension TestAffineTransform { func testIdentity() { check( - point: CGPoint(x: 25, y: 10), - transformedBy: .identity, - equals: CGPoint(x: 25, y: 10) + vector: CGVector(dx: 25, dy: 10), + withTransform: .identity, + mapsToPoint: CGPoint(x: 25, y: 10), + mapsToSize: CGSize(width: 25, height: 10) ) } } @@ -266,33 +288,36 @@ extension TestAffineTransform { func testTranslation() { check( - point: CGPoint(x: 10, y: 10), - transformedBy: AffineTransform( + vector: CGVector(dx: 10, dy: 10), + withTransform: AffineTransform( translationByX: 0, byY: 0 ), - equals: CGPoint(x: 10, y: 10) + mapsToPoint: CGPoint(x: 10, y: 10), + mapsToSize: CGSize(width: 10, height: 10) ) check( - point: CGPoint(x: 10, y: 10), - transformedBy: AffineTransform( + vector: CGVector(dx: 10, dy: 10), + withTransform: AffineTransform( translationByX: 0, byY: 5 ), - equals: CGPoint(x: 10, y: 15) + mapsToPoint: CGPoint(x: 10, y: 15), + mapsToSize: CGSize(width: 10, height: 10) ) check( - point: CGPoint(x: 10, y: 10), - transformedBy: AffineTransform( + vector: CGVector(dx: 10, dy: 10), + withTransform: AffineTransform( translationByX: 5, byY: 5 ), - equals: CGPoint(x: 15, y: 15) + mapsToPoint: CGPoint(x: 15, y: 15), + mapsToSize: CGSize(width: 10, height: 10) ) check( - point: CGPoint(x: -2, y: -3), + vector: CGVector(dx: -2, dy: -3), // Translate by 5 - transformedBy: { + withTransform: { var transform = AffineTransform.identity transform.translate(x: 2, y: 3) @@ -300,7 +325,8 @@ extension TestAffineTransform { return transform }(), - equals: CGPoint(x: 3, y: 2) + mapsToPoint: CGPoint(x: 3, y: 2), + mapsToSize: CGSize(width: -2, height: -3) ) } } @@ -360,33 +386,36 @@ extension TestAffineTransform { func testScaling() { check( - point: CGPoint(x: 10, y: 10), - transformedBy: AffineTransform( + vector: CGVector(dx: 10, dy: 10), + withTransform: AffineTransform( scaleByX: 1, byY: 0 ), - equals: CGPoint(x: 10, y: 0) + mapsToPoint: CGPoint(x: 10, y: 0), + mapsToSize: CGSize(width: 10, height: 0) ) check( - point: CGPoint(x: 10, y: 10), - transformedBy: AffineTransform( + vector: CGVector(dx: 10, dy: 10), + withTransform: AffineTransform( scaleByX: 0.5, byY: 1 ), - equals: CGPoint(x: 5, y: 10) + mapsToPoint: CGPoint(x: 5, y: 10), + mapsToSize: CGSize(width: 5, height: 10) ) check( - point: CGPoint(x: 10, y: 10), - transformedBy: AffineTransform( + vector: CGVector(dx: 10, dy: 10), + withTransform: AffineTransform( scaleByX: 0, byY: 2 ), - equals: CGPoint(x: 0, y: 20) + mapsToPoint: CGPoint(x: 0, y: 20), + mapsToSize: CGSize(width: 0, height: 20) ) check( - point: CGPoint(x: 10, y: 10), + vector: CGVector(dx: 10, dy: 10), // Scale by (2, 0) - transformedBy: { + withTransform: { var transform = AffineTransform.identity transform.scale(x: 4, y: 0) @@ -394,7 +423,8 @@ extension TestAffineTransform { return transform }(), - equals: CGPoint(x: 20, y: 0) + mapsToPoint: CGPoint(x: 20, y: 0), + mapsToSize: CGSize(width: 20, height: 0) ) } } @@ -412,12 +442,16 @@ extension TestAffineTransform { file: StaticString = #file, line: UInt = #line ) { - let point = CGPoint(x: 10, y: 15) - let newPoint = baseRotation.transform(point) + let vector = CGVector(dx: 10, dy: 15) self.check( - point: point, transformedBy: rotation, - equals: newPoint, + vector: vector, withTransform: rotation, + mapsToPoint: baseRotation.transform( + CGPoint(x: vector.dx, y: vector.dy) + ), + mapsToSize: baseRotation.transform( + CGSize(width: vector.dx, height: vector.dy) + ), file: file, line: line ) } @@ -457,43 +491,48 @@ extension TestAffineTransform { func testRotation() { check( - point: CGPoint(x: 10, y: 15), - transformedBy: AffineTransform(rotationByDegrees: 0), - equals: CGPoint(x: 10, y: 15) + vector: CGVector(dx: 10, dy: 15), + withTransform: AffineTransform(rotationByDegrees: 0), + mapsToPoint: CGPoint(x: 10, y: 15), + mapsToSize: CGSize(width: 10, height: 15) ) check( - point: CGPoint(x: 10, y: 15), - transformedBy: AffineTransform(rotationByDegrees: 1080), - equals: CGPoint(x: 10, y: 15) + vector: CGVector(dx: 10, dy: 15), + withTransform: AffineTransform(rotationByDegrees: 1080), + mapsToPoint: CGPoint(x: 10, y: 15), + mapsToSize: CGSize(width: 10, height: 15) ) // Counter-clockwise rotation check( - point: CGPoint(x: 15, y: 10), - transformedBy: AffineTransform(rotationByRadians: .pi / 2), - equals: CGPoint(x: -10, y: 15) + vector: CGVector(dx: 15, dy: 10), + withTransform: AffineTransform(rotationByRadians: .pi / 2), + mapsToPoint: CGPoint(x: -10, y: 15), + mapsToSize: CGSize(width: -10, height: 15) ) // Clockwise rotation check( - point: CGPoint(x: 15, y: 10), - transformedBy: AffineTransform(rotationByDegrees: -90), - equals: CGPoint(x: 10, y: -15) + vector: CGVector(dx: 15, dy: 10), + withTransform: AffineTransform(rotationByDegrees: -90), + mapsToPoint: CGPoint(x: 10, y: -15), + mapsToSize: CGSize(width: 10, height: -15) ) // Reflect about origin check( - point: CGPoint(x: 10, y: 15), - transformedBy: AffineTransform(rotationByRadians: .pi), - equals: CGPoint(x: -10, y: -15) + vector: CGVector(dx: 10, dy: 15), + withTransform: AffineTransform(rotationByRadians: .pi), + mapsToPoint: CGPoint(x: -10, y: -15), + mapsToSize: CGSize(width: -10, height: -15) ) // Composed reflection about origin check( - point: CGPoint(x: 10, y: 15), + vector: CGVector(dx: 10, dy: 15), // Rotate by 180º - transformedBy: { + withTransform: { var transform = AffineTransform.identity transform.rotate(byDegrees: 90) @@ -501,7 +540,8 @@ extension TestAffineTransform { return transform }(), - equals: CGPoint(x: -10, y: -15) + mapsToPoint: CGPoint(x: -10, y: -15), + mapsToSize: CGSize(width: -10, height: -15) ) } } @@ -511,9 +551,9 @@ extension TestAffineTransform { extension TestAffineTransform { func testTranslationScaling() { check( - point: CGPoint(x: 1, y: 3), + vector: CGVector(dx: 1, dy: 3), // Translate by (2, 0) then scale by (5, -5) - transformedBy: { + withTransform: { var transform = AffineTransform.identity transform.translate(x: 2, y: 0) @@ -521,13 +561,17 @@ extension TestAffineTransform { return transform }(), - equals: CGPoint(x: 15, y: -15) + mapsToPoint: CGPoint(x: 15, y: -15), + // [ 5 0 ] + // [ 0 -5 ] + // [ 10 0 ] + mapsToSize: CGSize(width: 5, height: -15) ) check( - point: CGPoint(x: 3, y: 1), + vector: CGVector(dx: 3, dy: 1), // Scale by (-5, 5) then scale by (0, 10) - transformedBy: { + withTransform: { var transform = AffineTransform.identity transform.scale(x: -5, y: 5) @@ -535,15 +579,16 @@ extension TestAffineTransform { return transform }(), - equals: CGPoint(x: -15, y: 15) + mapsToPoint: CGPoint(x: -15, y: 15), + mapsToSize: CGSize(width: -15, height: 5) ) } func testTranslationRotation() { check( - point: CGPoint(x: 10, y: 10), + vector: CGVector(dx: 10, dy: 10), // Translate by (20, -5) then rotate by 90º - transformedBy: { + withTransform: { var transform = AffineTransform.identity transform.translate(x: 20, y: -5) @@ -551,13 +596,14 @@ extension TestAffineTransform { return transform }(), - equals: CGPoint(x: -5, y: 30) + mapsToPoint: CGPoint(x: -5, y: 30), + mapsToSize: CGSize(width: -10, height: 10) ) check( - point: CGPoint(x: 10, y: 10), + vector: CGVector(dx: 10, dy: 10), // Rotate by 180º and then translate by (20, 15) - transformedBy: { + withTransform: { var transform = AffineTransform.identity transform.rotate(byDegrees: 180) @@ -565,15 +611,16 @@ extension TestAffineTransform { return transform }(), - equals: CGPoint(x: 10, y: 5) + mapsToPoint: CGPoint(x: 10, y: 5), + mapsToSize: CGSize(width: -10, height: -10) ) } func testScalingRotation() { check( - point: CGPoint(x: 20, y: 5), + vector: CGVector(dx: 20, dy: 5), // Scale by (0.5, 3) then rotate by -90º - transformedBy: { + withTransform: { var transform = AffineTransform.identity transform.scale(x: 0.5, y: 3) @@ -581,13 +628,14 @@ extension TestAffineTransform { return transform }(), - equals: CGPoint(x: 15, y: -10) + mapsToPoint: CGPoint(x: 15, y: -10), + mapsToSize: CGSize(width: 15, height: -10) ) check( - point: CGPoint(x: 20, y: 5), + vector: CGVector(dx: 20, dy: 5), // Rotate by -90º the scale by (0.5, 3) - transformedBy: { + withTransform: { var transform = AffineTransform.identity transform.rotate(byDegrees: -90) @@ -595,7 +643,8 @@ extension TestAffineTransform { return transform }(), - equals: CGPoint(x: 15, y: 10) + mapsToPoint: CGPoint(x: 15, y: 10), + mapsToSize: CGSize(width: 15, height: 10) ) } } @@ -632,9 +681,10 @@ extension TestAffineTransform { }() check( - point: CGPoint(x: 10, y: 10), - transformedBy: recoveredIdentity, - equals: CGPoint(x: 10, y: 10) + vector: CGVector(dx: 10, dy: 10), + withTransform: recoveredIdentity, + mapsToPoint: CGPoint(x: 10, y: 10), + mapsToSize: CGSize(width: 10, height: 10) ) } } @@ -644,19 +694,20 @@ extension TestAffineTransform { extension TestAffineTransform { func testPrependTransform() { check( - point: CGPoint(x: 10, y: 15), - transformedBy: { + vector: CGVector(dx: 10, dy: 15), + withTransform: { var transform = AffineTransform.identity transform.prepend(.identity) return transform }(), - equals: CGPoint(x: 10, y: 15) + mapsToPoint: CGPoint(x: 10, y: 15), + mapsToSize: CGSize(width: 10, height: 15) ) check( - point: CGPoint(x: 10, y: 15), + vector: CGVector(dx: 10, dy: 15), // Scale by 2 then translate by (10, 0) - transformedBy: { + withTransform: { let scale = AffineTransform(scale: 2) var transform = AffineTransform( @@ -666,25 +717,27 @@ extension TestAffineTransform { return transform }(), - equals: CGPoint(x: 30, y: 30) + mapsToPoint: CGPoint(x: 30, y: 30), + mapsToSize: CGSize(width: 20, height: 30) ) } func testAppendTransform() { check( - point: CGPoint(x: 10, y: 15), - transformedBy: { + vector: CGVector(dx: 10, dy: 15), + withTransform: { var transform = AffineTransform.identity transform.append(.identity) return transform }(), - equals: CGPoint(x: 10, y: 15) + mapsToPoint: CGPoint(x: 10, y: 15), + mapsToSize: CGSize(width: 10, height: 15) ) check( - point: CGPoint(x: 10, y: 15), + vector: CGVector(dx: 10, dy: 15), // Translate by (10, 0) then scale by 2 - transformedBy: { + withTransform: { let scale = AffineTransform(scale: 2) var transform = AffineTransform( @@ -694,7 +747,8 @@ extension TestAffineTransform { return transform }(), - equals: CGPoint(x: 40, y: 30) + mapsToPoint: CGPoint(x: 40, y: 30), + mapsToSize: CGSize(width: 20, height: 30) ) } } From cfed8d03f135d131607ebb070fdda3b63ad83231 Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Sat, 4 Sep 2021 12:25:43 +0300 Subject: [PATCH 10/11] [AffineTransform] Explain the operation calculations. --- Sources/Foundation/AffineTransform.swift | 93 ++++++++++++++++++++---- 1 file changed, 80 insertions(+), 13 deletions(-) diff --git a/Sources/Foundation/AffineTransform.swift b/Sources/Foundation/AffineTransform.swift index 8119111721..34471e8975 100644 --- a/Sources/Foundation/AffineTransform.swift +++ b/Sources/Foundation/AffineTransform.swift @@ -132,27 +132,28 @@ extension AffineTransform { } extension AffineTransform { - /// Creates an affine transformation matrix by combining the receiver with `transformStruct`. - /// That is, it computes `T * M` and returns the result, where `T` is the receiver's and `M` is - /// the `transformStruct`'s affine transformation matrix. - /// The resulting matrix takes the following form: + /// Creates an affine transformation matrix by combining the two matrices `A×B` and returns the result. /// - /// ```swift - /// [ m11_T m12_T 0 ] [ m11_M m12_M 0 ] - /// T * M = [ m21_T m22_T 0 ] [ m21_M m22_M 0 ] - /// [ tX_T tY_T 1 ] [ tX_M tY_M 1 ] - /// ``` + /// The resulting matrix takes the following form /// /// ```swift - /// [ (m11_T*m11_M + m12_T*m21_M) (m11_T*m12_M + m12_T*m22_M) 0 ] - /// = [ (m21_T*m11_M + m22_T*m21_M) (m21_T*m12_M + m22_T*m22_M) 0 ] - /// [ (tX_T*m11_M + tY_T*m21_M + tX_M) (tX_T*m12_M + tY_T*m22_M + tY_M) 1 ] + /// + /// [ a1, b1, 0 ] [ a2, b2, 0 ] + /// A×B = [ c1, d1, 0 ] × [ c2, d2, 0 ] + /// [ x1, y1, 1 ] [ x2, y2, 1 ] + /// + /// [ a1*a2+b1*c2+0*x2 a1*b2+b1*d2+0*y2 a1*0+b1*0+0*1 ] + /// A×B = [ c1*a2+d1*c2+0*x2 c1*b2+d1*d2+0*y2 c1*0+d1*0+0*1 ] + /// [ x1*a2+y1*c2+1*x2 x1*b2+y1*d2+1*y2 x1*0+y1*0+1*1 ] + /// + /// [ a1*a2+b1*c2 a1*b2+b1*d2 0 ] + /// A×B = [ c1*a2+d1*c2 c1*b2+d1*d2 0 ] + /// [ x1*a2+y1*c2+x2 x1*b2+y1*d2+y2 1 ] /// ``` @inline(__always) internal func concatenated(_ other: AffineTransform) -> AffineTransform { let (t, m) = (self, other) - // this could be optimized with a vector version return AffineTransform( m11: (t.m11 * m.m11) + (t.m12 * m.m21), m12: (t.m11 * m.m12) + (t.m12 * m.m22), m21: (t.m21 * m.m11) + (t.m22 * m.m21), m22: (t.m21 * m.m12) + (t.m22 * m.m22), @@ -224,8 +225,59 @@ extension AffineTransform { extension AffineTransform { /// Returns an inverted version of the matrix if possible, or nil if not. public func inverted() -> AffineTransform? { + // We need the matrix of cofactors to calculate the inverse, but first we + // need to calculate the minors of each element — where the minor of an + // element Ai,j is the determinant of the matrix derived from deleting + // the ith row and jth column: + // + // [ |d y| |c x| |c x| ] + // [ |0 1| |0 1| |d y| ] + // [ ] + // [ |b y| |a x| |a x| ] + // M = [ |0 1| |0 1| |b y| ] + // [ ] + // [ |b d| |a c| |a c| ] + // [ |0 0| |0 0| |b d| ] + // + // [ d*1-y*0 c*1-x*0 c*y-x*d ] + // M = [ b*1-y*0 a*1-x*0 a*y-x*b ] + // [ b*0-d*0 a*0-c*0 a*d-c*b ] + // + // [ d c c*y-x*d ] + // M = [ b a a*y-x*b ] + // [ 0 0 |A| ] + // + // Now we can calculate the matrix of cofactors by negating each element Ai,j + // where i+j is odd: + // + // [ d -c c*y-x*d ] + // C = [ -b a -(a*y-x*b) ] + // [ 0 -0 |A| ] + // + // Next, we can find the adjugate matrix, which is the transposed matrix of + // cofactors — a matrix whose ith column is the ith row of the matrix of C: + // + // [ d -b 0 ] + // adj(A) = [ -c a -0 ] + // [ c*y-x*d -(a*y-x*b) |A| ] + // + // Finally, the inverse matrix is the product of the reciprocal of the determinant + // of A times adj(A), assuming that |A|≠0: + // + // A^-1 = (1 / |A|) × adj(A) + // + // [ d/|A| -b/|A| 0/|A| ] + // A^-1 = [ -c/|A| a/|A| -0/|A| ] + // [ (c*y-x*d)/|A| -(a*y-x*b)/|A| |A|/|A| ] + // + // [ d/|A| -b/|A| 0 ] + // A^-1 = [ -c/|A| a/|A| 0 ] + // [ (c*y-x*d)/|A| (x*b-a*y)/|A| 1 ] + let determinant = (m11 * m22) - (m12 * m21) + // We compare to ulp of 0 instead of doing determinant != 0, + // to catch floating-point rounding errors. if abs(determinant) <= CGFloat.zero.ulp { return nil } @@ -260,6 +312,15 @@ extension AffineTransform { extension AffineTransform { /// Applies the transform to the specified point and returns the result. public func transform(_ point: CGPoint) -> CGPoint { + // Multiply the given point matrix with the matrix: + // + // [ m11 m12 0 ] + // [ x' y' 1 ] = [ x y 1 ] × [ m21 m22 0 ] + // [ tX tY 1 ] + // + // [ x' y' 1 ] = [ x*m11+y*m21+1*tX x*m12+y*m22+1*tY x*0+y*0+1*1 ] + // + // [ x' y' 1 ] = [ x*m11+y*m21+tX x*m12+y*m22+tY 1 ] CGPoint( x: (m11 * point.x) + (m21 * point.y) + tX, y: (m12 * point.x) + (m22 * point.y) + tY @@ -268,6 +329,12 @@ extension AffineTransform { /// Applies the transform to the specified size and returns the result. public func transform(_ size: CGSize) -> CGSize { + // Multiply the given size matrix with the scale & rotation matrix: + // + // [ w' h' ] = [ w h ] * [ m11 m12 ] + // [ m21 m22 ] + // + // [ w' h' ] = [ w*m11+h*m21 w*m12+h*m22 ] CGSize( width : (m11 * size.width) + (m21 * size.height), height: (m12 * size.width) + (m22 * size.height) From af044a22287537fdb97faedbae861e2df6a45afe Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Tue, 7 Sep 2021 13:25:09 +0300 Subject: [PATCH 11/11] [AffineTransform Tests] Add platform-independent Vector type. --- .../Tests/TestAffineTransform.swift | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/Tests/Foundation/Tests/TestAffineTransform.swift b/Tests/Foundation/Tests/TestAffineTransform.swift index eab1ec57ed..e322bc66db 100644 --- a/Tests/Foundation/Tests/TestAffineTransform.swift +++ b/Tests/Foundation/Tests/TestAffineTransform.swift @@ -15,6 +15,14 @@ #endif #endif +// MARK: - Vector + +// CGVector is only available on Darwin. +public struct Vector { + let dx: CGFloat + let dy: CGFloat +} + // MARK: - Tests class TestAffineTransform: XCTestCase { @@ -49,7 +57,7 @@ class TestAffineTransform: XCTestCase { extension TestAffineTransform { func check( - vector: CGVector, + vector: Vector, withTransform transform: AffineTransform, mapsToPoint expectedPoint: CGPoint, mapsToSize expectedSize: CGSize, @@ -194,7 +202,7 @@ extension TestAffineTransform { // = [ x*m11+y*m21+tX x*m12+y*m22+tY ] check( - vector: CGVector(dx: 10, dy: 20), + vector: Vector(dx: 10, dy: 20), withTransform: AffineTransform( m11: 1, m12: 2, m21: 3, m22: 4, @@ -213,7 +221,7 @@ extension TestAffineTransform { ) check( - vector: CGVector(dx: 5, dy: 25), + vector: Vector(dx: 5, dy: 25), withTransform: AffineTransform( m11: 5, m12: 4, m21: 3, m22: 2, @@ -254,7 +262,7 @@ extension TestAffineTransform { func testIdentity() { check( - vector: CGVector(dx: 25, dy: 10), + vector: Vector(dx: 25, dy: 10), withTransform: .identity, mapsToPoint: CGPoint(x: 25, y: 10), mapsToSize: CGSize(width: 25, height: 10) @@ -288,7 +296,7 @@ extension TestAffineTransform { func testTranslation() { check( - vector: CGVector(dx: 10, dy: 10), + vector: Vector(dx: 10, dy: 10), withTransform: AffineTransform( translationByX: 0, byY: 0 ), @@ -297,7 +305,7 @@ extension TestAffineTransform { ) check( - vector: CGVector(dx: 10, dy: 10), + vector: Vector(dx: 10, dy: 10), withTransform: AffineTransform( translationByX: 0, byY: 5 ), @@ -306,7 +314,7 @@ extension TestAffineTransform { ) check( - vector: CGVector(dx: 10, dy: 10), + vector: Vector(dx: 10, dy: 10), withTransform: AffineTransform( translationByX: 5, byY: 5 ), @@ -315,7 +323,7 @@ extension TestAffineTransform { ) check( - vector: CGVector(dx: -2, dy: -3), + vector: Vector(dx: -2, dy: -3), // Translate by 5 withTransform: { var transform = AffineTransform.identity @@ -386,7 +394,7 @@ extension TestAffineTransform { func testScaling() { check( - vector: CGVector(dx: 10, dy: 10), + vector: Vector(dx: 10, dy: 10), withTransform: AffineTransform( scaleByX: 1, byY: 0 ), @@ -395,7 +403,7 @@ extension TestAffineTransform { ) check( - vector: CGVector(dx: 10, dy: 10), + vector: Vector(dx: 10, dy: 10), withTransform: AffineTransform( scaleByX: 0.5, byY: 1 ), @@ -404,7 +412,7 @@ extension TestAffineTransform { ) check( - vector: CGVector(dx: 10, dy: 10), + vector: Vector(dx: 10, dy: 10), withTransform: AffineTransform( scaleByX: 0, byY: 2 ), @@ -413,7 +421,7 @@ extension TestAffineTransform { ) check( - vector: CGVector(dx: 10, dy: 10), + vector: Vector(dx: 10, dy: 10), // Scale by (2, 0) withTransform: { var transform = AffineTransform.identity @@ -442,7 +450,7 @@ extension TestAffineTransform { file: StaticString = #file, line: UInt = #line ) { - let vector = CGVector(dx: 10, dy: 15) + let vector = Vector(dx: 10, dy: 15) self.check( vector: vector, withTransform: rotation, @@ -491,14 +499,14 @@ extension TestAffineTransform { func testRotation() { check( - vector: CGVector(dx: 10, dy: 15), + vector: Vector(dx: 10, dy: 15), withTransform: AffineTransform(rotationByDegrees: 0), mapsToPoint: CGPoint(x: 10, y: 15), mapsToSize: CGSize(width: 10, height: 15) ) check( - vector: CGVector(dx: 10, dy: 15), + vector: Vector(dx: 10, dy: 15), withTransform: AffineTransform(rotationByDegrees: 1080), mapsToPoint: CGPoint(x: 10, y: 15), mapsToSize: CGSize(width: 10, height: 15) @@ -506,7 +514,7 @@ extension TestAffineTransform { // Counter-clockwise rotation check( - vector: CGVector(dx: 15, dy: 10), + vector: Vector(dx: 15, dy: 10), withTransform: AffineTransform(rotationByRadians: .pi / 2), mapsToPoint: CGPoint(x: -10, y: 15), mapsToSize: CGSize(width: -10, height: 15) @@ -514,7 +522,7 @@ extension TestAffineTransform { // Clockwise rotation check( - vector: CGVector(dx: 15, dy: 10), + vector: Vector(dx: 15, dy: 10), withTransform: AffineTransform(rotationByDegrees: -90), mapsToPoint: CGPoint(x: 10, y: -15), mapsToSize: CGSize(width: 10, height: -15) @@ -522,7 +530,7 @@ extension TestAffineTransform { // Reflect about origin check( - vector: CGVector(dx: 10, dy: 15), + vector: Vector(dx: 10, dy: 15), withTransform: AffineTransform(rotationByRadians: .pi), mapsToPoint: CGPoint(x: -10, y: -15), mapsToSize: CGSize(width: -10, height: -15) @@ -530,7 +538,7 @@ extension TestAffineTransform { // Composed reflection about origin check( - vector: CGVector(dx: 10, dy: 15), + vector: Vector(dx: 10, dy: 15), // Rotate by 180º withTransform: { var transform = AffineTransform.identity @@ -551,7 +559,7 @@ extension TestAffineTransform { extension TestAffineTransform { func testTranslationScaling() { check( - vector: CGVector(dx: 1, dy: 3), + vector: Vector(dx: 1, dy: 3), // Translate by (2, 0) then scale by (5, -5) withTransform: { var transform = AffineTransform.identity @@ -569,7 +577,7 @@ extension TestAffineTransform { ) check( - vector: CGVector(dx: 3, dy: 1), + vector: Vector(dx: 3, dy: 1), // Scale by (-5, 5) then scale by (0, 10) withTransform: { var transform = AffineTransform.identity @@ -586,7 +594,7 @@ extension TestAffineTransform { func testTranslationRotation() { check( - vector: CGVector(dx: 10, dy: 10), + vector: Vector(dx: 10, dy: 10), // Translate by (20, -5) then rotate by 90º withTransform: { var transform = AffineTransform.identity @@ -601,7 +609,7 @@ extension TestAffineTransform { ) check( - vector: CGVector(dx: 10, dy: 10), + vector: Vector(dx: 10, dy: 10), // Rotate by 180º and then translate by (20, 15) withTransform: { var transform = AffineTransform.identity @@ -618,7 +626,7 @@ extension TestAffineTransform { func testScalingRotation() { check( - vector: CGVector(dx: 20, dy: 5), + vector: Vector(dx: 20, dy: 5), // Scale by (0.5, 3) then rotate by -90º withTransform: { var transform = AffineTransform.identity @@ -633,7 +641,7 @@ extension TestAffineTransform { ) check( - vector: CGVector(dx: 20, dy: 5), + vector: Vector(dx: 20, dy: 5), // Rotate by -90º the scale by (0.5, 3) withTransform: { var transform = AffineTransform.identity @@ -681,7 +689,7 @@ extension TestAffineTransform { }() check( - vector: CGVector(dx: 10, dy: 10), + vector: Vector(dx: 10, dy: 10), withTransform: recoveredIdentity, mapsToPoint: CGPoint(x: 10, y: 10), mapsToSize: CGSize(width: 10, height: 10) @@ -694,7 +702,7 @@ extension TestAffineTransform { extension TestAffineTransform { func testPrependTransform() { check( - vector: CGVector(dx: 10, dy: 15), + vector: Vector(dx: 10, dy: 15), withTransform: { var transform = AffineTransform.identity transform.prepend(.identity) @@ -705,7 +713,7 @@ extension TestAffineTransform { ) check( - vector: CGVector(dx: 10, dy: 15), + vector: Vector(dx: 10, dy: 15), // Scale by 2 then translate by (10, 0) withTransform: { let scale = AffineTransform(scale: 2) @@ -724,7 +732,7 @@ extension TestAffineTransform { func testAppendTransform() { check( - vector: CGVector(dx: 10, dy: 15), + vector: Vector(dx: 10, dy: 15), withTransform: { var transform = AffineTransform.identity transform.append(.identity) @@ -735,7 +743,7 @@ extension TestAffineTransform { ) check( - vector: CGVector(dx: 10, dy: 15), + vector: Vector(dx: 10, dy: 15), // Translate by (10, 0) then scale by 2 withTransform: { let scale = AffineTransform(scale: 2)