From be5ef72c8be1c1f3c3d2f1681f9b9ee252bf6f24 Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Tue, 20 Jul 2021 01:51:56 +0300 Subject: [PATCH 1/7] Fix CGAffineTransform implementation. --- Sources/TokamakCore/Stubs/CGStubs.swift | 148 ++++++++++++++---------- 1 file changed, 88 insertions(+), 60 deletions(-) diff --git a/Sources/TokamakCore/Stubs/CGStubs.swift b/Sources/TokamakCore/Stubs/CGStubs.swift index 67e03a5fd..c685c88fd 100644 --- a/Sources/TokamakCore/Stubs/CGStubs.swift +++ b/Sources/TokamakCore/Stubs/CGStubs.swift @@ -22,6 +22,7 @@ extension CGPoint { func rotate(_ angle: Angle, around origin: Self) -> Self { let cosAngle = CGFloat(cos(angle.radians)) let sinAngle = CGFloat(sin(angle.radians)) + return .init( x: cosAngle * (x - origin.x) - sinAngle * (y - origin.y) + origin.x, y: sinAngle * (x - origin.x) + cosAngle * (y - origin.y) + origin.y @@ -69,24 +70,28 @@ public enum CGLineJoin { /// a b 0 /// c d 0 /// tx ty 1 -public struct CGAffineTransform: Equatable { +public struct CGAffineTransform: Equatable, Codable { + /// The value at position [1,1] in the matrix. public var a: CGFloat + /// The value at position [1,2] in the matrix. public var b: CGFloat + /// The value at position [2,1] in the matrix. public var c: CGFloat + /// The value at position [2,2] in the matrix. public var d: CGFloat + /// The value at position [3,1] in the matrix. public var tx: CGFloat + /// The value at position [3,2] in the matrix. public var ty: CGFloat - - /// The identity matrix - public static let identity: Self = .init( - a: 1, - b: 0, // 0 - c: 0, - d: 1, // 0 - tx: 0, - ty: 0 - ) // 1 - + + /// Creates an affine transform with the given matrix values. + /// - Parameters: + /// - a: The value at position [1,1] in the matrix. + /// - b: The value at position [1,2] in the matrix. + /// - c: The value at position [2,1] in the matrix. + /// - d: The value at position [2,2] in the matrix. + /// - tx: The value at position [3,1] in the matrix. + /// - ty: The value at position [3,2] in the matrix. public init( a: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat, @@ -99,14 +104,40 @@ public struct CGAffineTransform: Equatable { self.tx = tx self.ty = ty } +} + +extension CGAffineTransform { + /// The identity matrix. + public static let identity = Self( + a: 1, b: 0, // 0 + c: 0, d: 1, // 0 + tx: 0, ty: 0 // 1 + ) + + public init() { + self = .identity + } + + public var isIdentity: Bool { + self == .identity + } +} +extension CGAffineTransform { /// Returns an affine transformation matrix constructed from a rotation value you provide. /// - Parameters: - /// - angle: The angle, in radians, by which this matrix rotates the coordinate system axes. - /// A positive value specifies clockwise rotation and anegative value specifies - /// counterclockwise rotation. + /// - angle: The angle, in radians, by which this matrix rotates the coordinate + /// system axes. A positive value specifies clockwise rotation and a negative value + /// specifies counterclockwise rotation. public init(rotationAngle angle: CGFloat) { - self.init(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0) + let angleSine = sin(angle) + let angleCosine = cos(angle) + + self.init( + a: angleCosine, b: angleSine, + c: -angleSine, d: angleCosine, + tx: 0, ty: 0 + ) } /// Returns an affine transformation matrix constructed from scaling values you provide. @@ -115,12 +146,9 @@ public struct CGAffineTransform: Equatable { /// - sy: The factor by which to scale the y-axis of the coordinate system. public init(scaleX sx: CGFloat, y sy: CGFloat) { self.init( - a: sx, - b: 0, - c: 0, - d: sy, - tx: 0, - ty: 0 + a: sx, b: 0, + c: 0, d: sy, + tx: 0, ty: 0 ) } @@ -130,60 +158,64 @@ public struct CGAffineTransform: Equatable { /// - ty: The value by which to move the y-axis of the coordinate system. public init(translationX tx: CGFloat, y ty: CGFloat) { self.init( - a: 1, - b: 0, - c: 0, - d: 1, - tx: tx, - ty: ty + a: 1, b: 0, + c: 0, d: 1, + tx: tx, ty: ty ) } +} +extension CGAffineTransform { /// Returns an affine transformation matrix constructed by combining two existing affine /// transforms. + /// + /// Note that concatenation is not commutative, meaning that order is important. For instance, `t1.concatenating(t2)` != `t2.concatenating(t1)` — where `t1` and + /// `t2` are`CGAffineTransform` instances. + /// /// - Parameters: /// - t2: The affine transform to concatenate to this affine transform. /// - Returns: A new affine transformation matrix. That is, `t’ = t1*t2`. public func concatenating(_ t2: Self) -> Self { - let t1m = [[a, b, 0], - [c, d, 0], - [tx, ty, 1]] - let t2m = [[t2.a, t2.b, 0], - [t2.c, t2.d, 0], - [t2.tx, t2.ty, 1]] - var res: [[CGFloat]] = [[0, 0, 0], - [0, 0, 0], - [0, 0, 0]] - for i in 0..<3 { - for j in 0..<3 { - res[i][j] = 0 - for k in 0..<3 { - res[i][j] += t1m[i][k] * t2m[k][j] - } - } - } - return .init( - a: res[0][0], - b: res[0][1], - c: res[1][0], - d: res[1][1], - tx: res[2][0], - ty: res[2][1] + let t1 = self + + return AffineTransform( + a: (t1.a * t2.a) + (t1.b * t2.c), + b: (t1.a * t2.b) + (t1.b * t2.d), + c: (t1.c * t2.a) + (t1.d * t2.c), + d: (t1.c * t2.b) + (t1.d * t2.d), + tX: (t1.tX * t2.a) + (t1.tY * t2.c) + t2.tX, + tY: (t1.tX * t2.b) + (t1.tY * t2.d) + t2.tY ) } +} +extension CGAffineTransform { /// Returns an affine transformation matrix constructed by inverting an existing affine transform. + /// - Returns: A new affine transformation matrix. If the affine transform passed in + /// parameter t cannot be inverted, the affine transform is returned unchanged. public func inverted() -> Self { - .init(a: -a, b: -b, c: -c, d: -d, tx: -tx, ty: -ty) + let determinant = (a * d) - (b * c) + + guard determinant != 0 else { return self } + + return Self( + a: d / determinant, b: -b / determinant, + c: -c / determinant, d: a / determinant, + tx: (c * tY - d * tX) / determinant, + ty: (b * tX - a * tY) / determinant + ) } +} +// TODO: - Optimize operators. +extension CGAffineTransform { /// Returns an affine transformation matrix constructed by rotating an existing affine transform. /// - Parameters: /// - angle: The angle, in radians, by which to rotate the affine transform. /// A positive value specifies clockwise rotation and a negative value specifies /// counterclockwise rotation. public func rotated(by angle: CGFloat) -> Self { - Self(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0) + Self(rotationAngle: angle).concatenating(self) } /// Returns an affine transformation matrix constructed by translating an existing @@ -192,7 +224,7 @@ public struct CGAffineTransform: Equatable { /// - tx: The value by which to move x values with the affine transform. /// - ty: The value by which to move y values with the affine transform. public func translatedBy(x tx: CGFloat, y ty: CGFloat) -> Self { - .init(a: a, b: b, c: c, d: d, tx: self.tx + tx, ty: self.ty + ty) + Self(translationX: tx, y: ty).concatenating(self) } /// Returns an affine transformation matrix constructed by scaling an existing affine transform. @@ -200,11 +232,7 @@ public struct CGAffineTransform: Equatable { /// - sx: The value by which to scale x values of the affine transform. /// - sy: The value by which to scale y values of the affine transform. public func scaledBy(x sx: CGFloat, y sy: CGFloat) -> Self { - .init(a: a + sx, b: b, c: c, d: d + sy, tx: tx, ty: ty) - } - - public var isIdentity: Bool { - self == Self.identity + Self(scaleX: sx, y: sy).concatenating(self) } } From c627f036ba3b5512b48e9a366c976046380816e5 Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Wed, 21 Jul 2021 17:11:18 +0300 Subject: [PATCH 2/7] [CGAffineTransform] Optimize `translated(by:)` --- Sources/TokamakCore/Stubs/CGStubs.swift | 122 ++++++++++++++---------- 1 file changed, 72 insertions(+), 50 deletions(-) diff --git a/Sources/TokamakCore/Stubs/CGStubs.swift b/Sources/TokamakCore/Stubs/CGStubs.swift index c685c88fd..31d0fbcf0 100644 --- a/Sources/TokamakCore/Stubs/CGStubs.swift +++ b/Sources/TokamakCore/Stubs/CGStubs.swift @@ -22,7 +22,7 @@ extension CGPoint { func rotate(_ angle: Angle, around origin: Self) -> Self { let cosAngle = CGFloat(cos(angle.radians)) let sinAngle = CGFloat(sin(angle.radians)) - + return .init( x: cosAngle * (x - origin.x) - sinAngle * (y - origin.y) + origin.x, y: sinAngle * (x - origin.x) + cosAngle * (y - origin.y) + origin.y @@ -47,7 +47,8 @@ public extension CGAffineTransform { } } -#if !canImport(CoreGraphics) +#warning("Remove `|| true` before merging.") +#if !canImport(CoreGraphics) || true public enum CGLineCap { /// A line with a squared-off end. Extends to the endpoint of the Path. case butt @@ -83,7 +84,7 @@ public struct CGAffineTransform: Equatable, Codable { public var tx: CGFloat /// The value at position [3,2] in the matrix. public var ty: CGFloat - + /// Creates an affine transform with the given matrix values. /// - Parameters: /// - a: The value at position [1,1] in the matrix. @@ -106,37 +107,37 @@ public struct CGAffineTransform: Equatable, Codable { } } -extension CGAffineTransform { +public extension CGAffineTransform { /// The identity matrix. - public static let identity = Self( - a: 1, b: 0, // 0 - c: 0, d: 1, // 0 - tx: 0, ty: 0 // 1 + static let identity = Self( + a: 1, b: 0, // 0 + c: 0, d: 1, // 0 + tx: 0, ty: 0 // 1 ) - - public init() { + + init() { self = .identity } - - public var isIdentity: Bool { + + var isIdentity: Bool { self == .identity } } -extension CGAffineTransform { +public extension CGAffineTransform { /// Returns an affine transformation matrix constructed from a rotation value you provide. /// - Parameters: /// - angle: The angle, in radians, by which this matrix rotates the coordinate /// system axes. A positive value specifies clockwise rotation and a negative value /// specifies counterclockwise rotation. - public init(rotationAngle angle: CGFloat) { + init(rotationAngle angle: CGFloat) { let angleSine = sin(angle) let angleCosine = cos(angle) - + self.init( a: angleCosine, b: angleSine, - c: -angleSine, d: angleCosine, - tx: 0, ty: 0 + c: -angleSine, d: angleCosine, + tx: 0, ty: 0 ) } @@ -144,10 +145,10 @@ extension CGAffineTransform { /// - Parameters: /// - sx: The factor by which to scale the x-axis of the coordinate system. /// - sy: The factor by which to scale the y-axis of the coordinate system. - public init(scaleX sx: CGFloat, y sy: CGFloat) { + init(scaleX sx: CGFloat, y sy: CGFloat) { self.init( a: sx, b: 0, - c: 0, d: sy, + c: 0, d: sy, tx: 0, ty: 0 ) } @@ -156,16 +157,16 @@ extension CGAffineTransform { /// - Parameters: /// - tx: The value by which to move the x-axis of the coordinate system. /// - ty: The value by which to move the y-axis of the coordinate system. - public init(translationX tx: CGFloat, y ty: CGFloat) { + init(translationX tx: CGFloat, y ty: CGFloat) { self.init( - a: 1, b: 0, - c: 0, d: 1, + a: 1, b: 0, + c: 0, d: 1, tx: tx, ty: ty ) } } -extension CGAffineTransform { +public extension CGAffineTransform { /// Returns an affine transformation matrix constructed by combining two existing affine /// transforms. /// @@ -175,64 +176,85 @@ extension CGAffineTransform { /// - Parameters: /// - t2: The affine transform to concatenate to this affine transform. /// - Returns: A new affine transformation matrix. That is, `t’ = t1*t2`. - public func concatenating(_ t2: Self) -> Self { + func concatenating(_ t2: Self) -> Self { let t1 = self - - return AffineTransform( - a: (t1.a * t2.a) + (t1.b * t2.c), - b: (t1.a * t2.b) + (t1.b * t2.d), - c: (t1.c * t2.a) + (t1.d * t2.c), - d: (t1.c * t2.b) + (t1.d * t2.d), - tX: (t1.tX * t2.a) + (t1.tY * t2.c) + t2.tX, - tY: (t1.tX * t2.b) + (t1.tY * t2.d) + t2.tY + + return CGAffineTransform( + a: (t1.a * t2.a) + (t1.b * t2.c), + b: (t1.a * t2.b) + (t1.b * t2.d), + c: (t1.c * t2.a) + (t1.d * t2.c), + d: (t1.c * t2.b) + (t1.d * t2.d), + tx: (t1.tx * t2.a) + (t1.ty * t2.c) + t2.tx, + ty: (t1.tx * t2.b) + (t1.ty * t2.d) + t2.ty ) } } -extension CGAffineTransform { +public extension CGAffineTransform { /// Returns an affine transformation matrix constructed by inverting an existing affine transform. /// - Returns: A new affine transformation matrix. If the affine transform passed in /// parameter t cannot be inverted, the affine transform is returned unchanged. - public func inverted() -> Self { + func inverted() -> Self { let determinant = (a * d) - (b * c) - + guard determinant != 0 else { return self } - + return Self( - a: d / determinant, b: -b / determinant, + a: d / determinant, b: -b / determinant, c: -c / determinant, d: a / determinant, - tx: (c * tY - d * tX) / determinant, - ty: (b * tX - a * tY) / determinant + tx: (c * ty - d * tx) / determinant, + ty: (b * tx - a * ty) / determinant ) } } // TODO: - Optimize operators. -extension CGAffineTransform { +public extension CGAffineTransform { /// Returns an affine transformation matrix constructed by rotating an existing affine transform. + /// /// - Parameters: /// - angle: The angle, in radians, by which to rotate the affine transform. /// A positive value specifies clockwise rotation and a negative value specifies /// counterclockwise rotation. - public func rotated(by angle: CGFloat) -> Self { + func rotated(by angle: CGFloat) -> Self { Self(rotationAngle: angle).concatenating(self) } + /// Returns an affine transformation matrix constructed by scaling an existing affine transform. + /// + /// - Parameters: + /// - sx: The value by which to scale x values of the affine transform. + /// - sy: The value by which to scale y values of the affine transform. + func scaledBy(x sx: CGFloat, y sy: CGFloat) -> Self { + Self(scaleX: sx, y: sy).concatenating(self) + } + /// Returns an affine transformation matrix constructed by translating an existing /// affine transform. + /// /// - Parameters: /// - tx: The value by which to move x values with the affine transform. /// - ty: The value by which to move y values with the affine transform. - public func translatedBy(x tx: CGFloat, y ty: CGFloat) -> Self { - Self(translationX: tx, y: ty).concatenating(self) - } + func translatedBy(x tx: CGFloat, y ty: CGFloat) -> Self { + // To translate, we concatenate the translation matrix (T) with self (A): + // + // [ 1 0 0 ] [ a b 0 ] + // A×B = [ 0 1 0 ] × [ c d 0 ] + // [ tx ty 1 ] [ x y 1 ] + // + // [ 1*a+0*c 1*b+0*d 0 ] + // A×B = [ 0*a+1*c 0*b+1*d 0 ] + // [ tx*a+ty*c+x tx*b+ty*d+y 1 ] + // + // [ a b 0 ] + // A×B = [ c d 0 ] + // [ tx*a+ty*c+x tx*b+ty*d+y 1 ] - /// Returns an affine transformation matrix constructed by scaling an existing affine transform. - /// - Parameters: - /// - sx: The value by which to scale x values of the affine transform. - /// - sy: The value by which to scale y values of the affine transform. - public func scaledBy(x sx: CGFloat, y sy: CGFloat) -> Self { - Self(scaleX: sx, y: sy).concatenating(self) + Self( + a: a, b: b, + c: c, d: d, + tx: tx * a + ty * c + self.tx, ty: tx * b + ty * d + self.ty + ) } } From 1eb633ad4430fe4254682c61129a121f032b1755 Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Wed, 21 Jul 2021 17:39:32 +0300 Subject: [PATCH 3/7] [CGAffineTransform] Add invertibility guarantee. --- Sources/TokamakCore/Stubs/CGStubs.swift | 63 ++++++++++++++++++++----- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/Sources/TokamakCore/Stubs/CGStubs.swift b/Sources/TokamakCore/Stubs/CGStubs.swift index 31d0fbcf0..0eff9da00 100644 --- a/Sources/TokamakCore/Stubs/CGStubs.swift +++ b/Sources/TokamakCore/Stubs/CGStubs.swift @@ -86,6 +86,10 @@ public struct CGAffineTransform: Equatable, Codable { public var ty: CGFloat /// Creates an affine transform with the given matrix values. + /// + /// - Postcondition: The created transformation is invertible if its determinant is + /// not `0`: `a*d-b*c≠0`. + /// /// - Parameters: /// - a: The value at position [1,1] in the matrix. /// - b: The value at position [1,2] in the matrix. @@ -108,13 +112,18 @@ public struct CGAffineTransform: Equatable, Codable { } public extension CGAffineTransform { - /// The identity matrix. + /// The identity transformation matrix. + /// + /// - Postcondition: The created transformation is invertible. static let identity = Self( a: 1, b: 0, // 0 c: 0, d: 1, // 0 tx: 0, ty: 0 // 1 ) + /// Creates the identity transformation matrix. + /// + /// - Postcondition: The created transformation is invertible. init() { self = .identity } @@ -125,11 +134,15 @@ public extension CGAffineTransform { } public extension CGAffineTransform { - /// Returns an affine transformation matrix constructed from a rotation value you provide. + /// Creates an affine transformation matrix constructed from a rotation value you + /// provide. + /// + /// - Postcondition: The created transformation is invertible. + /// /// - Parameters: /// - angle: The angle, in radians, by which this matrix rotates the coordinate /// system axes. A positive value specifies clockwise rotation and a negative value - /// specifies counterclockwise rotation. + /// specifies counterclockwise rotation. init(rotationAngle angle: CGFloat) { let angleSine = sin(angle) let angleCosine = cos(angle) @@ -141,7 +154,11 @@ public extension CGAffineTransform { ) } - /// Returns an affine transformation matrix constructed from scaling values you provide. + /// Creates an affine transformation matrix constructed from scaling values you provide. + /// + /// - Postcondition: The created transformation is invertible if both `sx` and + /// `sy` are not `0`. + /// /// - Parameters: /// - sx: The factor by which to scale the x-axis of the coordinate system. /// - sy: The factor by which to scale the y-axis of the coordinate system. @@ -153,7 +170,11 @@ public extension CGAffineTransform { ) } - /// Returns an affine transformation matrix constructed from translation values you provide. + /// Creates an affine transformation matrix constructed from translation values you + /// provide. + /// + /// - Postcondition: The created transformation is invertible. + /// /// - Parameters: /// - tx: The value by which to move the x-axis of the coordinate system. /// - ty: The value by which to move the y-axis of the coordinate system. @@ -170,8 +191,12 @@ public extension CGAffineTransform { /// Returns an affine transformation matrix constructed by combining two existing affine /// transforms. /// - /// Note that concatenation is not commutative, meaning that order is important. For instance, `t1.concatenating(t2)` != `t2.concatenating(t1)` — where `t1` and - /// `t2` are`CGAffineTransform` instances. + /// Note that concatenation is not commutative, meaning that order is important. For + /// instance, `t1.concatenating(t2)` != `t2.concatenating(t1)` — where + /// `t1` and `t2` are`CGAffineTransform` instances. + /// + /// - Postcondition: The returned transformation is invertible if both `self` and + /// the given transformation (`t2`) are invertible. /// /// - Parameters: /// - t2: The affine transform to concatenate to this affine transform. @@ -191,9 +216,14 @@ public extension CGAffineTransform { } public extension CGAffineTransform { - /// Returns an affine transformation matrix constructed by inverting an existing affine transform. - /// - Returns: A new affine transformation matrix. If the affine transform passed in - /// parameter t cannot be inverted, the affine transform is returned unchanged. + /// Returns an affine transformation matrix constructed by inverting an existing affine + /// transform. + /// + /// - Postcondition: Invertibility is preserved, meaning that if `self` is + /// invertible, so will be the returned transformation. + /// + /// - Returns: A new affine transformation matrix. If `self` is not invertible, it's + /// returned unchanged. func inverted() -> Self { let determinant = (a * d) - (b * c) @@ -210,7 +240,11 @@ public extension CGAffineTransform { // TODO: - Optimize operators. public extension CGAffineTransform { - /// Returns an affine transformation matrix constructed by rotating an existing affine transform. + /// Returns an affine transformation matrix constructed by rotating an existing affine + /// transform. + /// + /// - Postcondition: Invertibility is preserved, meaning that if `self` is + /// invertible, so will be the returned transformation. /// /// - Parameters: /// - angle: The angle, in radians, by which to rotate the affine transform. @@ -222,6 +256,10 @@ public extension CGAffineTransform { /// Returns an affine transformation matrix constructed by scaling an existing affine transform. /// + /// - Postcondition: Invertibility is preserved if both `sx` and `sy` aren't `0`. + /// This means that if the aforementioned non-zero requirements are met and `self` + /// is invertible, so will be the returned transformation. + /// /// - Parameters: /// - sx: The value by which to scale x values of the affine transform. /// - sy: The value by which to scale y values of the affine transform. @@ -232,6 +270,9 @@ public extension CGAffineTransform { /// Returns an affine transformation matrix constructed by translating an existing /// affine transform. /// + /// - Postcondition: Invertibility is preserved, meaning that if `self` is + /// invertible, so will be the returned transformation. + /// /// - Parameters: /// - tx: The value by which to move x values with the affine transform. /// - ty: The value by which to move y values with the affine transform. From 00d90f2c8c95596a49276d6f55310872de2eb929 Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Wed, 21 Jul 2021 18:06:36 +0300 Subject: [PATCH 4/7] [CGAffineTransform] Optimize `scaledBy` and add non-zero checks. --- Sources/TokamakCore/Stubs/CGStubs.swift | 50 +++++++++++++++++++------ 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/Sources/TokamakCore/Stubs/CGStubs.swift b/Sources/TokamakCore/Stubs/CGStubs.swift index 0eff9da00..9b7fed8c8 100644 --- a/Sources/TokamakCore/Stubs/CGStubs.swift +++ b/Sources/TokamakCore/Stubs/CGStubs.swift @@ -230,8 +230,10 @@ public extension CGAffineTransform { guard determinant != 0 else { return self } return Self( - a: d / determinant, b: -b / determinant, - c: -c / determinant, d: a / determinant, + a: d / determinant, + b: -b / determinant, + c: -c / determinant, + d: a / determinant, tx: (c * ty - d * tx) / determinant, ty: (b * tx - a * ty) / determinant ) @@ -254,17 +256,42 @@ public extension CGAffineTransform { Self(rotationAngle: angle).concatenating(self) } - /// Returns an affine transformation matrix constructed by scaling an existing affine transform. + /// Returns an affine transformation matrix constructed by scaling an existing affine + /// transform. + /// + /// - Precondition: The scaling coefficients (`sx` and `sy`) must not be `0`. /// - /// - Postcondition: Invertibility is preserved if both `sx` and `sy` aren't `0`. - /// This means that if the aforementioned non-zero requirements are met and `self` - /// is invertible, so will be the returned transformation. + /// - Postcondition: Invertibility is preserved, meaning that if `self` is + /// invertible, so will be the returned transformation. /// /// - Parameters: /// - sx: The value by which to scale x values of the affine transform. /// - sy: The value by which to scale y values of the affine transform. func scaledBy(x sx: CGFloat, y sy: CGFloat) -> Self { - Self(scaleX: sx, y: sy).concatenating(self) + // To scale, we concatenate the scaling matrix (S) and + // self (A): A'=S×A, producing the concatenated matrix + // A'. A', given two non-zero scaling coefficients `sx` and + // `sy`, is: + // + // [ sx 0 0 ] [ a b 0 ] + // S×A = [ 0 sy 0 ] × [ c d 0 ] + // [ 0 0 1 ] [ x y 1 ] + // + // [ sx*a+0*c sx*b+0*d 0 ] + // S×A = [ 0*a+sy*c 0*b+sy*d 0 ] + // [ 0*a+0*c+x 0*b+0*d+y 1 ] + // + // [ sx*a sx*b 0 ] + // S×A = [ sy*c sy*d 0 ] + // [ x y 1 ] + + precondition(sx != 0 && sy != 0, "Scaling a transformation by 0 is prohibited.") + + return Self( + a: sx * a, b: sx * b, + c: sy * c, d: sy * d, + tx: tx, ty: ty + ) } /// Returns an affine transformation matrix constructed by translating an existing @@ -277,18 +304,19 @@ public extension CGAffineTransform { /// - tx: The value by which to move x values with the affine transform. /// - ty: The value by which to move y values with the affine transform. func translatedBy(x tx: CGFloat, y ty: CGFloat) -> Self { - // To translate, we concatenate the translation matrix (T) with self (A): + // To translate, we concatenate the translation matrix (T) + // with self (A): // // [ 1 0 0 ] [ a b 0 ] - // A×B = [ 0 1 0 ] × [ c d 0 ] + // T×A = [ 0 1 0 ] × [ c d 0 ] // [ tx ty 1 ] [ x y 1 ] // // [ 1*a+0*c 1*b+0*d 0 ] - // A×B = [ 0*a+1*c 0*b+1*d 0 ] + // T×A = [ 0*a+1*c 0*b+1*d 0 ] // [ tx*a+ty*c+x tx*b+ty*d+y 1 ] // // [ a b 0 ] - // A×B = [ c d 0 ] + // T×A = [ c d 0 ] // [ tx*a+ty*c+x tx*b+ty*d+y 1 ] Self( From d192cb93d84305466d6eae15c8ef1b42c7fd686c Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Wed, 4 Aug 2021 00:47:43 +0300 Subject: [PATCH 5/7] [CGAffineTransform] Optimize rotation operation. --- Sources/TokamakCore/Stubs/CGStubs.swift | 52 ++++++++++++++++++------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/Sources/TokamakCore/Stubs/CGStubs.swift b/Sources/TokamakCore/Stubs/CGStubs.swift index 9b7fed8c8..986442065 100644 --- a/Sources/TokamakCore/Stubs/CGStubs.swift +++ b/Sources/TokamakCore/Stubs/CGStubs.swift @@ -253,7 +253,29 @@ public extension CGAffineTransform { /// A positive value specifies clockwise rotation and a negative value specifies /// counterclockwise rotation. func rotated(by angle: CGFloat) -> Self { - Self(rotationAngle: angle).concatenating(self) + // To rotate, we concatenate the scaling matrix (R) with + // self (A): A'=R×A, producing the concatenated matrix + // A'. A', given an angle in radians α is: + // + // [ cos(α) sin(α) 0 ] [ a b 0 ] + // R×A = [ -sin(α) cos(α) 0 ] × [ c d 0 ] + // [ 0 0 1 ] [ x y 1 ] + // + // [ cos(α)*a+sin(α)*c cos(α)*b+sin(α)*d 0 ] + // R×A = [ -sin(α)*a+cos(α)*c -sin(α)*b+cos(α)*d 0 ] + // [ 0*a+0*c+x 0*b+0*d+y 1 ] + // + // [ cos(α)*a+sin(α)*c cos(α)*b+sin(α)*d 0 ] + // R×A = [ -sin(α)*a+cos(α)*c -sin(α)*b+cos(α)*d 0 ] + // [ x y 1 ] + + let cosα = cos(angle), sinα = sin(angle) + + return Self( + a: cosα*a + sinα*c, b: cosα*b + sinα*d, + c: -sinα*a+cosα*c, d: -sinα*b+cosα*d, + tx: tx, ty: ty + ) } /// Returns an affine transformation matrix constructed by scaling an existing affine @@ -268,10 +290,10 @@ public extension CGAffineTransform { /// - sx: The value by which to scale x values of the affine transform. /// - sy: The value by which to scale y values of the affine transform. func scaledBy(x sx: CGFloat, y sy: CGFloat) -> Self { - // To scale, we concatenate the scaling matrix (S) and + // To scale, we concatenate the scaling matrix (S) with // self (A): A'=S×A, producing the concatenated matrix - // A'. A', given two non-zero scaling coefficients `sx` and - // `sy`, is: + // A'. A', given two non-zero scaling coefficients sx and + // sy, is: // // [ sx 0 0 ] [ a b 0 ] // S×A = [ 0 sy 0 ] × [ c d 0 ] @@ -303,26 +325,26 @@ public extension CGAffineTransform { /// - Parameters: /// - tx: The value by which to move x values with the affine transform. /// - ty: The value by which to move y values with the affine transform. - func translatedBy(x tx: CGFloat, y ty: CGFloat) -> Self { + func translatedBy(x: CGFloat, y: CGFloat) -> Self { // To translate, we concatenate the translation matrix (T) // with self (A): // - // [ 1 0 0 ] [ a b 0 ] - // T×A = [ 0 1 0 ] × [ c d 0 ] - // [ tx ty 1 ] [ x y 1 ] + // [ 1 0 0 ] [ a b 0 ] + // T×A = [ 0 1 0 ] × [ c d 0 ] + // [ x y 1 ] [ tx ty 1 ] // - // [ 1*a+0*c 1*b+0*d 0 ] - // T×A = [ 0*a+1*c 0*b+1*d 0 ] - // [ tx*a+ty*c+x tx*b+ty*d+y 1 ] + // [ 1*a+0*c 1*b+0*d 0 ] + // T×A = [ 0*a+1*c 0*b+1*d 0 ] + // [ x*a+y*c+tx x*b+y*d+ty 1 ] // - // [ a b 0 ] - // T×A = [ c d 0 ] - // [ tx*a+ty*c+x tx*b+ty*d+y 1 ] + // [ a b 0 ] + // T×A = [ c d 0 ] + // [ x*a+y*c+tx x*b+y*d+ty 1 ] Self( a: a, b: b, c: c, d: d, - tx: tx * a + ty * c + self.tx, ty: tx * b + ty * d + self.ty + tx: x*a + y*c + tx, ty: x*b + y*d + ty ) } } From 6f66e14d8aa2a3789fdaa362c3085c9c8231e24d Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Fri, 20 Aug 2021 02:13:38 +0300 Subject: [PATCH 6/7] [CGAffineTransform] Improve documentation. --- Sources/TokamakCore/Stubs/CGStubs.swift | 116 ++++++++++++++++++------ 1 file changed, 88 insertions(+), 28 deletions(-) diff --git a/Sources/TokamakCore/Stubs/CGStubs.swift b/Sources/TokamakCore/Stubs/CGStubs.swift index 986442065..a51e98177 100644 --- a/Sources/TokamakCore/Stubs/CGStubs.swift +++ b/Sources/TokamakCore/Stubs/CGStubs.swift @@ -40,6 +40,17 @@ extension CGPoint { public extension CGAffineTransform { /// Transform the point into the transform's coordinate system. func transform(point: CGPoint) -> CGPoint { + // To transform, we multiply the given point's matrix with the + // scale-rotation sub-matrix: + // + // [ x' y' ] = [ px py ] × [ a b ] = [ px*a+py*c px*b+py*d ] + // [ c d ] + // + // And then add the translation values `tx` and `ty`: + // + // [ x' y' ] = [ px*a+py*c px*b+py*d ] + [ tx ty ] + // + // [ x' y' ] = [ px*a+py*c+tx px*b+py*d+ty ] CGPoint( x: (a * point.x) + (c * point.y) + tx, y: (b * point.x) + (d * point.y) + ty @@ -87,9 +98,6 @@ public struct CGAffineTransform: Equatable, Codable { /// Creates an affine transform with the given matrix values. /// - /// - Postcondition: The created transformation is invertible if its determinant is - /// not `0`: `a*d-b*c≠0`. - /// /// - Parameters: /// - a: The value at position [1,1] in the matrix. /// - b: The value at position [1,2] in the matrix. @@ -113,8 +121,6 @@ public struct CGAffineTransform: Equatable, Codable { public extension CGAffineTransform { /// The identity transformation matrix. - /// - /// - Postcondition: The created transformation is invertible. static let identity = Self( a: 1, b: 0, // 0 c: 0, d: 1, // 0 @@ -122,8 +128,6 @@ public extension CGAffineTransform { ) /// Creates the identity transformation matrix. - /// - /// - Postcondition: The created transformation is invertible. init() { self = .identity } @@ -137,8 +141,6 @@ public extension CGAffineTransform { /// Creates an affine transformation matrix constructed from a rotation value you /// provide. /// - /// - Postcondition: The created transformation is invertible. - /// /// - Parameters: /// - angle: The angle, in radians, by which this matrix rotates the coordinate /// system axes. A positive value specifies clockwise rotation and a negative value @@ -173,8 +175,6 @@ public extension CGAffineTransform { /// Creates an affine transformation matrix constructed from translation values you /// provide. /// - /// - Postcondition: The created transformation is invertible. - /// /// - Parameters: /// - tx: The value by which to move the x-axis of the coordinate system. /// - ty: The value by which to move the y-axis of the coordinate system. @@ -202,6 +202,17 @@ public extension CGAffineTransform { /// - t2: The affine transform to concatenate to this affine transform. /// - Returns: A new affine transformation matrix. That is, `t’ = t1*t2`. func concatenating(_ t2: Self) -> Self { + // [ a1, b1, 0 ] [ a2, b2, 0 ] + // Given: A = [ c1, d1, 0 ] and: B = [ 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 ] let t1 = self return CGAffineTransform( @@ -220,15 +231,75 @@ public extension CGAffineTransform { /// transform. /// /// - Postcondition: Invertibility is preserved, meaning that if `self` is - /// invertible, so will be the returned transformation. + /// invertible, so the returned transformation will also be invertible. /// /// - Returns: A new affine transformation matrix. If `self` is not invertible, it's /// returned unchanged. func inverted() -> Self { + // Before finding the inverse matrix we first have to find the + // determinant |A| by which we'll divide later. So given: + // [ a b 0 ] + // A = [ c d 0 ] + // [ x y 1 ] + // + // The determinant |A| is: + // + // |A| = a(d*1-y*0) - b(c*1-x*0) + 0(d*x-c*y) = a*d - b*c let determinant = (a * d) - (b * c) - + + // Since we're going divide by the determinant we must check + // that |A|≠0. Note that floating-point rounding could also + // produce infinity (the division-by-zero result), but we + // just want to detect simple cases, like scaling by 0. guard determinant != 0 else { return self } - + + // Then, we have to find the matrix of cofactors. To do that, + // we first 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 when i+j is odd: + // + // [ d -c c*y-x*d ] + // C = [ -b a -(a*y-x*b) ] + // [ 0 -0 |A| ] + // + // Next, we can calculate 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 |A| times 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 ] return Self( a: d / determinant, b: -b / determinant, @@ -245,9 +316,6 @@ public extension CGAffineTransform { /// Returns an affine transformation matrix constructed by rotating an existing affine /// transform. /// - /// - Postcondition: Invertibility is preserved, meaning that if `self` is - /// invertible, so will be the returned transformation. - /// /// - Parameters: /// - angle: The angle, in radians, by which to rotate the affine transform. /// A positive value specifies clockwise rotation and a negative value specifies @@ -281,10 +349,7 @@ public extension CGAffineTransform { /// Returns an affine transformation matrix constructed by scaling an existing affine /// transform. /// - /// - Precondition: The scaling coefficients (`sx` and `sy`) must not be `0`. - /// - /// - Postcondition: Invertibility is preserved, meaning that if `self` is - /// invertible, so will be the returned transformation. + /// - Postcondition: Invertibility is preserved if both `sx` and `sy` aren't `0`. /// /// - Parameters: /// - sx: The value by which to scale x values of the affine transform. @@ -306,10 +371,8 @@ public extension CGAffineTransform { // [ sx*a sx*b 0 ] // S×A = [ sy*c sy*d 0 ] // [ x y 1 ] - - precondition(sx != 0 && sy != 0, "Scaling a transformation by 0 is prohibited.") - - return Self( + + Self( a: sx * a, b: sx * b, c: sy * c, d: sy * d, tx: tx, ty: ty @@ -319,9 +382,6 @@ public extension CGAffineTransform { /// Returns an affine transformation matrix constructed by translating an existing /// affine transform. /// - /// - Postcondition: Invertibility is preserved, meaning that if `self` is - /// invertible, so will be the returned transformation. - /// /// - Parameters: /// - tx: The value by which to move x values with the affine transform. /// - ty: The value by which to move y values with the affine transform. From e9446269c507118773a58e67c0df30392533b6f4 Mon Sep 17 00:00:00 2001 From: Filippos Sakellariou Date: Sat, 4 Sep 2021 17:47:33 +0300 Subject: [PATCH 7/7] [CGAffineTransform] Update to Foundation-backed type. --- Sources/TokamakCore/Stubs/CGStubs.swift | 349 ++++++--------- Tests/TokamakTests/AffineTransform.swift | 523 +++++++++++++++++++++++ 2 files changed, 642 insertions(+), 230 deletions(-) create mode 100644 Tests/TokamakTests/AffineTransform.swift diff --git a/Sources/TokamakCore/Stubs/CGStubs.swift b/Sources/TokamakCore/Stubs/CGStubs.swift index a51e98177..95329f309 100644 --- a/Sources/TokamakCore/Stubs/CGStubs.swift +++ b/Sources/TokamakCore/Stubs/CGStubs.swift @@ -37,29 +37,7 @@ extension CGPoint { } } -public extension CGAffineTransform { - /// Transform the point into the transform's coordinate system. - func transform(point: CGPoint) -> CGPoint { - // To transform, we multiply the given point's matrix with the - // scale-rotation sub-matrix: - // - // [ x' y' ] = [ px py ] × [ a b ] = [ px*a+py*c px*b+py*d ] - // [ c d ] - // - // And then add the translation values `tx` and `ty`: - // - // [ x' y' ] = [ px*a+py*c px*b+py*d ] + [ tx ty ] - // - // [ x' y' ] = [ px*a+py*c+tx px*b+py*d+ty ] - CGPoint( - x: (a * point.x) + (c * point.y) + tx, - y: (b * point.x) + (d * point.y) + ty - ) - } -} - -#warning("Remove `|| true` before merging.") -#if !canImport(CoreGraphics) || true +#if !canImport(CoreGraphics) public enum CGLineCap { /// A line with a squared-off end. Extends to the endpoint of the Path. case butt @@ -76,25 +54,79 @@ public enum CGLineJoin { /// A join with a squared-off end. Extends past the endpoint of the Path. case bevel } +#endif + +#if !canImport(CoreGraphics) +// For cross-platform testing. +@_spi(Tests) +public struct _CGAffineTransform { + // Internal for testing purposes. + internal let _transform: AffineTransform +} +#else +public struct _CGAffineTransform { + // Internal for testing purposes. + internal private(set) var _transform: AffineTransform +} /// An affine transformation matrix for use in drawing 2D graphics. /// /// a b 0 /// c d 0 /// tx ty 1 -public struct CGAffineTransform: Equatable, Codable { +public typealias CGAffineTransform = _CGAffineTransform +#endif + +extension _CGAffineTransform: Equatable {} + +extension _CGAffineTransform: Codable { + public init(from decoder: Decoder) throws { + try self.init( + _transform: AffineTransform(from: decoder) + ) + } + + public func encode(to encoder: Encoder) throws { + try _transform.encode(to: encoder) + } +} + +public extension _CGAffineTransform { /// The value at position [1,1] in the matrix. - public var a: CGFloat + var a: CGFloat { + get { _transform.m11 } + set { _transform.m11 = newValue } + } + /// The value at position [1,2] in the matrix. - public var b: CGFloat + var b: CGFloat { + get { _transform.m12 } + set { _transform.m12 = newValue } + } + /// The value at position [2,1] in the matrix. - public var c: CGFloat + var c: CGFloat { + get { _transform.m21 } + set { _transform.m21 = newValue } + } + /// The value at position [2,2] in the matrix. - public var d: CGFloat + var d: CGFloat { + get { _transform.m22 } + set { _transform.m22 = newValue } + } + /// The value at position [3,1] in the matrix. - public var tx: CGFloat + var tx: CGFloat { + get { _transform.tX } + set { _transform.tX = newValue } + } + /// The value at position [3,2] in the matrix. - public var ty: CGFloat + var ty: CGFloat { + get { _transform.tY } + set { _transform.tY = newValue } + } /// Creates an affine transform with the given matrix values. /// @@ -105,31 +137,26 @@ public struct CGAffineTransform: Equatable, Codable { /// - d: The value at position [2,2] in the matrix. /// - tx: The value at position [3,1] in the matrix. /// - ty: The value at position [3,2] in the matrix. - public init( + init( a: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat, tx: CGFloat, ty: CGFloat ) { - self.a = a - self.b = b - self.c = c - self.d = d - self.tx = tx - self.ty = ty + self.init(_transform: AffineTransform( + m11: a, m12: b, + m21: c, m22: d, + tX: tx, tY: ty + )) } } -public extension CGAffineTransform { +public extension _CGAffineTransform { /// The identity transformation matrix. - static let identity = Self( - a: 1, b: 0, // 0 - c: 0, d: 1, // 0 - tx: 0, ty: 0 // 1 - ) + static let identity = Self(_transform: .identity) /// Creates the identity transformation matrix. init() { - self = .identity + self.init(_transform: AffineTransform()) } var isIdentity: Bool { @@ -137,7 +164,7 @@ public extension CGAffineTransform { } } -public extension CGAffineTransform { +public extension _CGAffineTransform { /// Creates an affine transformation matrix constructed from a rotation value you /// provide. /// @@ -146,30 +173,16 @@ public extension CGAffineTransform { /// system axes. A positive value specifies clockwise rotation and a negative value /// specifies counterclockwise rotation. init(rotationAngle angle: CGFloat) { - let angleSine = sin(angle) - let angleCosine = cos(angle) - - self.init( - a: angleCosine, b: angleSine, - c: -angleSine, d: angleCosine, - tx: 0, ty: 0 - ) + self.init(_transform: AffineTransform(rotationByRadians: angle)) } /// Creates an affine transformation matrix constructed from scaling values you provide. /// - /// - Postcondition: The created transformation is invertible if both `sx` and - /// `sy` are not `0`. - /// /// - Parameters: /// - sx: The factor by which to scale the x-axis of the coordinate system. /// - sy: The factor by which to scale the y-axis of the coordinate system. - init(scaleX sx: CGFloat, y sy: CGFloat) { - self.init( - a: sx, b: 0, - c: 0, d: sy, - tx: 0, ty: 0 - ) + init(scaleX x: CGFloat, y: CGFloat) { + self.init(_transform: AffineTransform(scaleByX: x, byY: y)) } /// Creates an affine transformation matrix constructed from translation values you @@ -178,16 +191,23 @@ public extension CGAffineTransform { /// - Parameters: /// - tx: The value by which to move the x-axis of the coordinate system. /// - ty: The value by which to move the y-axis of the coordinate system. - init(translationX tx: CGFloat, y ty: CGFloat) { - self.init( - a: 1, b: 0, - c: 0, d: 1, - tx: tx, ty: ty - ) + init(translationX x: CGFloat, y: CGFloat) { + self.init(_transform: AffineTransform(translationByX: x, byY: y)) } } -public extension CGAffineTransform { +public extension _CGAffineTransform { + private func withBackingTransform( + _ modify: (inout AffineTransform) -> () + ) -> Self { + var newTransform = _transform + modify(&newTransform) + + return _CGAffineTransform(_transform: newTransform) + } +} + +public extension _CGAffineTransform { /// Returns an affine transformation matrix constructed by combining two existing affine /// transforms. /// @@ -202,31 +222,11 @@ public extension CGAffineTransform { /// - t2: The affine transform to concatenate to this affine transform. /// - Returns: A new affine transformation matrix. That is, `t’ = t1*t2`. func concatenating(_ t2: Self) -> Self { - // [ a1, b1, 0 ] [ a2, b2, 0 ] - // Given: A = [ c1, d1, 0 ] and: B = [ 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 ] - let t1 = self - - return CGAffineTransform( - a: (t1.a * t2.a) + (t1.b * t2.c), - b: (t1.a * t2.b) + (t1.b * t2.d), - c: (t1.c * t2.a) + (t1.d * t2.c), - d: (t1.c * t2.b) + (t1.d * t2.d), - tx: (t1.tx * t2.a) + (t1.ty * t2.c) + t2.tx, - ty: (t1.tx * t2.b) + (t1.ty * t2.d) + t2.ty - ) + withBackingTransform { $0.append(t2._transform) } } } -public extension CGAffineTransform { +public extension _CGAffineTransform { /// Returns an affine transformation matrix constructed by inverting an existing affine /// transform. /// @@ -236,83 +236,17 @@ public extension CGAffineTransform { /// - Returns: A new affine transformation matrix. If `self` is not invertible, it's /// returned unchanged. func inverted() -> Self { - // Before finding the inverse matrix we first have to find the - // determinant |A| by which we'll divide later. So given: - // [ a b 0 ] - // A = [ c d 0 ] - // [ x y 1 ] - // - // The determinant |A| is: - // - // |A| = a(d*1-y*0) - b(c*1-x*0) + 0(d*x-c*y) = a*d - b*c - let determinant = (a * d) - (b * c) - - // Since we're going divide by the determinant we must check - // that |A|≠0. Note that floating-point rounding could also - // produce infinity (the division-by-zero result), but we - // just want to detect simple cases, like scaling by 0. - guard determinant != 0 else { return self } - - // Then, we have to find the matrix of cofactors. To do that, - // we first 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 when i+j is odd: - // - // [ d -c c*y-x*d ] - // C = [ -b a -(a*y-x*b) ] - // [ 0 -0 |A| ] - // - // Next, we can calculate 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 |A| times 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 ] - return Self( - a: d / determinant, - b: -b / determinant, - c: -c / determinant, - d: a / determinant, - tx: (c * ty - d * tx) / determinant, - ty: (b * tx - a * ty) / determinant - ) + withBackingTransform { _transform in + guard let inverted = _transform.inverted() else { + fatalError("This affine transform is non invertible.") + } + + _transform = inverted + } } } -// TODO: - Optimize operators. -public extension CGAffineTransform { +public extension _CGAffineTransform { /// Returns an affine transformation matrix constructed by rotating an existing affine /// transform. /// @@ -321,29 +255,7 @@ public extension CGAffineTransform { /// A positive value specifies clockwise rotation and a negative value specifies /// counterclockwise rotation. func rotated(by angle: CGFloat) -> Self { - // To rotate, we concatenate the scaling matrix (R) with - // self (A): A'=R×A, producing the concatenated matrix - // A'. A', given an angle in radians α is: - // - // [ cos(α) sin(α) 0 ] [ a b 0 ] - // R×A = [ -sin(α) cos(α) 0 ] × [ c d 0 ] - // [ 0 0 1 ] [ x y 1 ] - // - // [ cos(α)*a+sin(α)*c cos(α)*b+sin(α)*d 0 ] - // R×A = [ -sin(α)*a+cos(α)*c -sin(α)*b+cos(α)*d 0 ] - // [ 0*a+0*c+x 0*b+0*d+y 1 ] - // - // [ cos(α)*a+sin(α)*c cos(α)*b+sin(α)*d 0 ] - // R×A = [ -sin(α)*a+cos(α)*c -sin(α)*b+cos(α)*d 0 ] - // [ x y 1 ] - - let cosα = cos(angle), sinα = sin(angle) - - return Self( - a: cosα*a + sinα*c, b: cosα*b + sinα*d, - c: -sinα*a+cosα*c, d: -sinα*b+cosα*d, - tx: tx, ty: ty - ) + withBackingTransform { $0.rotate(byRadians: angle) } } /// Returns an affine transformation matrix constructed by scaling an existing affine @@ -354,29 +266,8 @@ public extension CGAffineTransform { /// - Parameters: /// - sx: The value by which to scale x values of the affine transform. /// - sy: The value by which to scale y values of the affine transform. - func scaledBy(x sx: CGFloat, y sy: CGFloat) -> Self { - // To scale, we concatenate the scaling matrix (S) with - // self (A): A'=S×A, producing the concatenated matrix - // A'. A', given two non-zero scaling coefficients sx and - // sy, is: - // - // [ sx 0 0 ] [ a b 0 ] - // S×A = [ 0 sy 0 ] × [ c d 0 ] - // [ 0 0 1 ] [ x y 1 ] - // - // [ sx*a+0*c sx*b+0*d 0 ] - // S×A = [ 0*a+sy*c 0*b+sy*d 0 ] - // [ 0*a+0*c+x 0*b+0*d+y 1 ] - // - // [ sx*a sx*b 0 ] - // S×A = [ sy*c sy*d 0 ] - // [ x y 1 ] - - Self( - a: sx * a, b: sx * b, - c: sy * c, d: sy * d, - tx: tx, ty: ty - ) + func scaledBy(x: CGFloat, y: CGFloat) -> Self { + withBackingTransform { $0.scale(x: x, y: y) } } /// Returns an affine transformation matrix constructed by translating an existing @@ -386,27 +277,25 @@ public extension CGAffineTransform { /// - tx: The value by which to move x values with the affine transform. /// - ty: The value by which to move y values with the affine transform. func translatedBy(x: CGFloat, y: CGFloat) -> Self { - // To translate, we concatenate the translation matrix (T) - // with self (A): - // - // [ 1 0 0 ] [ a b 0 ] - // T×A = [ 0 1 0 ] × [ c d 0 ] - // [ x y 1 ] [ tx ty 1 ] - // - // [ 1*a+0*c 1*b+0*d 0 ] - // T×A = [ 0*a+1*c 0*b+1*d 0 ] - // [ x*a+y*c+tx x*b+y*d+ty 1 ] - // - // [ a b 0 ] - // T×A = [ c d 0 ] - // [ x*a+y*c+tx x*b+y*d+ty 1 ] - - Self( + withBackingTransform { $0.translate(x: x, y: y) } + } +} + +internal extension _CGAffineTransform { + func _transform(point: CGPoint) -> CGPoint { + _transform.transform(point) + } +} + +public extension CGAffineTransform { + /// Transform the point into the transform's coordinate system. + func transform(point: CGPoint) -> CGPoint { + let transform = _CGAffineTransform( a: a, b: b, c: c, d: d, - tx: x*a + y*c + tx, ty: x*b + y*d + ty + tx: tx, ty: ty ) + + return transform._transform(point: point) } } - -#endif diff --git a/Tests/TokamakTests/AffineTransform.swift b/Tests/TokamakTests/AffineTransform.swift new file mode 100644 index 000000000..d2c1572d8 --- /dev/null +++ b/Tests/TokamakTests/AffineTransform.swift @@ -0,0 +1,523 @@ +// Copyright 2020 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@_spi(Tests) +@testable import TokamakCore + +import XCTest + +typealias CGAffineTransform = TokamakCore._CGAffineTransform + +// MARK: - Tests + +final class CGAffineTransformTest: XCTestCase { + private let accuracyThreshold = 0.001 + + static var allTests: [(String, (CGAffineTransformTest) -> () throws -> ())] { + [ + ("testConstruction", testConstruction), + ("testVectorTransformations", testVectorTransformations), + ("testIdentityConstruction", testIdentityConstruction), + ("testIdentity", testIdentity), + ("testTranslationConstruction", testTranslationConstruction), + ("testTranslation", testTranslation), + ("testScalingConstruction", testScalingConstruction), + ("testScaling", testScaling), + ("testRotationConstruction", testRotationConstruction), + ("testRotation", testRotation), + ("testTranslationScaling", testTranslationScaling), + ("testTranslationRotation", testTranslationRotation), + ("testScalingRotation", testScalingRotation), + ("testInversion", testInversion), + ("testConcatenation", testConcatenation), + ("testCoding", testCoding), + ] + } +} + +// MARK: - Helper + +extension CGAffineTransformTest { + func check( + point: CGPoint, + withTransform transform: CGAffineTransform, + mapsTo expectedPoint: CGPoint, + _ message: String = "", + file: StaticString = #file, line: UInt = #line + ) { + let newPoint = transform.transform(point: point) + + XCTAssertEqual( + newPoint.x, expectedPoint.x, + accuracy: accuracyThreshold, + "Invalid x: \(message)", + file: file, line: line + ) + + XCTAssertEqual( + newPoint.y, expectedPoint.y, + accuracy: accuracyThreshold, + "Invalid y: \(message)", + file: file, line: line + ) + } +} + +// MARK: - Construction + +extension CGAffineTransformTest { + func testConstruction() { + let transform = CGAffineTransform( + a: 1, b: 2, + c: 3, d: 4, + tx: 5, ty: 6 + ) + + XCTAssertEqual(transform.a, 1) + XCTAssertEqual(transform.b, 2) + XCTAssertEqual(transform.c, 3) + XCTAssertEqual(transform.d, 4) + XCTAssertEqual(transform.tx, 5) + XCTAssertEqual(transform.ty, 6) + + let mutatedTransform: CGAffineTransform = { + var copy = transform + + copy.a = -1 + copy.b = -2 + copy.c = -3 + copy.d = -4 + copy.tx = -5 + copy.ty = -6 + + return copy + }() + + XCTAssertEqual(mutatedTransform.a, -1) + XCTAssertEqual(mutatedTransform.b, -2) + XCTAssertEqual(mutatedTransform.c, -3) + XCTAssertEqual(mutatedTransform.d, -4) + XCTAssertEqual(mutatedTransform.tx, -5) + XCTAssertEqual(mutatedTransform.ty, -6) + } +} + +// MARK: - Vector Transformations + +extension CGAffineTransformTest { + func testVectorTransformations() { + // To transform a given point with coordinates x and y, + // we do: + // + // [ m11 m12 0 ] + // [ w' h' 1 ] = [ x y 1 ] * [ m21 m22 0 ] + // [ tx ty 1 ] + // + // [ w' h' 1 ] = [ x*m11+y*m21+1*tX x*m12+y*m22+1*tY ] + + check( + point: CGPoint(x: 10, y: 20), + withTransform: CGAffineTransform( + a: 1, b: 2, + c: 3, d: 4, + tx: 5, ty: 6 + ), + + // [ px*m11+py*m21+tX px*m12+py*m22+tY ] + // [ 10*1+20*3+5 10*2+20*4+6 ] + // [ 75 106 ] + mapsTo: CGPoint(x: 75, y: 106) + ) + + check( + point: CGPoint(x: 5, y: 25), + withTransform: CGAffineTransform( + a: 5, b: 4, + c: 3, d: 2, + tx: 1, ty: 0 + ), + + // [ px*m11+py*m21+tX px*m12+py*m22+tY ] + // [ 5*5+25*3+1 5*4+25*2+0 ] + // [ 101 70 ] + mapsTo: CGPoint(x: 101, y: 70) + ) + } +} + +// MARK: - Identity + +extension CGAffineTransformTest { + func testIdentityConstruction() { + // Check that the transform matrix is the identity: + // [ 1 0 0 ] + // [ 0 1 0 ] + // [ 0 0 1 ] + let identity = CGAffineTransform( + a: 1, b: 0, + c: 0, d: 1, + tx: 0, ty: 0 + ) + + XCTAssertEqual(CGAffineTransform(), identity) + XCTAssertEqual(CGAffineTransform.identity, identity) + } + + func testIdentity() { + check( + point: CGPoint(x: 25, y: 10), + withTransform: .identity, + mapsTo: CGPoint(x: 25, y: 10) + ) + } +} + +// MARK: - Translation + +extension CGAffineTransformTest { + func testTranslationConstruction() { + let translatedIdentity = CGAffineTransform.identity + .translatedBy(x: 15, y: 20) + + let translation = CGAffineTransform( + translationX: 15, y: 20 + ) + + XCTAssertEqual(translatedIdentity, translation) + } + + func testTranslation() { + check( + point: CGPoint(x: 10, y: 10), + withTransform: CGAffineTransform( + translationX: 0, y: 0 + ), + mapsTo: CGPoint(x: 10, y: 10) + ) + + check( + point: CGPoint(x: 10, y: 10), + withTransform: CGAffineTransform( + translationX: 0, y: 5 + ), + mapsTo: CGPoint(x: 10, y: 15) + ) + + check( + point: CGPoint(x: 10, y: 10), + withTransform: CGAffineTransform( + translationX: 5, y: 5 + ), + mapsTo: CGPoint(x: 15, y: 15) + ) + + check( + point: CGPoint(x: -2, y: -3), + // Translate by 5 + withTransform: CGAffineTransform.identity + .translatedBy(x: 2, y: 3) + .translatedBy(x: 3, y: 2), + mapsTo: CGPoint(x: 3, y: 2) + ) + } +} + +// MARK: - Scaling + +extension CGAffineTransformTest { + func testScalingConstruction() { + let scaledIdentity = CGAffineTransform.identity + .scaledBy(x: 15, y: 20) + + let scaling = CGAffineTransform( + scaleX: 15, y: 20 + ) + + XCTAssertEqual(scaledIdentity, scaling) + } + + func testScaling() { + check( + point: CGPoint(x: 10, y: 10), + withTransform: CGAffineTransform( + scaleX: 1, y: 0 + ), + mapsTo: CGPoint(x: 10, y: 0) + ) + + check( + point: CGPoint(x: 10, y: 10), + withTransform: CGAffineTransform( + scaleX: 0.5, y: 1 + ), + mapsTo: CGPoint(x: 5, y: 10) + ) + + check( + point: CGPoint(x: 10, y: 10), + withTransform: CGAffineTransform( + scaleX: 0, y: 2 + ), + mapsTo: CGPoint(x: 0, y: 20) + ) + + check( + point: CGPoint(x: 10, y: 10), + // Scale by (2, 0) + withTransform: CGAffineTransform.identity + .scaledBy(x: 4, y: 0) + .scaledBy(x: 0.5, y: 1), + mapsTo: CGPoint(x: 20, y: 0) + ) + } +} + +// MARK: - Rotation + +extension CGAffineTransformTest { + func testRotationConstruction() { + let baseRotation = CGAffineTransform(rotationAngle: .pi) + + let point = CGPoint(x: 10, y: 15) + let expectedPoint = baseRotation.transform(point: point) + + check( + point: point, + withTransform: .identity.rotated(by: .pi), + mapsTo: expectedPoint, + "Rotation operation on identity doesn't work as identity init." + ) + } + + func testRotation() { + check( + point: CGPoint(x: 10, y: 15), + withTransform: CGAffineTransform(rotationAngle: 0), + mapsTo: CGPoint(x: 10, y: 15) + ) + + check( + point: CGPoint(x: 10, y: 15), + // Rotate by 3*360º + withTransform: CGAffineTransform(rotationAngle: .pi * 6), + mapsTo: CGPoint(x: 10, y: 15) + ) + + // Counter-clockwise rotation + check( + point: CGPoint(x: 15, y: 10), + // Rotate by 90º + withTransform: CGAffineTransform(rotationAngle: .pi / 2), + mapsTo: CGPoint(x: -10, y: 15) + ) + + // Clockwise rotation + check( + point: CGPoint(x: 15, y: 10), + // Rotate by -90º + withTransform: CGAffineTransform(rotationAngle: .pi / -2), + mapsTo: CGPoint(x: 10, y: -15) + ) + + // Reflect about origin + check( + point: CGPoint(x: 10, y: 15), + // Rotate by 180º + withTransform: CGAffineTransform(rotationAngle: .pi), + mapsTo: CGPoint(x: -10, y: -15) + ) + + // Composed reflection about origin + check( + point: CGPoint(x: 10, y: 15), + // Rotate by 180º + withTransform: CGAffineTransform.identity + .rotated(by: .pi / 2) + .rotated(by: .pi / 2), + mapsTo: CGPoint(x: -10, y: -15) + ) + } +} + +// MARK: - Permutations + +extension CGAffineTransformTest { + func testTranslationScaling() { + check( + point: CGPoint(x: 1, y: 3), + // Translate by (2, 0) then scale by (5, -5) + withTransform: CGAffineTransform.identity + .translatedBy(x: 2, y: 0) + .scaledBy(x: 5, y: -5), + mapsTo: CGPoint(x: 15, y: -15) + ) + + check( + point: CGPoint(x: 3, y: 1), + // Scale by (-5, 5) then scale by (0, 10) + withTransform: CGAffineTransform.identity + .scaledBy(x: -5, y: 5) + .translatedBy(x: 0, y: 10), + mapsTo: CGPoint(x: -15, y: 15) + ) + } + + func testTranslationRotation() { + check( + point: CGPoint(x: 10, y: 10), + // Translate by (20, -5) then rotate by 90º + withTransform: CGAffineTransform.identity + .translatedBy(x: 20, y: -5) + .rotated(by: .pi / 2), + mapsTo: CGPoint(x: -5, y: 30) + ) + + check( + point: CGPoint(x: 10, y: 10), + // Rotate by 180º and then translate by (20, 15) + withTransform: CGAffineTransform.identity + .rotated(by: .pi) + .translatedBy(x: 20, y: 15), + mapsTo: CGPoint(x: 10, y: 5) + ) + } + + func testScalingRotation() { + check( + point: CGPoint(x: 20, y: 5), + // Scale by (0.5, 3) then rotate by -90º + withTransform: CGAffineTransform.identity + .scaledBy(x: 0.5, y: 3) + .rotated(by: .pi / -2), + mapsTo: CGPoint(x: 15, y: -10) + ) + + check( + point: CGPoint(x: 20, y: 5), + // Rotate by -90º the scale by (0.5, 3) + withTransform: CGAffineTransform.identity + .rotated(by: .pi / -2) + .scaledBy(x: 3, y: -0.5), + mapsTo: CGPoint(x: 15, y: 10) + ) + } +} + +// MARK: - Inversion + +extension CGAffineTransformTest { + func testInversion() { + let transforms = [ + CGAffineTransform(translationX: -30, y: 40), + CGAffineTransform(rotationAngle: .pi / 4), + CGAffineTransform(scaleX: 20, y: -10), + ] + + let composeTransform: CGAffineTransform = { + var transform = CGAffineTransform.identity + + for component in transforms { + transform = transform.concatenating(component) + } + + return transform + }() + + let recoveredIdentity: CGAffineTransform = { + var transform = composeTransform + + // Append inverse transformations in reverse order + for component in transforms.reversed() { + transform = transform.concatenating( + component.inverted() + ) + } + + return transform + }() + + check( + point: CGPoint(x: 10, y: 10), + withTransform: recoveredIdentity, + mapsTo: CGPoint(x: 10, y: 10) + ) + } +} + +// MARK: - Concatenation + +extension CGAffineTransformTest { + func testConcatenation() { + check( + point: CGPoint(x: 10, y: 15), + withTransform: CGAffineTransform.identity + .concatenating(.identity), + mapsTo: CGPoint(x: 10, y: 15) + ) + + check( + point: CGPoint(x: 10, y: 15), + // Scale by 2 then translate by (10, 0) + withTransform: CGAffineTransform(scaleX: 2, y: 2) + .concatenating(CGAffineTransform( + translationX: 10, y: 0 + )), + mapsTo: CGPoint(x: 30, y: 30) + ) + + check( + point: CGPoint(x: 10, y: 15), + // Translate by (10, 0) then scale by 2 + withTransform: CGAffineTransform(translationX: 10, y: 0) + .concatenating(CGAffineTransform(scaleX: 2, y: 2)), + mapsTo: CGPoint(x: 40, y: 30) + ) + } +} + +// MARK: - Coding + +extension CGAffineTransformTest { + func testCoding() throws { + let transform = CGAffineTransform( + a: 1, b: 2, + c: 3, d: 4, + tx: 5, ty: 6 + ) + + let encodedData = try JSONEncoder().encode(transform) + + let encodedString = String( + data: encodedData, encoding: .utf8 + ) + + let commaSeparatedNumbers = (1...6) + .map(String.init) + .joined(separator: ",") + + XCTAssertEqual( + encodedString, "[\(commaSeparatedNumbers)]", + "Invalid coding representation" + ) + + let recovered = try JSONDecoder().decode( + CGAffineTransform.self, from: encodedData + ) + + XCTAssertEqual( + transform, recovered, + "Encoded and then decoded transform does not equal original" + ) + } +}