diff --git a/Feature Catalog/View Controllers/FeatureCatalogViewController.swift b/Feature Catalog/View Controllers/FeatureCatalogViewController.swift index 9bfe7a7..5b4ba9c 100644 --- a/Feature Catalog/View Controllers/FeatureCatalogViewController.swift +++ b/Feature Catalog/View Controllers/FeatureCatalogViewController.swift @@ -168,7 +168,8 @@ UINavigationControllerDelegate, NavigationConvenience { self.kitsTab(), self.database(), self.vectors(), - self.buttons() + self.buttons(), + self.research() ] }() @@ -292,6 +293,15 @@ UINavigationControllerDelegate, NavigationConvenience { return Section(title: "Vector Images", items: items) } + func research() -> Section { + let items: [FeatureCatalogItem] = [ + FeatureCatalogItem(name: "Send Motion Data", + creationBlock: { MotionSendViewController() }) + ] + + return Section(title: "Research", items: items) + } + // MARK: - Navigation Swipe Back func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, diff --git a/Feature Catalog/View Controllers/Research/MotionSendViewController.swift b/Feature Catalog/View Controllers/Research/MotionSendViewController.swift new file mode 100644 index 0000000..ab60dc4 --- /dev/null +++ b/Feature Catalog/View Controllers/Research/MotionSendViewController.swift @@ -0,0 +1,264 @@ +// +// MotionSendViewController +// Created on 4/14/18. +// Copyright © 2018 John Coates. All rights reserved. +// + +import UIKit +import CFNetwork +import CoreMotion + +class MotionSendViewController: UIViewController, StreamDelegate, +NetServiceBrowserDelegate, NetServiceDelegate { + + // MARK: - View Management + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.white + setUpButton() + setUpStackView() + start() + } + + // MARK: - View Events + + override func viewWillAppear(_ animated: Bool) { + super.viewDidAppear(animated) + start() + } + override func viewWillDisappear(_ animated: Bool) { + super.viewDidAppear(animated) + stop() + } + + let button = UIButton(type: .custom) + private func setUpButton() { + + button.setTitle("Restart", for: .normal) + button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) + button.setTitleColor(.black, for: .normal) + view.addSubview(button) + button.centerXY --> view + } + + let pitchLabel = UILabel() + let yawLabel = UILabel() + let rollLabel = UILabel() + let orientationLabel = UILabel() + private func setUpStackView() { + let stackView = UIStackView(arrangedSubviews: [ + pitchLabel, + yawLabel, + rollLabel, + orientationLabel + ]) + stackView.axis = .vertical + stackView.distribution = .equalSpacing + stackView.spacing = 15 + stackView.alignment = .leading + stackView.isLayoutMarginsRelativeArrangement = true + stackView.layoutMargins = UIEdgeInsets(top: 10, left: 10, + bottom: 10, right: 10) + + view.addSubview(stackView) + + stackView.bottom.pin(to: button.top, add: -15) + } + + // MARK: - Start, Stop + + var orientation: UIInterfaceOrientation = .unknown + + func start() { + RotationManager.updateInterval = 0.01 + RotationManager.shared.attitudeUpdated = { attitude in + var motionData = MotionData() + + DispatchQueue.main.async { + self.updateLabels(with: attitude) + } + + let quaternion = attitude.quaternion + motionData.x = quaternion.x + motionData.y = quaternion.y + motionData.z = quaternion.z + motionData.w = quaternion.w + let encoder = JSONEncoder() + do { + let json = try encoder.encode(motionData) + self.send(data: json) + } catch let error { + print("Couldn't send data, error: \(error)") + } + } + + if let host = hostAddress { + setUpStreams(host: host) + } else { + searchForHost() + } + + RotationManager.beginOrientationEvents() + } + + func updateLabels(with attitude: CMAttitude) { + let pitch = RotationManager.degrees(radians: attitude.pitch) + let yaw = RotationManager.degrees(radians: attitude.yaw) + let roll = RotationManager.degrees(radians: attitude.roll) + pitchLabel.text = "pitch: \(pitch)" + yawLabel.text = "yaw: \(yaw)" + rollLabel.text = "roll: \(roll)" + orientation = RotationManager.newOrientation(from: self.orientation, + pitch: pitch, + yaw: yaw, + roll: roll) + orientationLabel.text = self.orientation.description + } + + func stop() { + RotationManager.endOrientationEvents() + inputStream?.close() + outputStream?.close() + inputStream = nil + outputStream = nil + } + + // MARK: - Streams + + var inputStream: InputStream? + var outputStream: OutputStream? + + func setUpStreams(host: String) { + var readStream: Unmanaged? + var writeStream: Unmanaged? + CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, + host as CFString, 9845, + &readStream, + &writeStream) + inputStream = readStream!.takeRetainedValue() + outputStream = writeStream!.takeRetainedValue() + guard let inputStream = inputStream, let outputStream = outputStream else { + print("Failed to create streams") + return + } + inputStream.delegate = self + outputStream.delegate = self + inputStream.schedule(in: .current, forMode: .commonModes) + outputStream.schedule(in: .current, forMode: .commonModes) + inputStream.open() + outputStream.open() + } + + func send(data: Data) { + guard let outputStream = outputStream else { + return + } + _ = data.withUnsafeBytes { + outputStream.write($0, maxLength: data.count) + } + } + + let maxReadLength = 4096 + + public func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + if eventCode == .errorOccurred { + inputStream = nil + outputStream = nil + print("Error: Stream error") + } else if eventCode == .endEncountered { + inputStream = nil + outputStream = nil + print("Error: Encountered end of stream") + } + + if eventCode == .hasBytesAvailable { + guard let inputStream = inputStream else { + return + } + while inputStream.hasBytesAvailable { + let buffer = UnsafeMutablePointer.allocate(capacity: maxReadLength) + inputStream.read(buffer, maxLength: maxReadLength) + buffer.deallocate() + } + } + } + + // MARK: - User Interaction + + @objc func buttonTapped() { + stop() + start() + } + + // MARK: - Bonjour + + private let browser = NetServiceBrowser() + var searching = false + func searchForHost() { + guard searching == false else { + return + } + searching = true + print("searching for host") + browser.delegate = self + browser.searchForServices(ofType: "_companion-link._tcp", inDomain: "local.") + } + + private var hostService: NetService? + private var hostAddress: String? + + func netServiceBrowser(_ browser: NetServiceBrowser, + didFind service: NetService, moreComing: Bool) { + print("found service: \(service)") + service.delegate = self + service.resolve(withTimeout: 20) + hostService = service + browser.stop() + } + + func netServiceDidResolveAddress(_ sender: NetService) { + guard let addresses = sender.addresses else { + return + } + + for addressData in addresses { + let addressPointer = addressData.withUnsafeBytes { (pointer: UnsafePointer) in + return pointer + } + let rawAddress = addressPointer.pointee + + // IPv4 only + guard rawAddress.sin_family == AF_INET else { + continue + } + var host = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + let result = addressData.withUnsafeBytes { (sockAddress: UnsafePointer) in + getnameinfo(sockAddress, socklen_t(addressData.count), &host, + socklen_t(host.count), nil, 0, NI_NUMERICHOST) + + } + guard result == 0 else { + return + } + guard hostAddress == nil else { + return + } + let address = String(cString: host) + hostAddress = address + print("found host address: \(address)") + setUpStreams(host: address) + } + + } + +} + +// MARK: - Models + +private struct MotionData: Codable { + var x: Double = 0 + var y: Double = 0 + var z: Double = 0 + var w: Double = 0 +} diff --git a/Research/Motion/Blender/client.py b/Research/Motion/Blender/client.py new file mode 100644 index 0000000..f925f27 --- /dev/null +++ b/Research/Motion/Blender/client.py @@ -0,0 +1,6 @@ +import socket + +send = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +send.connect(("localhost", 9845)) +send.send("{ \"x\": 1.2, \"y\": 0, \"z\": 1.2, \"w\": 0}") +send.close() diff --git a/Research/Motion/Blender/server.py b/Research/Motion/Blender/server.py new file mode 100644 index 0000000..812deed --- /dev/null +++ b/Research/Motion/Blender/server.py @@ -0,0 +1,102 @@ +# Run the server with this Blender code: +# import bpy +# import os +# +# filename = os.path.join(os.path.dirname(bpy.data.filepath), "Blender Plugins/server.py") +# exec(compile(open(filename).read(), filename, 'exec')) # + +import socket +import select +import json +import threading +import traceback + +class ServerThread(threading.Thread): + def __init__(self): + threading.Thread.__init__(self) + self.running = True + + def stopServer(self): + self.running = False + self.server.running = False + + def run(self): + try: + self.server = Server() + while self.running: + self.server.receive() + except: + pass + + +class Server: + def __init__(self): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.setblocking(False) + self.socket.bind((str(socket.INADDR_ANY), 9845)) + self.socket.listen(2) + self.running = True + + def __exit__(self, exc_type, exc_value, traceback): + self.socket.close() + + def receive(self): + pairs = [] + timeout = 1 + while self.running: + sockets = list(map(lambda x: x[0], pairs)) + if len(pairs) > 0: + read_sockets, write_sockets, error_sockets = select.select(sockets, [], [], timeout) + for sock in read_sockets: + data = sock.recv(4096) + if not data : + print('Client disconnected') + pairs = [] + else : + self.connectionReceivedData(connection, data.decode()) + try: + try: + connection,address = self.socket.accept() + print("new connection: ", connection) + pairs.append((connection, address)) + except: + pass + + except: + pass + + for pair in pairs: + (connection, address) = pair + connection.close() + + def connectionReceivedData(self, connection, data): + try: + motionData = json.loads(data) + except json.decoder.JSONDecodeError: + print("Invalid JSON: ", data) + return None + receivedMotionData(motionData) + +# This is a global so when we run the script again, we can keep the server alive +# but change how it works +import bpy +def receivedMotionData(motionData): + phone = bpy.context.scene.objects["iPhone"] + phone.rotation_quaternion.x = float(motionData['x']) + phone.rotation_quaternion.y = 0 - float(motionData['z']) + phone.rotation_quaternion.z = float(motionData['y']) + phone.rotation_quaternion.w = float(motionData['w']) + pass + +try: + if serverThread.running == False: + serverThread = ServerThread() + serverThread.start() + print("Starting server") + else: + print("Server already running, using new motion handler.") +except: + serverThread = ServerThread() + serverThread.start() + print("Starting server") diff --git a/Slate.xcodeproj/project.pbxproj b/Slate.xcodeproj/project.pbxproj index 71bde71..49afa28 100644 --- a/Slate.xcodeproj/project.pbxproj +++ b/Slate.xcodeproj/project.pbxproj @@ -20,6 +20,8 @@ FA0882522080AD6200EA7081 /* CameraPermissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0882512080AD6200EA7081 /* CameraPermissionTests.swift */; }; FA0882552080AE0A00EA7081 /* UIViewController+Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0882542080AE0A00EA7081 /* UIViewController+Testing.swift */; }; FA0882592080B10C00EA7081 /* SimulatorPermissionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0882582080B10C00EA7081 /* SimulatorPermissionsManager.swift */; }; + FA08825D2080E88800EA7081 /* RotationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA08825C2080E88800EA7081 /* RotationManager.swift */; }; + FA08825E2080E88800EA7081 /* RotationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA08825C2080E88800EA7081 /* RotationManager.swift */; }; FA0C964F1EE7647300EA6EC8 /* AnchorOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0C964E1EE7647300EA6EC8 /* AnchorOperators.swift */; }; FA0C96501EE7647300EA6EC8 /* AnchorOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0C964E1EE7647300EA6EC8 /* AnchorOperators.swift */; }; FA0C96591EE8952800EA6EC8 /* Anchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0C96581EE8952800EA6EC8 /* Anchor.swift */; }; @@ -160,6 +162,8 @@ FA373E58207B22AF0092BA67 /* BarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA373E56207B22AF0092BA67 /* BarButton.swift */; }; FA373E5B207B22D10092BA67 /* KitSettingsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA373E5A207B22D10092BA67 /* KitSettingsDataSource.swift */; }; FA373E5C207B22D10092BA67 /* KitSettingsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA373E5A207B22D10092BA67 /* KitSettingsDataSource.swift */; }; + FA39249C20832BE300EB929E /* Orientation+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA39249B20832BE300EB929E /* Orientation+Convenience.swift */; }; + FA39249D20832C1200EB929E /* Orientation+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA39249B20832BE300EB929E /* Orientation+Convenience.swift */; }; FA3AAA8B1EE24C5B006028AC /* CameraController+StillOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3AAA8A1EE24C5B006028AC /* CameraController+StillOutput.swift */; }; FA3AAA8D1EE24C6E006028AC /* CameraController+Presets.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3AAA8C1EE24C6E006028AC /* CameraController+Presets.swift */; }; FA3AAA8F1EE24C83006028AC /* CameraController+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3AAA8E1EE24C83006028AC /* CameraController+Preview.swift */; }; @@ -652,6 +656,7 @@ FAF5AB241EEBCD5B00EDED04 /* EditingDeleteImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF5AB231EEBCD5B00EDED04 /* EditingDeleteImage.swift */; }; FAF5AB271EEBCE1F00EDED04 /* PathsImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF5AB251EEBCDFE00EDED04 /* PathsImageButton.swift */; }; FAF5AB281EEBCE2100EDED04 /* PathsImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF5AB251EEBCDFE00EDED04 /* PathsImageButton.swift */; }; + FAFFD1FB2082049F000C5B73 /* MotionSendViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFD1FA2082049F000C5B73 /* MotionSendViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXBuildRule section */ @@ -794,6 +799,7 @@ FA0882512080AD6200EA7081 /* CameraPermissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPermissionTests.swift; sourceTree = ""; }; FA0882542080AE0A00EA7081 /* UIViewController+Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Testing.swift"; sourceTree = ""; }; FA0882582080B10C00EA7081 /* SimulatorPermissionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorPermissionsManager.swift; sourceTree = ""; }; + FA08825C2080E88800EA7081 /* RotationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotationManager.swift; sourceTree = ""; }; FA0C964E1EE7647300EA6EC8 /* AnchorOperators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnchorOperators.swift; sourceTree = ""; }; FA0C96581EE8952800EA6EC8 /* Anchor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Anchor.swift; sourceTree = ""; }; FA0C965D1EE8C52900EA6EC8 /* Priority.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Priority.swift; sourceTree = ""; }; @@ -845,6 +851,7 @@ FA373E51207AE8220092BA67 /* NSManagedObject+Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Migration.swift"; sourceTree = ""; }; FA373E56207B22AF0092BA67 /* BarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarButton.swift; sourceTree = ""; }; FA373E5A207B22D10092BA67 /* KitSettingsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KitSettingsDataSource.swift; sourceTree = ""; }; + FA39249B20832BE300EB929E /* Orientation+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Orientation+Convenience.swift"; sourceTree = ""; }; FA3AAA8A1EE24C5B006028AC /* CameraController+StillOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraController+StillOutput.swift"; sourceTree = ""; }; FA3AAA8C1EE24C6E006028AC /* CameraController+Presets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraController+Presets.swift"; sourceTree = ""; }; FA3AAA8E1EE24C83006028AC /* CameraController+Preview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraController+Preview.swift"; sourceTree = ""; }; @@ -1125,6 +1132,7 @@ FAF5AB231EEBCD5B00EDED04 /* EditingDeleteImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditingDeleteImage.swift; sourceTree = ""; }; FAF5AB251EEBCDFE00EDED04 /* PathsImageButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathsImageButton.swift; sourceTree = ""; }; FAF5AB291EEC6B5600EDED04 /* Lint.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Lint.xcconfig; sourceTree = ""; }; + FAFFD1FA2082049F000C5B73 /* MotionSendViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionSendViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1326,6 +1334,22 @@ path = Utilities; sourceTree = ""; }; + FA08825A2080E86200EA7081 /* Controllers */ = { + isa = PBXGroup; + children = ( + FA08825B2080E87100EA7081 /* Motion */, + ); + path = Controllers; + sourceTree = ""; + }; + FA08825B2080E87100EA7081 /* Motion */ = { + isa = PBXGroup; + children = ( + FA08825C2080E88800EA7081 /* RotationManager.swift */, + ); + path = Motion; + sourceTree = ""; + }; FA0C964D1EE7645B00EA6EC8 /* Operators */ = { isa = PBXGroup; children = ( @@ -1685,6 +1709,14 @@ path = Protocols; sourceTree = ""; }; + FA39249A20832BD500EB929E /* Device */ = { + isa = PBXGroup; + children = ( + FA39249B20832BE300EB929E /* Orientation+Convenience.swift */, + ); + path = Device; + sourceTree = ""; + }; FA3AAA891EE24C35006028AC /* Extensions */ = { isa = PBXGroup; children = ( @@ -2506,6 +2538,7 @@ FA8CE4201ECD7E4600788026 /* UI */ = { isa = PBXGroup; children = ( + FA39249A20832BD500EB929E /* Device */, FA7648FF1F9EDEB800C6DD87 /* Navigation */, FA7648FB1F9EDBD700C6DD87 /* Responders */, FA60B68C1EF374F7003441E1 /* Views */, @@ -2613,6 +2646,7 @@ FA9F89E41D953C540058C369 /* Source */ = { isa = PBXGroup; children = ( + FA08825A2080E86200EA7081 /* Controllers */, FA9B341D207DA6570098D2FE /* Globals */, FACF0F391EECD95C00B0211C /* Database */, FA89D1341EDF5C200012F2F0 /* Metal */, @@ -2823,6 +2857,7 @@ FAB0AA7C1EC808CD008DCD23 /* View Controllers */ = { isa = PBXGroup; children = ( + FAFFD1F92082046E000C5B73 /* Research */, FACF0F441EECEC8C00B0211C /* Database */, FAAE76491EEB36D100CA3416 /* Vector Images */, FA2B1E5A1EC80EC800E44613 /* Buttons */, @@ -3556,6 +3591,14 @@ path = SwitchCamera; sourceTree = ""; }; + FAFFD1F92082046E000C5B73 /* Research */ = { + isa = PBXGroup; + children = ( + FAFFD1FA2082049F000C5B73 /* MotionSendViewController.swift */, + ); + path = Research; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -4402,6 +4445,7 @@ FAB0AA5E1EC6A095008DCD23 /* EditPosition.swift in Sources */, FAD5DACA1EF8ABB100239C96 /* EditKitLayoutViewController.swift in Sources */, FA0462B31EC3CC76007B74A0 /* SwitchCameraButton.swift in Sources */, + FA08825D2080E88800EA7081 /* RotationManager.swift in Sources */, FAD5DAE51EF8D34C00239C96 /* SmartPin.swift in Sources */, FADFC6C61F4E71220023B92C /* PhotoSettings.swift in Sources */, FAB64A252080540500106268 /* LayoutAnchors.swift in Sources */, @@ -4412,6 +4456,7 @@ FAAE767A1EEB881C00CA3416 /* DataPoint.swift in Sources */, FA9F89E61D953C540058C369 /* AppDelegate.swift in Sources */, FA8AA5451ECC13140093D317 /* CanvasIconView.swift in Sources */, + FA39249C20832BE300EB929E /* Orientation+Convenience.swift in Sources */, FA7649011F9EE1C600C6DD87 /* NavigationViewController+Convenience.swift in Sources */, FA9B342F207DBA220098D2FE /* BurstSpeed.swift in Sources */, FAC424921EEF2F7700E2EB26 /* NSManagedObjectContext+Changes.swift in Sources */, @@ -4669,11 +4714,13 @@ FAF5AB271EEBCE1F00EDED04 /* PathsImageButton.swift in Sources */, FAB64A1B20804CF200106268 /* UILayoutSupport+Anchors.swift in Sources */, FA2B1E7D1EC92B5B00E44613 /* Kit.swift in Sources */, + FA39249D20832C1200EB929E /* Orientation+Convenience.swift in Sources */, FAC4248C1EEF28EE00E2EB26 /* ManagedObjectObserver.swift in Sources */, FAAE76401EEB2F3800CA3416 /* DataPath.swift in Sources */, FAB64A23208053D500106268 /* LayoutAnchorGenerator.swift in Sources */, FA2B1E6E1EC9235700E44613 /* PathIcon.swift in Sources */, FAAB69CD207F1D5B00246918 /* TestCamera.swift in Sources */, + FAFFD1FB2082049F000C5B73 /* MotionSendViewController.swift in Sources */, FA5651AE1EDE4AB6006B5A19 /* AVPreviewCaptureViewController.swift in Sources */, FA0C9FA420791F8500534901 /* SimulatorCamera.swift in Sources */, FA2B1E5C1EC80EE100E44613 /* InvertedMaskButtonViewController.swift in Sources */, @@ -4700,6 +4747,7 @@ FA2B1E891EC92B5B00E44613 /* FragmentFilter.swift in Sources */, FA2B1EA61EC92B5B00E44613 /* DebugBarItemView.swift in Sources */, FACA797C1EE66A5B0001D468 /* KitTableViewCell.swift in Sources */, + FA08825E2080E88800EA7081 /* RotationManager.swift in Sources */, FA3E23F3207967B8008D5227 /* KitSettingsViewController.swift in Sources */, FA5F8CA01ECFF81400921A61 /* UIScreen+Layout.swift in Sources */, FA7648FE1F9EDC0B00C6DD87 /* KeyboardHandler.swift in Sources */, diff --git a/Source/Controllers/Motion/RotationManager.swift b/Source/Controllers/Motion/RotationManager.swift new file mode 100644 index 0000000..0c70ebd --- /dev/null +++ b/Source/Controllers/Motion/RotationManager.swift @@ -0,0 +1,143 @@ +// +// RotationManager +// Created on 4/13/18. +// Copyright © 2018 John Coates. All rights reserved. +// + +import UIKit +import CoreGraphics +import CoreMotion + +final class RotationManager { + + // MARK: - Public + + static let shared = RotationManager() + static var requests: Int = 0 + + static func beginOrientationEvents() { + requests += 1 + if requests == 1 { + shared.start() + } + } + + static func endOrientationEvents() { + requests -= 1 + if requests == 0 { + shared.stop() + } else if requests < 0 { + fatalError("Unbalanced call to \(#function)") + } + } + + // MARK: - Init + + private let queue = OperationQueue() + private let motionManager = CMMotionManager() + + private init() { + if !motionManager.isAccelerometerAvailable, + Platform.isSimulator { + print("No rotation events for simulator") + } + } + + // MARK: - Start, Stop + + var attitudeUpdated: ((CMAttitude) -> Void)? + static var updateInterval = 0.2 + private func start() { + guard motionManager.isAccelerometerAvailable else { + print("Error: No accelerometer available!") + return + } + motionManager.deviceMotionUpdateInterval = RotationManager.updateInterval + motionManager.startDeviceMotionUpdates(using: .xArbitraryZVertical, to: queue) { [unowned self] data, error in + guard let data = data else { + let error = Critical.unwrap(error) + print("Error receiving accelerometer data: \(error)") + return + } + + let attitude = data.attitude + self.attitudeUpdated?(data.attitude) + + let pitch = RotationManager.degrees(radians: attitude.pitch) + let yaw = RotationManager.degrees(radians: attitude.yaw) + let roll = RotationManager.degrees(radians: attitude.roll) + self.interfaceOrientation = RotationManager.newOrientation(from: self.interfaceOrientation, + pitch: pitch, yaw: yaw, roll: roll) + } + } + + private func stop() { + motionManager.stopAccelerometerUpdates() + } + + // MARK: - Calculate orientation + + static func degrees(radians: Double) -> Int { + return Int(180 / Double.pi * radians) + } + + static var orientation: UIInterfaceOrientation { + return shared.interfaceOrientation + } + + private var interfaceOrientation: UIInterfaceOrientation = .unknown + + // swiftlint:disable:next cyclomatic_complexity + static func newOrientation(from previous: UIInterfaceOrientation, + pitch: Int, yaw: Int, roll: Int) -> UIInterfaceOrientation { + + let isPointingDown = { (roll: Int, pitch: Int) in + return abs(roll) < 30 && abs(pitch) < 30 + } + + let isPointingUp = { (roll: Int, pitch: Int) in + return abs(roll) > 150 && abs(pitch) < 40 + } + + if isPointingDown(roll, pitch) || isPointingUp(roll, pitch) { + if case .unknown = previous { + return .portrait + } + return previous + } + + switch previous { + case .landscapeRight, .landscapeLeft: + if pitch > 50 { + return .portrait + } else if pitch < -50 { + return .portraitUpsideDown + } + case .portrait: + if pitch < 30 && roll < 0 { + return .landscapeLeft + } else if pitch < 30 && roll > 0 { + return .landscapeRight + } + case .portraitUpsideDown: + if pitch > -40 && roll < 0 { + return .landscapeLeft + } else if pitch > -40 && roll > 0 { + return .landscapeRight + } + return .portraitUpsideDown + case .unknown: + if pitch > 30 { + return .portrait + } else if pitch < -50 { + return .portraitUpsideDown + } else if roll < 0 { + return .landscapeLeft + } else { + return .landscapeRight + } + } + + return previous + } +} diff --git a/Source/Extensions/UI/Device/Orientation+Convenience.swift b/Source/Extensions/UI/Device/Orientation+Convenience.swift new file mode 100644 index 0000000..dce700e --- /dev/null +++ b/Source/Extensions/UI/Device/Orientation+Convenience.swift @@ -0,0 +1,56 @@ +// +// Orientation+Convenience +// Created on 4/15/18. +// Copyright © 2018 John Coates. All rights reserved. +// + +import UIKit + +extension UIInterfaceOrientation: CustomStringConvertible { + var device: UIDeviceOrientation { + switch self { + case .landscapeLeft: + return .landscapeLeft + case .landscapeRight: + return .landscapeRight + case .portraitUpsideDown: + return .portraitUpsideDown + case .portrait: + fallthrough + default: + return .portrait + } + } + + public var description: String { + switch self { + case .landscapeLeft: + return "landscapeLeft" + case .landscapeRight: + return "landscapeRight" + case .portraitUpsideDown: + return "portraitUpsideDown" + case .portrait: + return "portrait" + case .unknown: + return "unknown" + } + } +} + +extension UIDeviceOrientation { + var interface: UIInterfaceOrientation { + switch self { + case .landscapeLeft: + return .landscapeLeft + case .landscapeRight: + return .landscapeRight + case .portraitUpsideDown: + return .portraitUpsideDown + case .portrait: + fallthrough + default: + return .portrait + } + } +} diff --git a/Source/UI/Capture/View Controllers/MetalCaptureViewController.swift b/Source/UI/Capture/View Controllers/MetalCaptureViewController.swift index b75460f..0020d2d 100644 --- a/Source/UI/Capture/View Controllers/MetalCaptureViewController.swift +++ b/Source/UI/Capture/View Controllers/MetalCaptureViewController.swift @@ -15,6 +15,21 @@ import ImageIO final class MetalCaptureViewController: BaseCaptureViewController, AVCapturePhotoCaptureDelegate { + // MARK: - Init + + required init(kit: Kit) { + super.init(kit: kit) + RotationManager.beginOrientationEvents() + } + + required init(coder aDecoder: NSCoder) { + Critical.methodNotDefined() + } + + deinit { + RotationManager.endOrientationEvents() + } + // MARK: - Setup lazy var metalView = MTKView() @@ -57,30 +72,88 @@ final class MetalCaptureViewController: BaseCaptureViewController, AVCapturePhot } stillOutput.captureStillImageAsynchronously(from: videoConnection) { sampleBuffer, error in - guard let sampleBuffer = sampleBuffer, let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { - print("Error: Can't save image, couldn't get image buffer") + guard let sampleBuffer = sampleBuffer else { + print("Error: Can't save image, nil sample buffer") return } - let image = CIImage(cvImageBuffer: imageBuffer) - - let data = NSMutableData() - guard let destination = CGImageDestinationCreateWithData(data as CFMutableData, kUTTypeJPEG, 1, nil) else { - print("Couldn't create image destination!") + guard CMSampleBufferIsValid(sampleBuffer) else { + print("Error: invalid sample buffer") return } - let conversionContext = CIContext(mtlDevice: renderer.device) - guard let cgImage = conversionContext.createCGImage(image, from: image.extent) else { - print("Couldn't convert image to CGImage") +// CFDictionaryRef exifAttachments = +// CMGetAttachment(imageSampleBuffer, kCGImagePropertyExifDictionary, NULL); +// if (exifAttachments) { +// // Do something with the attachments. +// } + + guard let imageData = self.getJpeg(from32BGRA: sampleBuffer) else { + print("Error: Failed to save image.") return } - CGImageDestinationAddImage(destination, cgImage, nil) - CGImageDestinationFinalize(destination) - ImageCaptureManager.captured(imageData: data as Data) + ImageCaptureManager.captured(imageData: imageData) + } + + } + + private func jpeg(from buffer: CMSampleBuffer) -> Data? { + return AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(buffer) + } + + @available (iOS 11.0, *) + private func jpegPhoto(from buffer: CMSampleBuffer) -> Data? { + return AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: buffer, + previewPhotoSampleBuffer: nil) + } + + private func getJpeg(from32BGRA buffer: CMSampleBuffer) -> Data? { + guard let imageBuffer = CMSampleBufferGetImageBuffer(buffer) else { + print("Error: Couldn't get image buffer") + return nil } + guard let renderer = renderer else { + print("Error: Missing renderer") + return nil + } + + let image = CIImage(cvImageBuffer: imageBuffer) + let data = NSMutableData() + guard let destination = CGImageDestinationCreateWithData(data as CFMutableData, kUTTypeJPEG, 1, nil) else { + print("Couldn't create image destination!") + return nil + } + + let conversionContext = CIContext(mtlDevice: renderer.device) + guard let cgImage = conversionContext.createCGImage(image, from: image.extent) else { + print("Couldn't convert image to CGImage") + return nil + } + + var properties: [CFString: Any] = [ + kCGImageDestinationMergeMetadata: 1 + ] + + let phoneOrientation = RotationManager.orientation + let orientation: Int + switch phoneOrientation { + case .landscapeRight: + orientation = 3 + case .portrait: + orientation = 6 + case .portraitUpsideDown: + orientation = 8 + case .landscapeLeft: + fallthrough + default: + orientation = 1 + } + properties[kCGImagePropertyOrientation] = orientation + CGImageDestinationAddImage(destination, cgImage, properties as NSDictionary) + CGImageDestinationFinalize(destination) + return data as Data } // MARK: - Camera Switching