Skip to content

Commit

Permalink
- Better orientation handling
Browse files Browse the repository at this point in the history
- Made angle conversion methods an extension to the FloatingPoint protocol
- Prevented the motionManger handler code from being executed after self is deallocated
  • Loading branch information
scihant committed Feb 13, 2017
1 parent 56c97bf commit f54d433
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 53 deletions.
90 changes: 44 additions & 46 deletions CTPanoramaView/CTPanoramaView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import ImageIO
func updateUI(rotationAngle: CGFloat, fieldOfViewAngle: CGFloat)
}

@objc public enum CTPanaromaControlMethod: Int {
@objc public enum CTPanoramaControlMethod: Int {
case Motion
case Touch
}

@objc public enum CTPanaromaType: Int {
@objc public enum CTpanoramaType: Int {
case Cylindrical
case Spherical
}
Expand All @@ -33,7 +33,7 @@ import ImageIO

public var image: UIImage? {
didSet {
panaromaType = panoramaTypeForCurrentImage
panoramaType = panoramaTypeForCurrentImage
}
}

Expand All @@ -43,14 +43,14 @@ import ImageIO
}
}

public var panaromaType: CTPanaromaType = .Cylindrical {
public var panoramaType: CTpanoramaType = .Cylindrical {
didSet {
createGeometryNode()
resetCameraAngles()
}
}

public var controlMethod: CTPanaromaControlMethod! {
public var controlMethod: CTPanoramaControlMethod! {
didSet {
switchControlMethod(to: controlMethod!)
resetCameraAngles()
Expand Down Expand Up @@ -79,14 +79,14 @@ import ImageIO
}()

private lazy var fovHeight: CGFloat = {
return CGFloat(tan(self.cameraNode.camera!.yFov/2 * Double.pi / 180.0)) * 2 * self.radius
return CGFloat(tan(self.cameraNode.camera!.yFov/2 * .pi / 180.0)) * 2 * self.radius
}()

private var xFov: CGFloat {
return CGFloat(self.cameraNode.camera!.yFov) * self.bounds.width / self.bounds.height
}

private var panoramaTypeForCurrentImage: CTPanaromaType {
private var panoramaTypeForCurrentImage: CTpanoramaType {
if let image = image {
if image.size.width / image.size.height == 2 {
return .Spherical
Expand All @@ -112,7 +112,13 @@ import ImageIO
({self.image = image})() // Force Swift to call the property observer by calling the setter from a non-init context
}

private func commonInit() {
deinit {
if motionManager.isDeviceMotionActive {
motionManager.stopDeviceMotionUpdates()
}
}

private func commonInit() {
add(view: sceneView)

scene.rootNode.addChildNode(cameraNode)
Expand Down Expand Up @@ -140,7 +146,7 @@ import ImageIO
material.diffuse.wrapS = .repeat
material.cullMode = .front

if panaromaType == .Spherical {
if panoramaType == .Spherical {
let sphere = SCNSphere(radius: radius)
sphere.segmentCount = 300
sphere.firstMaterial = material
Expand Down Expand Up @@ -168,7 +174,7 @@ import ImageIO
add(view: newOverlayView)
}

private func switchControlMethod(to method: CTPanaromaControlMethod) {
private func switchControlMethod(to method: CTPanoramaControlMethod) {
sceneView.gestureRecognizers?.removeAll()

if method == .Touch {
Expand All @@ -182,42 +188,28 @@ import ImageIO
else {
guard motionManager.isDeviceMotionAvailable else {return}
motionManager.deviceMotionUpdateInterval = 0.015
motionManager.startDeviceMotionUpdates(using: .xArbitraryZVertical, to: OperationQueue.main, withHandler: {[unowned self] (motionData, error) in
guard self.controlMethod == .Motion else {return}
motionManager.startDeviceMotionUpdates(using: .xArbitraryZVertical, to: OperationQueue.main, withHandler: {[weak self] (motionData, error) in
guard let panoramaView = self else {return}
guard panoramaView.controlMethod == .Motion else {return}

guard let motionData = motionData else {
print("\(error?.localizedDescription)")
self.motionManager.stopDeviceMotionUpdates()
panoramaView.motionManager.stopDeviceMotionUpdates()
return
}

let rm = motionData.attitude.rotationMatrix
var userHeading = .pi - atan2(rm.m32, rm.m31)
userHeading += .pi/2

/*
// 0 Landscape Left, 90 Portrait 180 Landscape Right 270 Inverse Portrait
var userRoll = fabs(atan2(motionData.gravity.y, -motionData.gravity.x))

if motionData.gravity.y > 0 {
userRoll = 2 * .pi - userRoll
}

let x = motionData.gravity.z

let y = UIDeviceOrientationIsPortrait(UIDevice.current.orientation) ? -motionData.gravity.y : -motionData.gravity.x
let userTilt = fabs(atan2(y, x)) //- .pi/2
// 0 face down, 90 vertical 180 face up
*/

userHeading += .pi / 2

if self.panaromaType == .Cylindrical {
self.cameraNode.eulerAngles = SCNVector3Make(0, Float(-userHeading), 0) // Prevent vertical movement in a cylindrical panorama
if panoramaView.panoramaType == .Cylindrical {
panoramaView.cameraNode.eulerAngles = SCNVector3Make(0, Float(-userHeading), 0) // Prevent vertical movement in a cylindrical panorama
}
else {
// Use quaternions when in spherical mode to prevent gimbal lock
self.cameraNode.orientation = motionData.look(at: UIApplication.shared.statusBarOrientation)
panoramaView.cameraNode.orientation = motionData.orientation()
}
self.reportMovement(CGFloat(userHeading), self.xFov.toRadians())
panoramaView.reportMovement(CGFloat(userHeading), panoramaView.xFov.toRadians())
})
}
}
Expand All @@ -243,7 +235,7 @@ import ImageIO
else if panRec.state == .changed {
var modifiedPanSpeed = panSpeed

if panaromaType == .Cylindrical {
if panoramaType == .Cylindrical {
modifiedPanSpeed.y = 0 // Prevent vertical movement in a cylindrical panorama
}

Expand Down Expand Up @@ -275,7 +267,7 @@ import ImageIO

fileprivate extension CMDeviceMotion {

func look(at orientation: UIInterfaceOrientation) -> SCNVector4 {
func orientation() -> SCNVector4 {

let attitude = self.attitude.quaternion
let aq = GLKQuaternionMake(Float(attitude.x), Float(attitude.y), Float(attitude.z), Float(attitude.w))
Expand All @@ -285,27 +277,33 @@ fileprivate extension CMDeviceMotion {
switch UIApplication.shared.statusBarOrientation {

case .landscapeRight:
let cq = GLKQuaternionMakeWithAngleAndAxis(Float(M_PI_2), 0, 1, 0)
let q = GLKQuaternionMultiply(cq, aq)
let cq1 = GLKQuaternionMakeWithAngleAndAxis(.pi/2, 0, 1, 0)
let cq2 = GLKQuaternionMakeWithAngleAndAxis(-(.pi/2), 1, 0, 0)
var q = GLKQuaternionMultiply(cq1, aq)
q = GLKQuaternionMultiply(cq2, q)

result = SCNVector4(x: -q.y, y: q.x, z: q.z, w: q.w)

case .landscapeLeft:
let cq = GLKQuaternionMakeWithAngleAndAxis(Float(-M_PI_2), 0, 1, 0)
let q = GLKQuaternionMultiply(cq, aq)
let cq1 = GLKQuaternionMakeWithAngleAndAxis(-(.pi/2), 0, 1, 0)
let cq2 = GLKQuaternionMakeWithAngleAndAxis(-(.pi/2), 1, 0, 0)
var q = GLKQuaternionMultiply(cq1, aq)
q = GLKQuaternionMultiply(cq2, q)

result = SCNVector4(x: q.y, y: -q.x, z: q.z, w: q.w)

case .portraitUpsideDown:
let cq = GLKQuaternionMakeWithAngleAndAxis(Float(M_PI_2), 1, 0, 0)
let q = GLKQuaternionMultiply(cq, aq)
let cq1 = GLKQuaternionMakeWithAngleAndAxis(-(.pi/2), 1, 0, 0)
let cq2 = GLKQuaternionMakeWithAngleAndAxis(.pi, 0, 0, 1)
var q = GLKQuaternionMultiply(cq1, aq)
q = GLKQuaternionMultiply(cq2, q)

result = SCNVector4(x: -q.x, y: -q.y, z: q.z, w: q.w)

case .unknown:
fallthrough
case .portrait:
let cq = GLKQuaternionMakeWithAngleAndAxis(Float(-M_PI_2), 1, 0, 0)
let cq = GLKQuaternionMakeWithAngleAndAxis(-(.pi/2), 1, 0, 0)
let q = GLKQuaternionMultiply(cq, aq)

result = SCNVector4(x: q.x, y: q.y, z: q.z, w: q.w)
Expand All @@ -324,12 +322,12 @@ fileprivate extension UIView {
}
}

fileprivate extension CGFloat {
func toDegrees() -> CGFloat {
fileprivate extension FloatingPoint {
func toDegrees() -> Self {
return self * 180 / .pi
}

func toRadians() -> CGFloat {
func toRadians() -> Self {
return self * .pi / 180
}
}
2 changes: 1 addition & 1 deletion CTPanoramaView/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class ViewController: UIViewController {
}

@IBAction func panoramaTypeTapped() {
if pv.panaromaType == .Spherical {
if pv.panoramaType == .Spherical {
pv.image = UIImage(named: "cylindrical.jpg")
}
else {
Expand Down
12 changes: 6 additions & 6 deletions CTPanoramaViewTests/CTPanoramaViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import XCTest

class CTPanoramaViewTests: XCTestCase {

private let sphericalImageName = "spherical.png"
private let cylindricalImageName = "cylindrical.jpg"
private let sphericalImageName = "spherical"
private let cylindricalImageName = "cylindrical"

private var sphericalImage: UIImage!
private var cylindricalImage: UIImage!
Expand All @@ -29,16 +29,16 @@ class CTPanoramaViewTests: XCTestCase {

func testThatPanoramaTypeChangesAccordingToImage() {
var pv = CTPanoramaView(frame: CGRect.zero, image: sphericalImage)
XCTAssert(pv.panaromaType == .Spherical)
XCTAssert(pv.panoramaType == .Spherical)

pv = CTPanoramaView(frame: CGRect.zero, image: cylindricalImage)
XCTAssert(pv.panaromaType == .Cylindrical)
XCTAssert(pv.panoramaType == .Cylindrical)

pv.image = sphericalImage
XCTAssert(pv.panaromaType == .Spherical)
XCTAssert(pv.panoramaType == .Spherical)

pv.image = cylindricalImage
XCTAssert(pv.panaromaType == .Cylindrical)
XCTAssert(pv.panoramaType == .Cylindrical)
}

func testThatSettingOverlayViewAddsTheViewOnTop() {
Expand Down

0 comments on commit f54d433

Please sign in to comment.