Skip to content

Commit cb38823

Browse files
author
Dominique d'Argent
committed
Implement NSAffineTransformStruct.invert() and tests
It also adds missing arithmetic operators to `CGFloat` which are needed for the matrix inversion.
1 parent aebfe00 commit cb38823

File tree

3 files changed

+93
-2
lines changed

3 files changed

+93
-2
lines changed

Foundation/NSAffineTransform.swift

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,14 @@ public class NSAffineTransform : NSObject, NSCopying, NSSecureCoding {
9696
}
9797

9898
// Inverting
99-
public func invert() { NSUnimplemented() }
99+
public func invert() {
100+
if let inverse = transformStruct.inverse {
101+
transformStruct = inverse
102+
}
103+
else {
104+
preconditionFailure("NSAffineTransform: Transform has no inverse")
105+
}
106+
}
100107

101108
// Transforming with transform
102109
public func appendTransform(transform: NSAffineTransform) {
@@ -241,6 +248,45 @@ private extension NSAffineTransformStruct {
241248

242249
return NSSize(width: w, height: h)
243250
}
251+
252+
253+
/**
254+
Returns the inverse affine transformation matrix or `nil` if it has no inverse.
255+
The receiver's affine transformation matrix can be divided into matrix sub-block as
256+
[ M t ]
257+
[ 0 1 ]
258+
where `M` represents the linear map and `t` the translation vector.
259+
260+
The inversion can then be calculated as
261+
[ inv(M) -inv(M) * t ]
262+
[ 0 1 ]
263+
if `M` is invertible.
264+
*/
265+
var inverse: NSAffineTransformStruct? {
266+
get {
267+
// Calculate determinant of M: det(M)
268+
let det = (m11 * m22) - (m12 * m21)
269+
if det == CGFloat() {
270+
return nil
271+
}
272+
273+
let detReciprocal = CGFloat(1.0) / det
274+
275+
// Calculate the inverse of M: inv(M)
276+
let (invM11, invM12) = (detReciprocal * m22, detReciprocal * -m12)
277+
let (invM21, invM22) = (detReciprocal * -m21, detReciprocal * m11)
278+
279+
// Calculate -inv(M)*t
280+
let invTX = ((-invM11 * tX) + (-invM12 * tY))
281+
let invTY = ((-invM21 * tX) + (-invM22 * tY))
282+
283+
return NSAffineTransformStruct(
284+
m11: invM11, m12: invM12,
285+
m21: invM21, m22: invM22,
286+
tX: invTX, tY: invTY
287+
)
288+
}
289+
}
244290
}
245291

246292

Foundation/NSGeometry.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ public func +(lhs: CGFloat, rhs: CGFloat) -> CGFloat {
4545
return CGFloat(lhs.native + rhs.native)
4646
}
4747

48+
public func -(lhs: CGFloat, rhs: CGFloat) -> CGFloat {
49+
return CGFloat(lhs.native - rhs.native)
50+
}
51+
52+
public func /(lhs: CGFloat, rhs: CGFloat) -> CGFloat {
53+
return CGFloat(lhs.native / rhs.native)
54+
}
55+
56+
prefix public func -(x: CGFloat) -> CGFloat {
57+
return CGFloat(-x.native)
58+
}
59+
4860
@_transparent extension Double {
4961
public init(_ value: CGFloat) {
5062
self = Double(value.native)

TestFoundation/TestNSAffineTransform.swift

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ class TestNSAffineTransform : XCTestCase {
3333
("test_Translation", test_Translation),
3434
("test_Scale", test_Scale),
3535
("test_Rotation_Degrees", test_Rotation_Degrees),
36-
("test_Rotation_Radians", test_Rotation_Radians)
36+
("test_Rotation_Radians", test_Rotation_Radians),
37+
("test_Inversion", test_Inversion)
3738
]
3839
}
3940

@@ -143,6 +144,38 @@ class TestNSAffineTransform : XCTestCase {
143144
reflectAboutOrigin.rotateByRadians(CGFloat(M_PI))
144145
checkPointTransformation(reflectAboutOrigin, point: point, expectedPoint: NSPoint(x: CGFloat(-10.0), y: CGFloat(-10.0)))
145146
}
147+
148+
func test_Inversion() {
149+
let point = NSPoint(x: CGFloat(10.0), y: CGFloat(10.0))
150+
151+
let translate = NSAffineTransform()
152+
translate.translateXBy(CGFloat(-30.0), yBy: CGFloat(40.0))
153+
154+
let rotate = NSAffineTransform()
155+
translate.rotateByDegrees(CGFloat(30.0))
156+
157+
let scale = NSAffineTransform()
158+
scale.scaleBy(CGFloat(2.0))
159+
160+
let identityTransform = NSAffineTransform()
161+
162+
// append transformations
163+
identityTransform.appendTransform(translate)
164+
identityTransform.appendTransform(rotate)
165+
identityTransform.appendTransform(scale)
166+
167+
// invert transformations
168+
scale.invert()
169+
rotate.invert()
170+
translate.invert()
171+
172+
// append inverse transformations in reverse order
173+
identityTransform.appendTransform(scale)
174+
identityTransform.appendTransform(rotate)
175+
identityTransform.appendTransform(translate)
176+
177+
checkPointTransformation(identityTransform, point: point, expectedPoint: point)
178+
}
146179
}
147180

148181

0 commit comments

Comments
 (0)