Skip to content

Commit a1b54ad

Browse files
Merge pull request #232 from markuswntr/quaternion/argument
Allow direct access to `halfAngle` on quaternions
2 parents b0e86b6 + 7b46b41 commit a1b54ad

File tree

2 files changed

+78
-79
lines changed

2 files changed

+78
-79
lines changed

Sources/QuaternionModule/Polar.swift

Lines changed: 64 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ extension Quaternion {
4949
/// - If a quaternion is not finite, its `.length` is `infinity`.
5050
///
5151
/// See also `.magnitude`, `.lengthSquared`, `.polar` and
52-
/// `init(length:,phase:,axis:)`.
52+
/// `init(length:halfAngle:axis:)`.
5353
@_transparent
5454
public var length: RealType {
5555
let naive = lengthSquared
@@ -70,131 +70,130 @@ extension Quaternion {
7070
/// The squared length `(r*r + x*x + y*y + z*z)`.
7171
///
7272
/// This value is highly prone to overflow or underflow.
73-
///
73+
///
7474
/// For many cases, `.magnitude` can be used instead, which is similarly
7575
/// cheap to compute and always returns a representable value.
7676
///
7777
/// This property is more efficient to compute than `length`.
7878
///
7979
/// See also `.magnitude`, `.length`, `.polar` and
80-
/// `init(length:,phase:,axis:)`.
80+
/// `init(length:halfAngle:axis:)`.
8181
@_transparent
8282
public var lengthSquared: RealType {
8383
(components * components).sum()
8484
}
8585

86+
/// The half rotation angle in radians within *[0, π]* range.
87+
///
88+
/// Edge cases:
89+
/// - If the quaternion is zero or non-finite, halfAngle is `nan`.
90+
@inlinable
91+
public var halfAngle: RealType {
92+
guard isFinite else { return .nan }
93+
// A zero quaternion does not encode transformation properties.
94+
// If imaginary is zero, real must be non-zero or nan is returned.
95+
guard !isReal else { return isPure ? .nan : .zero }
96+
// If lengthSquared computes without over/underflow, everything is fine
97+
// and the result is correct. If not, we have to do the computation
98+
// carefully and unscale the quaternion first.
99+
let lenSq = imaginary.lengthSquared
100+
guard lenSq.isNormal else { return divided(by: magnitude).halfAngle }
101+
return .atan2(y: .sqrt(lenSq), x: real)
102+
}
103+
86104
/// The [polar decomposition][wiki].
87105
///
88-
/// Returns the length of this quaternion, phase in radians of range *[0, π]*
89-
/// and the rotation axis as SIMD3 vector of unit length.
106+
/// Returns the length of this quaternion, halfAngle in radians of range
107+
/// *[0, π]* and the rotation axis as SIMD3 vector of unit length.
90108
///
91109
/// Edge cases:
92-
/// - If the quaternion is zero, length is `.zero` and angle and axis
110+
/// - If the quaternion is zero, length is `.zero` and halfAngle and axis
93111
/// are `nan`.
94-
/// - If the quaternion is non-finite, length is `.infinity` and angle and
112+
/// - If the quaternion is non-finite, length is `.infinity` and halfAngle and
95113
/// axis are `nan`.
96-
/// - For any length other than `.zero` or `.infinity`, if angle is zero, axis
97-
/// is `nan`.
114+
/// - For any length other than `.zero` or `.infinity`, if halfAngle is zero,
115+
/// axis is `nan`.
98116
///
99117
/// See also `.magnitude`, `.length`, `.lengthSquared` and
100-
/// `init(length:,phase:,axis:)`.
118+
/// `init(length:halfAngle:axis:)`.
101119
///
102120
/// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition
103-
public var polar: (length: RealType, phase: RealType, axis: SIMD3<RealType>) {
121+
public var polar: (
122+
length: RealType,
123+
halfAngle: RealType,
124+
axis: SIMD3<RealType>
125+
) {
104126
(length, halfAngle, axis)
105127
}
106128

129+
/// Creates a new quaternion from given half rotation angle about given
130+
/// rotation axis.
131+
///
132+
/// The angle-axis values are transformed using the following equation:
133+
///
134+
/// Q = (cos(halfAngle), unitAxis * sin(halfAngle))
135+
///
136+
/// - Parameters:
137+
/// - halfAngle: The half rotation angle
138+
/// - unitAxis: The rotation axis of unit length
139+
@usableFromInline @inline(__always)
140+
internal init(halfAngle: RealType, unitAxis: SIMD3<RealType>) {
141+
self.init(real: .cos(halfAngle), imaginary: unitAxis * .sin(halfAngle))
142+
}
143+
107144
/// Creates a quaternion specified with [polar coordinates][wiki].
108145
///
109-
/// This initializer reads given `length`, `phase` and `axis` values and
146+
/// This initializer reads given `length`, `halfAngle` and `axis` values and
110147
/// creates a quaternion of equal rotation properties and specified *length*
111148
/// using the following equation:
112149
///
113-
/// Q = (cos(phase), axis * sin(phase)) * length
114-
///
115-
/// - Note: `axis` must be of unit length, or an assertion failure occurs.
150+
/// Q = (cos(halfAngle), axis * sin(halfAngle)) * length
116151
///
117152
/// Edge cases:
118153
/// - Negative lengths are interpreted as reflecting the point through the
119154
/// origin, i.e.:
120155
/// ```
121-
/// Quaternion(length: -r, phase: θ, axis: axis) == -Quaternion(length: r, phase: θ, axis: axis)
156+
/// Quaternion(length: -r, halfAngle: θ, axis: axis) == -Quaternion(length: r, halfAngle: θ, axis: axis)
122157
/// ```
123158
/// - For any `θ` and any `axis`, even `.infinity` or `.nan`:
124159
/// ```
125-
/// Quaternion(length: .zero, phase: θ, axis: axis) == .zero
160+
/// Quaternion(length: .zero, halfAngle: θ, axis: axis) == .zero
126161
/// ```
127162
/// - For any `θ` and any `axis`, even `.infinity` or `.nan`:
128163
/// ```
129-
/// Quaternion(length: .infinity, phase: θ, axis: axis) == .infinity
164+
/// Quaternion(length: .infinity, halfAngle: θ, axis: axis) == .infinity
130165
/// ```
131-
/// - Otherwise, `θ` must be finite, or a precondition failure occurs.
166+
/// - Otherwise, `θ` must be finite, or a precondition failure occurs and
167+
/// `axis` must be of unit length, or an assertion failure occurs.
132168
///
133169
/// See also `.magnitude`, `.length`, `.lengthSquared` and `.polar`.
134170
///
135171
/// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition
136172
@inlinable
137-
public init(length: RealType, phase: RealType, axis: SIMD3<RealType>) {
173+
public init(length: RealType, halfAngle: RealType, axis: SIMD3<RealType>) {
138174
guard !length.isZero, length.isFinite else {
139175
self = Quaternion(length)
140176
return
141177
}
142178

143179
// Length is finite and non-zero, therefore
144-
// 1. `phase` must be finite or a precondition failure needs to occur; as
145-
// this is not representable.
180+
// 1. `halfAngle` must be finite or a precondition failure needs to occur;
181+
// as this is not representable.
146182
// 2. `axis` must be of unit length or an assertion failure occurs; while
147183
// "wrong" by definition, it is representable.
148184
precondition(
149-
phase.isFinite,
150-
"Either phase must be finite, or length must be zero or infinite."
185+
halfAngle.isFinite,
186+
"Either halfAngle must be finite, or length must be zero or infinite."
151187
)
152188
assert(
153189
// TODO: Replace with `approximateEquality()`
154190
abs(.sqrt(axis.lengthSquared)-1) < max(.sqrt(axis.lengthSquared), 1)*RealType.ulpOfOne.squareRoot(),
155191
"Given axis must be of unit length."
156192
)
157193

158-
self = Quaternion(halfAngle: phase, unitAxis: axis).multiplied(by: length)
159-
}
160-
}
161-
162-
// MARK: - Operations for working with polar form
163-
164-
extension Quaternion {
165-
/// The half rotation angle in radians within *[0, π]* range.
166-
///
167-
/// Edge cases:
168-
/// - If the quaternion is zero or non-finite, halfAngle is `nan`.
169-
@usableFromInline @inline(__always)
170-
internal var halfAngle: RealType {
171-
guard isFinite else { return .nan }
172-
guard imaginary != .zero else {
173-
// A zero quaternion does not encode transformation properties.
174-
// If imaginary is zero, real must be non-zero or nan is returned.
175-
return real.isZero ? .nan : .zero
176-
}
177-
178-
// If lengthSquared computes without over/underflow, everything is fine
179-
// and the result is correct. If not, we have to do the computation
180-
// carefully and unscale the quaternion first.
181-
let lenSq = imaginary.lengthSquared
182-
guard lenSq.isNormal else { return divided(by: magnitude).halfAngle }
183-
return .atan2(y: .sqrt(lenSq), x: real)
184-
}
185-
186-
/// Creates a new quaternion from given half rotation angle about given
187-
/// rotation axis.
188-
///
189-
/// The angle-axis values are transformed using the following equation:
190-
///
191-
/// Q = (cos(halfAngle), unitAxis * sin(halfAngle))
192-
///
193-
/// - Parameters:
194-
/// - halfAngle: The half rotation angle
195-
/// - unitAxis: The rotation axis of unit length
196-
@usableFromInline @inline(__always)
197-
internal init(halfAngle: RealType, unitAxis: SIMD3<RealType>) {
198-
self.init(real: .cos(halfAngle), imaginary: unitAxis * .sin(halfAngle))
194+
self = Quaternion(
195+
halfAngle: halfAngle,
196+
unitAxis: axis
197+
).multiplied(by: length)
199198
}
200199
}

Tests/QuaternionTests/TransformationTests.swift

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,12 @@ final class TransformationTests: XCTestCase {
181181
func testPolarDecomposition<T: Real & SIMDScalar>(_ type: T.Type) {
182182
let axis = SIMD3<T>(0,-1,0)
183183

184-
let q = Quaternion<T>(length: 5, phase: .pi, axis: axis)
184+
let q = Quaternion<T>(length: 5, halfAngle: .pi, axis: axis)
185185
XCTAssertEqual(q.axis, axis)
186186
XCTAssertEqual(q.angle, .pi * 2)
187187

188188
XCTAssertEqual(q.polar.length, 5)
189-
XCTAssertEqual(q.polar.phase, .pi)
189+
XCTAssertEqual(q.polar.halfAngle, .pi)
190190
XCTAssertEqual(q.polar.axis, axis)
191191
}
192192

@@ -196,18 +196,18 @@ final class TransformationTests: XCTestCase {
196196
}
197197

198198
func testPolarDecompositionEdgeCases<T: Real & SIMDScalar>(_ type: T.Type) {
199-
XCTAssertEqual(Quaternion<T>(length: .zero, phase: .zero , axis: .zero ), .zero)
200-
XCTAssertEqual(Quaternion<T>(length: .zero, phase: .infinity, axis: .infinity), .zero)
201-
XCTAssertEqual(Quaternion<T>(length: .zero, phase:-.infinity, axis: -.infinity), .zero)
202-
XCTAssertEqual(Quaternion<T>(length: .zero, phase: .nan , axis: .nan ), .zero)
203-
XCTAssertEqual(Quaternion<T>(length: .infinity, phase: .zero , axis: .zero ), .infinity)
204-
XCTAssertEqual(Quaternion<T>(length: .infinity, phase: .infinity, axis: .infinity), .infinity)
205-
XCTAssertEqual(Quaternion<T>(length: .infinity, phase:-.infinity, axis: -.infinity), .infinity)
206-
XCTAssertEqual(Quaternion<T>(length: .infinity, phase: .nan , axis: .infinity), .infinity)
207-
XCTAssertEqual(Quaternion<T>(length:-.infinity, phase: .zero , axis: .zero ), .infinity)
208-
XCTAssertEqual(Quaternion<T>(length:-.infinity, phase: .infinity, axis: .infinity), .infinity)
209-
XCTAssertEqual(Quaternion<T>(length:-.infinity, phase:-.infinity, axis: -.infinity), .infinity)
210-
XCTAssertEqual(Quaternion<T>(length:-.infinity, phase: .nan , axis: .infinity), .infinity)
199+
XCTAssertEqual(Quaternion<T>(length: .zero, halfAngle: .zero, axis: .zero), .zero)
200+
XCTAssertEqual(Quaternion<T>(length: .zero, halfAngle: .infinity, axis: .infinity), .zero)
201+
XCTAssertEqual(Quaternion<T>(length: .zero, halfAngle:-.infinity, axis:-.infinity), .zero)
202+
XCTAssertEqual(Quaternion<T>(length: .zero, halfAngle: .nan, axis: .nan), .zero)
203+
XCTAssertEqual(Quaternion<T>(length: .infinity, halfAngle: .zero, axis: .zero), .infinity)
204+
XCTAssertEqual(Quaternion<T>(length: .infinity, halfAngle: .infinity, axis: .infinity), .infinity)
205+
XCTAssertEqual(Quaternion<T>(length: .infinity, halfAngle:-.infinity, axis:-.infinity), .infinity)
206+
XCTAssertEqual(Quaternion<T>(length: .infinity, halfAngle: .nan, axis: .infinity), .infinity)
207+
XCTAssertEqual(Quaternion<T>(length:-.infinity, halfAngle: .zero, axis: .zero), .infinity)
208+
XCTAssertEqual(Quaternion<T>(length:-.infinity, halfAngle: .infinity, axis: .infinity), .infinity)
209+
XCTAssertEqual(Quaternion<T>(length:-.infinity, halfAngle:-.infinity, axis:-.infinity), .infinity)
210+
XCTAssertEqual(Quaternion<T>(length:-.infinity, halfAngle: .nan, axis: .infinity), .infinity)
211211
}
212212

213213
func testPolarDecompositionEdgeCases() {

0 commit comments

Comments
 (0)