├── CloudCube
├── .DS_Store
├── CloudCube
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── edIcon.imageset
│ │ │ ├── edIcon.png
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── Icon.png
│ │ │ ├── Icon-40.png
│ │ │ ├── Icon-72.png
│ │ │ ├── Icon-76.png
│ │ │ ├── Icon@2x.png
│ │ │ ├── Icon-40@2x.png
│ │ │ ├── Icon-40@3x.png
│ │ │ ├── Icon-60@2x.png
│ │ │ ├── Icon-60@3x.png
│ │ │ ├── Icon-72@2x.png
│ │ │ ├── Icon-76@2x.png
│ │ │ ├── Icon-Small.png
│ │ │ ├── Icon-83.5@2x.png
│ │ │ ├── Icon-Small-50.png
│ │ │ ├── Icon-Small@2x.png
│ │ │ ├── Icon-Small@3x.png
│ │ │ ├── Icon-Small-50@2x.png
│ │ │ ├── NotificationIcon@2x.png
│ │ │ ├── NotificationIcon@3x.png
│ │ │ ├── NotificationIcon~ipad.png
│ │ │ ├── NotificationIcon~ipad@2x.png
│ │ │ └── Contents.json
│ │ ├── restart.imageset
│ │ │ ├── restart@2x.png
│ │ │ ├── restart@3x.png
│ │ │ └── Contents.json
│ │ └── edIconSmall.imageset
│ │ │ ├── edIconSmall.png
│ │ │ └── Contents.json
│ ├── AppDelegate.swift
│ ├── Info.plist
│ ├── Apple Focus Square
│ │ ├── Utilities
│ │ │ └── Utilities.swift
│ │ ├── FocusSquare+Extensions.swift
│ │ ├── FocusSquareSegment.swift
│ │ └── FocusSquare.swift
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Custom Views
│ │ └── BlackMirrorzViews.swift
│ ├── Custom Nodes
│ │ └── Box.swift
│ ├── Multipeer
│ │ └── ARCloudShare.swift
│ ├── AR Settings
│ │ └── ARSettings.swift
│ └── AR ViewControllers
│ │ ├── ViewController+Interactions.swift
│ │ ├── ARDelegation+Updates.swift
│ │ └── ViewController.swift
└── CloudCube.xcodeproj
│ ├── xcuserdata
│ └── saurus.xcuserdatad
│ │ ├── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── saurus.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── project.pbxproj
├── LICENSE
└── README.md
/CloudCube/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/.DS_Store
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/edIcon.imageset/edIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/edIcon.imageset/edIcon.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-40.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-72.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon@2x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/restart.imageset/restart@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/restart.imageset/restart@2x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/restart.imageset/restart@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/restart.imageset/restart@3x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube.xcodeproj/xcuserdata/saurus.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/edIconSmall.imageset/edIconSmall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/edIconSmall.imageset/edIconSmall.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png
--------------------------------------------------------------------------------
/CloudCube/CloudCube.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube.xcodeproj/project.xcworkspace/xcuserdata/saurus.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlackMirrorz/ARKitWorldMaps/HEAD/CloudCube/CloudCube.xcodeproj/project.xcworkspace/xcuserdata/saurus.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/CloudCube/CloudCube.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/edIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "edIcon.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/edIconSmall.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "edIconSmall.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/CloudCube/CloudCube.xcodeproj/xcuserdata/saurus.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | CloudCube.xcscheme
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/restart.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "restart@2x.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "restart@3x.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/CloudCube/CloudCube/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // CloudCube
4 | //
5 | // Created by Josh Robbins on 15/06/2018.
6 | // Copyright © 2018 BlackMirrorz. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18 |
19 | return true
20 | }
21 |
22 | func applicationWillResignActive(_ application: UIApplication) { }
23 |
24 | func applicationDidEnterBackground(_ application: UIApplication) { }
25 |
26 | func applicationWillEnterForeground(_ application: UIApplication) { }
27 |
28 | func applicationDidBecomeActive(_ application: UIApplication) { }
29 |
30 | func applicationWillTerminate(_ application: UIApplication) { }
31 |
32 |
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Josh Robbins
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # ARKitWorldMaps
3 |
4 | This project is a basic example of using `MultipeerConnectivity` within `ARKit2` to share `ARWorldMaps` and `ARAnchors` across multiple devices.
5 |
6 | All the code is fully commented so the apps functionality should be clear to everyone.
7 |
8 | **Branches:**
9 |
10 | The Master Branch was originally compiled in XCode10 Beta using Swift 4.
11 |
12 | An updated Branch called 'Swift4.2' contains the project built in XCode 10.5 Beta and uses Swift 4.2.
13 |
14 | **Requirements:**
15 |
16 | The project is setup for iPhone, and in Landscape Orientation.
17 |
18 | **Core Functionality:**
19 |
20 | The application automatically creates an `MCSession` which can be found in `ARCloudShare.swift` and shares `ARWorldMaps` and `ARAnchors` in real time.
21 |
22 | Using `NSKeyedArchiver` and `NSKeyedUnarchiver` the users can rotate (using a `UIPanGestureRecognizer`), scale (using `UIPinchGestureRecognizer`), and change the colours of each of the faces of a single `SCNBox` using a `UITapGestureRecognizer`.
23 |
24 | For the purpose of this application, I have only allowed placement of one Cube but this can easily be adapted to meet your needs.
25 |
26 | The cube can only be placed on a detected ARPlaneAnchor, which again can easily be customised as per your needs.
27 |
28 | The core idea behind this app, was a basic comparison between the functionality of `Google Cloud Anchors`, and `ARKit`, with `ARKit` winning easily.
29 |
30 | To change the colour of one of the faces of the cube, you need to double tap on the face and then select one of the colours from the bottom menu.
31 |
32 |
33 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | NSCameraUsageDescription
24 | For BlackMirrorz AR
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationPortraitUpsideDown
37 |
38 | UISupportedInterfaceOrientations~ipad
39 |
40 | UIInterfaceOrientationPortrait
41 | UIInterfaceOrientationPortraitUpsideDown
42 | UIInterfaceOrientationLandscapeLeft
43 | UIInterfaceOrientationLandscapeRight
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Apple Focus Square/Utilities/Utilities.swift:
--------------------------------------------------------------------------------
1 | /*
2 | See LICENSE folder for this sample’s licensing information.
3 |
4 | Abstract:
5 | Utility functions and type extensions used throughout the projects.
6 | */
7 |
8 | import Foundation
9 | import ARKit
10 |
11 | // MARK: - float4x4 extensions
12 |
13 | extension float4x4 {
14 | /**
15 | Treats matrix as a (right-hand column-major convention) transform matrix
16 | and factors out the translation component of the transform.
17 | */
18 | var translation: float3 {
19 | get {
20 | let translation = columns.3
21 | return float3(translation.x, translation.y, translation.z)
22 | }
23 | set(newValue) {
24 | columns.3 = float4(newValue.x, newValue.y, newValue.z, columns.3.w)
25 | }
26 | }
27 |
28 | /**
29 | Factors out the orientation component of the transform.
30 | */
31 | var orientation: simd_quatf {
32 | return simd_quaternion(self)
33 | }
34 |
35 | /**
36 | Creates a transform matrix with a uniform scale factor in all directions.
37 | */
38 | init(uniformScale scale: Float) {
39 | self = matrix_identity_float4x4
40 | columns.0.x = scale
41 | columns.1.y = scale
42 | columns.2.z = scale
43 | }
44 | }
45 |
46 | // MARK: - CGPoint extensions
47 |
48 | extension CGPoint {
49 | /// Extracts the screen space point from a vector returned by SCNView.projectPoint(_:).
50 | init(_ vector: SCNVector3) {
51 | self.init(x: CGFloat(vector.x), y: CGFloat(vector.y))
52 | }
53 |
54 | /// Returns the length of a point when considered as a vector. (Used with gesture recognizers.)
55 | var length: CGFloat {
56 | return sqrt(x * x + y * y)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Custom Views/BlackMirrorzViews.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BlackMirrorzViews.swift
3 | // CloudCube
4 | //
5 | // Created by Josh Robbins on 15/06/2018.
6 | // Copyright © 2018 BlackMirrorz. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import ARKit
11 |
12 | //---------------------------------
13 | //MARK: - BlackMirrorz Round Button
14 | //---------------------------------
15 |
16 | @IBDesignable public class BlackMirrorzRoundButton: UIButton {
17 |
18 | @IBInspectable var borderColor: UIColor = .white{
19 | didSet {
20 | layer.borderColor = borderColor.cgColor
21 | }
22 | }
23 |
24 | @IBInspectable var borderWidth: CGFloat = 2.0 {
25 | didSet {
26 | layer.borderWidth = borderWidth
27 | }
28 | }
29 |
30 | @IBInspectable var cornerRadius: CGFloat = 0 {
31 | didSet {
32 | layer.cornerRadius = 0.5 * bounds.size.width
33 |
34 | }
35 | }
36 |
37 | override public func layoutSubviews() {
38 | super.layoutSubviews()
39 | self.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
40 | clipsToBounds = true
41 | }
42 | }
43 |
44 | //------------------------------------
45 | //MARK: - BlackMirrorz Round ImageView
46 | //------------------------------------
47 |
48 | @IBDesignable public class BlackMirrorzRoundImageView: UIImageView {
49 |
50 | @IBInspectable var borderColor: UIColor = .white{
51 | didSet {
52 | layer.borderColor = borderColor.cgColor
53 | }
54 | }
55 |
56 | @IBInspectable var borderWidth: CGFloat = 2.0 {
57 | didSet {
58 | layer.borderWidth = borderWidth
59 | }
60 | }
61 |
62 | @IBInspectable var cornerRadius: CGFloat = 0 {
63 | didSet {
64 | layer.cornerRadius = 0.5 * bounds.size.width
65 |
66 | }
67 | }
68 |
69 | override public func layoutSubviews() {
70 | super.layoutSubviews()
71 | clipsToBounds = true
72 | }
73 | }
74 |
75 | //-------------------------------
76 | //MARK: - BlackMirrorz Round View
77 | //-------------------------------
78 |
79 | @IBDesignable public class BlackMirrorzRoundView: UIView {
80 |
81 | public override init(frame: CGRect) {
82 | super.init(frame: frame)
83 | layoutView()
84 | }
85 |
86 | public required init?(coder aDecoder: NSCoder) {
87 | super.init(coder: aDecoder)
88 | layoutView()
89 | }
90 |
91 | func layoutView(){
92 |
93 | self.layer.borderColor = UIColor.white.cgColor
94 | self.layer.borderWidth = 2
95 | self.layer.cornerRadius = 5
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Apple Focus Square/FocusSquare+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FocusSquare+Extensions.swift
3 | // TCloud
4 | //
5 | // Created by Josh Robbins on 04/06/2018.
6 | // Copyright © 2018 BlackMirrorz. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SceneKit
11 | import ARKit
12 |
13 | extension ARSCNView{
14 |
15 | func smartHitTest(_ point: CGPoint,
16 | infinitePlane: Bool = false,
17 | objectPosition: float3? = nil,
18 | allowedAlignments: [ARPlaneAnchor.Alignment] = [.horizontal, .vertical]) -> ARHitTestResult? {
19 |
20 | // Perform the hit test.
21 | let results = hitTest(point, types: [.existingPlaneUsingGeometry, .estimatedVerticalPlane, .estimatedHorizontalPlane])
22 |
23 | // 1. Check for a result on an existing plane using geometry.
24 | if let existingPlaneUsingGeometryResult = results.first(where: { $0.type == .existingPlaneUsingGeometry }),
25 | let planeAnchor = existingPlaneUsingGeometryResult.anchor as? ARPlaneAnchor, allowedAlignments.contains(planeAnchor.alignment) {
26 | return existingPlaneUsingGeometryResult
27 | }
28 |
29 | if infinitePlane {
30 |
31 | // 2. Check for a result on an existing plane, assuming its dimensions are infinite.
32 | // Loop through all hits against infinite existing planes and either return the
33 | // nearest one (vertical planes) or return the nearest one which is within 5 cm
34 | // of the object's position.
35 | let infinitePlaneResults = hitTest(point, types: .existingPlane)
36 |
37 | for infinitePlaneResult in infinitePlaneResults {
38 | if let planeAnchor = infinitePlaneResult.anchor as? ARPlaneAnchor, allowedAlignments.contains(planeAnchor.alignment) {
39 | if planeAnchor.alignment == .vertical {
40 | // Return the first vertical plane hit test result.
41 | return infinitePlaneResult
42 | } else {
43 | // For horizontal planes we only want to return a hit test result
44 | // if it is close to the current object's position.
45 | if let objectY = objectPosition?.y {
46 | let planeY = infinitePlaneResult.worldTransform.translation.y
47 | if objectY > planeY - 0.05 && objectY < planeY + 0.05 {
48 | return infinitePlaneResult
49 | }
50 | } else {
51 | return infinitePlaneResult
52 | }
53 | }
54 | }
55 | }
56 | }
57 |
58 | // 3. As a final fallback, check for a result on estimated planes.
59 | let vResult = results.first(where: { $0.type == .estimatedVerticalPlane })
60 | let hResult = results.first(where: { $0.type == .estimatedHorizontalPlane })
61 | switch (allowedAlignments.contains(.horizontal), allowedAlignments.contains(.vertical)) {
62 | case (true, false):
63 | return hResult
64 | case (false, true):
65 | // Allow fallback to horizontal because we assume that objects meant for vertical placement
66 | // (like a picture) can always be placed on a horizontal surface, too.
67 | return vResult ?? hResult
68 | case (true, true):
69 | if hResult != nil && vResult != nil {
70 | return hResult!.distance < vResult!.distance ? hResult! : vResult!
71 | } else {
72 | return hResult ?? vResult
73 | }
74 | default:
75 | return nil
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "NotificationIcon@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "NotificationIcon@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-Small.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-Small@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-Small@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "57x57",
47 | "idiom" : "iphone",
48 | "filename" : "Icon.png",
49 | "scale" : "1x"
50 | },
51 | {
52 | "size" : "57x57",
53 | "idiom" : "iphone",
54 | "filename" : "Icon@2x.png",
55 | "scale" : "2x"
56 | },
57 | {
58 | "size" : "60x60",
59 | "idiom" : "iphone",
60 | "filename" : "Icon-60@2x.png",
61 | "scale" : "2x"
62 | },
63 | {
64 | "size" : "60x60",
65 | "idiom" : "iphone",
66 | "filename" : "Icon-60@3x.png",
67 | "scale" : "3x"
68 | },
69 | {
70 | "size" : "20x20",
71 | "idiom" : "ipad",
72 | "filename" : "NotificationIcon~ipad.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "20x20",
77 | "idiom" : "ipad",
78 | "filename" : "NotificationIcon~ipad@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "29x29",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-Small.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "29x29",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-Small@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "40x40",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-40.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "40x40",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-40@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "50x50",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-Small-50.png",
109 | "scale" : "1x"
110 | },
111 | {
112 | "size" : "50x50",
113 | "idiom" : "ipad",
114 | "filename" : "Icon-Small-50@2x.png",
115 | "scale" : "2x"
116 | },
117 | {
118 | "size" : "72x72",
119 | "idiom" : "ipad",
120 | "filename" : "Icon-72.png",
121 | "scale" : "1x"
122 | },
123 | {
124 | "size" : "72x72",
125 | "idiom" : "ipad",
126 | "filename" : "Icon-72@2x.png",
127 | "scale" : "2x"
128 | },
129 | {
130 | "size" : "76x76",
131 | "idiom" : "ipad",
132 | "filename" : "Icon-76.png",
133 | "scale" : "1x"
134 | },
135 | {
136 | "size" : "76x76",
137 | "idiom" : "ipad",
138 | "filename" : "Icon-76@2x.png",
139 | "scale" : "2x"
140 | },
141 | {
142 | "size" : "83.5x83.5",
143 | "idiom" : "ipad",
144 | "filename" : "Icon-83.5@2x.png",
145 | "scale" : "2x"
146 | },
147 | {
148 | "idiom" : "ios-marketing",
149 | "size" : "1024x1024",
150 | "scale" : "1x"
151 | }
152 | ],
153 | "info" : {
154 | "version" : 1,
155 | "author" : "xcode"
156 | }
157 | }
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Custom Nodes/Box.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BoxNode.swift
3 | // CloudCube
4 | //
5 | // Created by Josh Robbins on 15/06/2018.
6 | // Copyright © 2018 BlackMirrorz. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import ARKit
11 |
12 |
13 | /// The Geometry Index Of An SCNBox Geometry
14 | enum BoxFaces: Int{
15 |
16 | case Front, Right, Back, Left, Top, Botton
17 | }
18 |
19 | class Box: SCNNode {
20 |
21 | private var faceArray = [SCNMaterial]()
22 |
23 | /// Creates An SCNBox With A Single Colour Or Image For It's Material
24 | ///
25 | /// - Parameters:
26 | /// - width: Optional CGFloat (Defaults To 20cm)
27 | /// - height: Optional CGFloat (Defaults To 20cm)
28 | /// - length: Optional CGFloat (Defaults To 20cm)
29 | /// - chamferRadius: Optional CGFloat (Defaults To 0cm)
30 | /// - colour: Optional UIColor
31 | /// - image: Optional UIColor
32 | init(width: CGFloat = 0.2, height: CGFloat = 0.2, length: CGFloat = 0.2, chamferRadius: CGFloat = 0, colour: UIColor?, image: UIImage?) {
33 |
34 | super.init()
35 |
36 | //1. Create The Box Geometry With Our Width, Height, Length & Chamfer Radius Parameters
37 | self.geometry = SCNBox(width: width, height: height, length: length, chamferRadius: chamferRadius)
38 |
39 | //2. Create A New Material
40 | let material = SCNMaterial()
41 |
42 | //3. If A Colour Has Not Be Set The Then Material Will Be A UIImage
43 | if colour == nil{
44 | material.diffuse.contents = image
45 | }else{
46 | //The Material Will Be A UIColor
47 | material.diffuse.contents = colour
48 | }
49 |
50 | //4. Set The Material Of The Box
51 | self.geometry?.firstMaterial = material
52 |
53 | }
54 |
55 | /// Creates An SCNBox With Either A Colour Or UIImage For Each Of It's Faces
56 | /// (Either An Array [Colour] Or [UIImage] Must Be Input)
57 | /// - Parameters:
58 | /// - width: Optional CGFloat (Defaults To 20cm)
59 | /// - height: Optional CGFloat (Defaults To 20cm)
60 | /// - length: Optional CGFloat (Defaults To 20cm)
61 | /// - chamferRadius: Optional CGFloat (Defaults To 0cm)
62 | /// - colours: Optional [UIColor] - [Front, Right, Back, Left, Top, Bottom]
63 | /// - images: Optional [UIImage] - [Front, Right, Back, Left, Top, Bottom]
64 | init(width: CGFloat = 0.2, height: CGFloat = 0.2, length: CGFloat = 0.2, chamferRadius: CGFloat = 0, colours: [UIColor]?, images: [UIImage]?) {
65 |
66 | super.init()
67 |
68 | //1. Create The Box Geometry With Our Width, Height, Length & Chamfer Radius Parameters
69 | self.geometry = SCNBox(width: width, height: height, length: length, chamferRadius: chamferRadius)
70 |
71 | //2. Create A Temporary Array To Store Either Our UIColors Or UIImages
72 | var sideArray = [Any]()
73 |
74 | //3. If Our Color Array Is Nil Then Our Side Array Will Be Equal To Our Images Array
75 | if colours == nil{
76 | guard let imageArray = images else { return }
77 | sideArray = imageArray
78 | }else{
79 | //Our Side Array Will Be Equal To Our Colours Array
80 | guard let coloursArray = colours else { return }
81 | sideArray = coloursArray
82 | }
83 |
84 | //4. Loop Through The Number Of Faces & Create A New Material For Each
85 | for index in 0 ..< 6{
86 | let face = SCNMaterial()
87 | face.diffuse.contents = sideArray[index]
88 | //Add The Material To Our Face Array
89 | faceArray.append(face)
90 | }
91 |
92 | //5. Set The Boxes Materials From Our Face Array
93 | self.geometry?.materials = faceArray
94 |
95 | }
96 |
97 | required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Multipeer/ARCloudShare.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ARCloudShare.swift
3 | // CloudCube
4 | //
5 | // Created by Josh Robbins on 15/06/2018.
6 | // Copyright © 2018 BlackMirrorz. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import MultipeerConnectivity
11 |
12 | //--------------------------
13 | // MARK: - MCSessionDelegate
14 | //--------------------------
15 |
16 | extension ARCloudShare: MCSessionDelegate {
17 |
18 | func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { }
19 |
20 | func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
21 | receivedDataHandler(data, peerID)
22 | }
23 |
24 | func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
25 | fatalError("This Service Does Not Send Or Receive Streams")
26 | }
27 |
28 | func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
29 | fatalError("This Service Does Not Send Or Receive Resources")
30 | }
31 |
32 | func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
33 | fatalError("This Service Does Not Send Or Receive Resources")
34 | }
35 |
36 | }
37 |
38 | //---------------------------------------
39 | // MARK: - MCNearbyServiceBrowserDelegate
40 | //---------------------------------------
41 |
42 | extension ARCloudShare: MCNearbyServiceBrowserDelegate {
43 |
44 | public func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
45 |
46 | //Invite A New User To The Session
47 | browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10)
48 | }
49 |
50 | public func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { }
51 |
52 | }
53 |
54 | //------------------------------------------
55 | // MARK: - MCNearbyServiceAdvertiserDelegate
56 | //------------------------------------------
57 |
58 | extension ARCloudShare: MCNearbyServiceAdvertiserDelegate {
59 |
60 | //----------------------------------------------------------
61 | // MARK: - Allows The User To Accept The Invitation To Share
62 | //----------------------------------------------------------
63 |
64 | func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
65 |
66 | //Allow The User To Accept The Invitation & Join The Twunkl Session
67 | invitationHandler(true, self.session)
68 | }
69 |
70 | }
71 |
72 | class ARCloudShare: NSObject{
73 |
74 | static let serviceType = "arcloud-share"
75 |
76 | let myPeerID = MCPeerID(displayName: UIDevice.current.name)
77 | var session: MCSession!
78 | var serviceAdvertiser: MCNearbyServiceAdvertiser!
79 | var serviceBrowser: MCNearbyServiceBrowser!
80 | let receivedDataHandler: (Data, MCPeerID) -> Void
81 |
82 | //-----------------------
83 | // MARK: - Initialization
84 | //-----------------------
85 |
86 | init(receivedDataHandler: @escaping (Data, MCPeerID) -> Void ) {
87 |
88 | self.receivedDataHandler = receivedDataHandler
89 |
90 | super.init()
91 |
92 | session = MCSession(peer: myPeerID, securityIdentity: nil, encryptionPreference: .required)
93 | session.delegate = self
94 |
95 | serviceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerID, discoveryInfo: nil, serviceType: ARCloudShare.serviceType)
96 | serviceAdvertiser.delegate = self
97 | serviceAdvertiser.startAdvertisingPeer()
98 |
99 | serviceBrowser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: ARCloudShare.serviceType)
100 | serviceBrowser.delegate = self
101 | serviceBrowser.startBrowsingForPeers()
102 | }
103 |
104 | //---------------------
105 | // MARK: - Data Sending
106 | //---------------------
107 |
108 | func sendDataToUsers(_ data: Data) {
109 | do {
110 | try session.send(data, toPeers: session.connectedPeers, with: .reliable)
111 | } catch {
112 | print("Error Sending Data To Users: \(error.localizedDescription)")
113 | }
114 | }
115 |
116 | //----------------------
117 | // MARK: - Peer Tracking
118 | //----------------------
119 |
120 | var connectedPeers: [MCPeerID] { return session.connectedPeers }
121 | }
122 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Apple Focus Square/FocusSquareSegment.swift:
--------------------------------------------------------------------------------
1 | /*
2 | See LICENSE folder for this sample’s licensing information.
3 |
4 | Abstract:
5 | Corner segments for the focus square UI.
6 | */
7 |
8 | import SceneKit
9 |
10 | extension FocusSquare {
11 |
12 | /*
13 | The focus square consists of eight segments as follows, which can be individually animated.
14 |
15 | s1 s2
16 | _ _
17 | s3 | | s4
18 |
19 | s5 | | s6
20 | - -
21 | s7 s8
22 | */
23 | enum Corner {
24 | case topLeft // s1, s3
25 | case topRight // s2, s4
26 | case bottomRight // s6, s8
27 | case bottomLeft // s5, s7
28 | }
29 |
30 | enum Alignment {
31 | case horizontal // s1, s2, s7, s8
32 | case vertical // s3, s4, s5, s6
33 | }
34 |
35 | enum Direction {
36 | case up, down, left, right
37 |
38 | var reversed: Direction {
39 | switch self {
40 | case .up: return .down
41 | case .down: return .up
42 | case .left: return .right
43 | case .right: return .left
44 | }
45 | }
46 | }
47 |
48 | class Segment: SCNNode {
49 |
50 | // MARK: - Configuration & Initialization
51 |
52 | /// Thickness of the focus square lines in m.
53 | static let thickness: CGFloat = 0.018
54 |
55 | /// Length of the focus square lines in m.
56 | static let length: CGFloat = 0.5 // segment length
57 |
58 | /// Side length of the focus square segments when it is open (w.r.t. to a 1x1 square).
59 | static let openLength: CGFloat = 0.2
60 |
61 | let corner: Corner
62 | let alignment: Alignment
63 | let plane: SCNPlane
64 |
65 | init(name: String, corner: Corner, alignment: Alignment) {
66 | self.corner = corner
67 | self.alignment = alignment
68 |
69 | switch alignment {
70 | case .vertical:
71 | plane = SCNPlane(width: Segment.thickness, height: Segment.length)
72 | case .horizontal:
73 | plane = SCNPlane(width: Segment.length, height: Segment.thickness)
74 | }
75 | super.init()
76 | self.name = name
77 |
78 | let material = plane.firstMaterial!
79 | material.diffuse.contents = FocusSquare.primaryColor
80 | material.isDoubleSided = true
81 | material.ambient.contents = UIColor.black
82 | material.lightingModel = .constant
83 | material.emission.contents = FocusSquare.primaryColor
84 | geometry = plane
85 | }
86 |
87 | required init?(coder aDecoder: NSCoder) {
88 | fatalError("\(#function) has not been implemented")
89 | }
90 |
91 | // MARK: - Animating Open/Closed
92 |
93 | var openDirection: Direction {
94 | switch (corner, alignment) {
95 | case (.topLeft, .horizontal): return .left
96 | case (.topLeft, .vertical): return .up
97 | case (.topRight, .horizontal): return .right
98 | case (.topRight, .vertical): return .up
99 | case (.bottomLeft, .horizontal): return .left
100 | case (.bottomLeft, .vertical): return .down
101 | case (.bottomRight, .horizontal): return .right
102 | case (.bottomRight, .vertical): return .down
103 | }
104 | }
105 |
106 | func open() {
107 | if alignment == .horizontal {
108 | plane.width = Segment.openLength
109 | } else {
110 | plane.height = Segment.openLength
111 | }
112 |
113 | let offset = Segment.length / 2 - Segment.openLength / 2
114 | updatePosition(withOffset: Float(offset), for: openDirection)
115 | }
116 |
117 | func close() {
118 | let oldLength: CGFloat
119 | if alignment == .horizontal {
120 | oldLength = plane.width
121 | plane.width = Segment.length
122 | } else {
123 | oldLength = plane.height
124 | plane.height = Segment.length
125 | }
126 |
127 | let offset = Segment.length / 2 - oldLength / 2
128 | updatePosition(withOffset: Float(offset), for: openDirection.reversed)
129 | }
130 |
131 | private func updatePosition(withOffset offset: Float, for direction: Direction) {
132 | switch direction {
133 | case .left: position.x -= offset
134 | case .right: position.x += offset
135 | case .up: position.y -= offset
136 | case .down: position.y += offset
137 | }
138 | }
139 |
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/AR Settings/ARSettings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ARSettings.swift
3 | // CloudCube
4 | //
5 | // Created by Josh Robbins on 15/06/2018.
6 | // Copyright © 2018 BlackMirrorz. All rights reserved.
7 | //
8 |
9 |
10 | import Foundation
11 | import UIKit
12 | import ARKit
13 |
14 | //----------------------------------------------------
15 | //MARK: VIewController Extensions For Setting Up ARKit
16 | //----------------------------------------------------
17 |
18 | extension UIViewController{
19 |
20 | /// The Type Of Plane Detection Needed During The ARSession
21 | ///
22 | /// - Horizontal: Horizontal Plane Detection
23 | /// - Vertical: Vertical Plane Detection
24 | /// - Both: Horizontal & Vertical Plane Detection
25 | /// - None: No Plane Detection
26 | enum ARPlaneDetection {
27 |
28 | case Horizontal, Vertical, Both, None
29 | }
30 |
31 | /// The Type Of Debug Options Needed During The ARSession
32 | ///
33 | /// - FeaturePoints: Show Feature Points
34 | /// - WorldOrigin: Show The World Origin
35 | /// - Both: Show Both Feature Points & The World Origin
36 | /// - **None**: Show None (Development Build)
37 | enum ARDebugOptions{
38 |
39 | case FeaturePoints, WorldOrigin, Both, None
40 | }
41 |
42 | /// The Type Of ARConfiguration Needed
43 | ///
44 | /// - ResetTracking: Resets World Tracking
45 | /// - RemoveAnchors: Removes All Existing Session Anchors
46 | /// - **ResetAndRemove**: Resets World Tracking & Removes All Existing Anchors
47 | /// - None: No Congifuration
48 | enum ARConfigurationOptions{
49 |
50 | case ResetTracking, RemoveAnchors, ResetAndRemove, None
51 |
52 | }
53 |
54 | /// Sets The Type Of Plane Detection Needed During The Session
55 | ///
56 | /// - Parameter planeDetection: ARPlaneDetection
57 | /// - Returns: ARWorldTrackingConfiguration.PlaneDetection
58 | func planeDetection(_ planeDetection: ARPlaneDetection) -> ARWorldTrackingConfiguration.PlaneDetection{
59 |
60 | switch planeDetection {
61 |
62 | case .Horizontal:
63 | return [.horizontal]
64 | case .Vertical:
65 | return [.vertical]
66 | case .Both:
67 | return [.horizontal, .vertical]
68 | case .None:
69 | return []
70 |
71 | }
72 | }
73 |
74 | /// Sets The ARSession Debug Options
75 | ///
76 | /// - Parameter options: ARDebugOptions
77 | /// - Returns: SCNDebugOptions
78 | func debug(_ options: ARDebugOptions) -> SCNDebugOptions{
79 |
80 | switch options {
81 |
82 | case .FeaturePoints:
83 | return [ARSCNDebugOptions.showFeaturePoints]
84 | case .WorldOrigin:
85 | return [ARSCNDebugOptions.showWorldOrigin]
86 | case .Both:
87 | return [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin]
88 | case .None:
89 | return []
90 | }
91 | }
92 |
93 | /// Sets The ARSession Run Options
94 | ///
95 | /// - Parameter configuration: ARConfigurationOptions
96 | /// - Returns: ARSession.RunOptions
97 | func runOptions(_ configuration: ARConfigurationOptions) -> ARSession.RunOptions{
98 |
99 | switch configuration {
100 |
101 | case .ResetTracking:
102 | return [.resetTracking]
103 | case .RemoveAnchors:
104 | return [.removeExistingAnchors]
105 | case .ResetAndRemove:
106 | return [.resetTracking, .removeExistingAnchors ]
107 | case .None:
108 | return []
109 | }
110 | }
111 | }
112 |
113 | //------------------------------------------------
114 | //MARK: ARSession Extension To Log Tracking States
115 | //------------------------------------------------
116 |
117 | extension ARCamera.TrackingState: CustomStringConvertible{
118 |
119 | public var description: String {
120 |
121 | switch self {
122 |
123 | case .notAvailable: return "Tracking Unavailable"
124 | case .limited(.excessiveMotion): return "Please Slow Your Movement"
125 | case .limited(.insufficientFeatures): return "Try To Point At A Flat Surface"
126 | case .limited(.initializing): return "Initializing"
127 | case .limited(.relocalizing): return "Relocalizing"
128 | case .normal: return ""
129 | }
130 | }
131 | }
132 |
133 | //-------------------------------
134 | //MARK ARFrame WorldMappingStatus
135 | //-------------------------------
136 |
137 | extension ARFrame.WorldMappingStatus: CustomStringConvertible{
138 |
139 | public var description: String {
140 | switch self {
141 | case .notAvailable:
142 | return "World Mapping Not Available"
143 | case .limited:
144 | return "World Mapping Is Limited"
145 | case .extending:
146 | return "World Mapping Is Extending"
147 | case .mapped:
148 | return "World Is Succesfully Mapped"
149 | }
150 | }
151 | }
152 |
153 | //--------------------------------------------
154 | //MARK: ARSCNView Extension For Lighting Setup
155 | //--------------------------------------------
156 |
157 | extension ARSCNView{
158 |
159 | /// Applies Auto Lighting Of The ARSCNView
160 | func applyLighting() {
161 | self.autoenablesDefaultLighting = true
162 | self.automaticallyUpdatesLighting = true
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/AR ViewControllers/ViewController+Interactions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController+Interactions.swift
3 | // CloudCube
4 | //
5 | // Created by Josh Robbins on 16/06/2018.
6 | // Copyright © 2018 BlackMirrorz. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ARKit
11 |
12 | extension ViewController{
13 |
14 | //-----------------------------------
15 | // MARK: - World Map & Anchor Sharing
16 | //-----------------------------------
17 |
18 | /// Shares An ARWorldMap With Any Connected Users
19 | @IBAction func shareWorldMap(){
20 |
21 | //1. Attempt To Get The World Map From Our ARSession
22 | augmentedRealitySession.getCurrentWorldMap { worldMap, error in
23 |
24 | guard let mapToShare = worldMap else { print("Error: \(error!.localizedDescription)"); return }
25 |
26 | //2. We Have A Valid ARWorldMap So Send It To Any Peers
27 | guard let data = try? NSKeyedArchiver.archivedData(withRootObject: mapToShare, requiringSecureCoding: true) else { fatalError("Can't Encode Map") }
28 | self.cloudSession.sendDataToUsers(data)
29 | }
30 | }
31 |
32 | //-------------------------
33 | // MARK: - Anchor Placement
34 | //-------------------------
35 |
36 | /// Allows The User To Create An ARAnchor
37 | ///
38 | /// - Parameter gesture: UITapGestureRecognizer
39 | @IBAction func placeAnchor(_ gesture: UITapGestureRecognizer){
40 |
41 | //1. Get The Current Touch Location
42 | let currentTouchLocation = gesture.location(in: self.augmentedRealityView)
43 |
44 | //2. Perform An ARSCNHitTest For Any Existing Or Perceived Horizontal Planes
45 | guard let hitTest = self.augmentedRealityView.hitTest(currentTouchLocation, types: [.existingPlaneUsingGeometry, .estimatedHorizontalPlane]).first else { return }
46 |
47 | //3. Create Our Anchor & Add It To The Scene
48 | let validAnchor = ARAnchor(name: CLOUD_ANCHOR_ID, transform: hitTest.worldTransform)
49 | self.augmentedRealitySession.add(anchor: validAnchor)
50 |
51 | //4. Share The Angle, Rotation & Scale With Peers
52 | guard let anchorData = try? NSKeyedArchiver.archivedData(withRootObject: validAnchor, requiringSecureCoding: true) else { fatalError("Can't Encode Anchor") }
53 | self.cloudSession.sendDataToUsers(anchorData)
54 |
55 | }
56 |
57 | //---------------------------------
58 | // MARK: - Model Scaling & Rotation
59 | //---------------------------------
60 |
61 | /// Rotates The Model On It's YAxis
62 | ///
63 | /// - Parameter gesture: UIPanGestureRecognizer
64 | @objc func rotateModel(_ gesture: UIPanGestureRecognizer) {
65 |
66 | guard let modelNode = modelNode else { return }
67 |
68 | let translation = gesture.translation(in: gesture.view!)
69 | var newAngleY = (Float)(translation.x)*(Float)(Double.pi)/180.0
70 | newAngleY += currentAngleY
71 |
72 | modelNode.eulerAngles.y = newAngleY
73 |
74 | if(gesture.state == .ended) { currentAngleY = newAngleY }
75 |
76 | //2. Send It To Any Connected Users
77 | if let data = try? NSKeyedArchiver.archivedData(withRootObject: newAngleY, requiringSecureCoding: true){
78 |
79 | cloudSession.sendDataToUsers(data)
80 | }
81 |
82 | }
83 |
84 | /// Scales The Model
85 | ///
86 | /// - Parameter gesture: UIPinchGestureRecognizer
87 | @objc func scaleModel(_ gesture: UIPinchGestureRecognizer) {
88 |
89 | guard let nodeToScale = modelNode else { return }
90 |
91 | if gesture.state == .changed {
92 |
93 | let pinchScaleX: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.x))
94 | let pinchScaleY: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.y))
95 | let pinchScaleZ: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.z))
96 | let scaleVector = SCNVector3(Float(pinchScaleX), Float(pinchScaleY), Float(pinchScaleZ))
97 | nodeToScale.scale = scaleVector
98 |
99 | //2. Send It To Any Connected Users
100 | if let data = try? NSKeyedArchiver.archivedData(withRootObject: scaleVector, requiringSecureCoding: true){
101 |
102 | cloudSession.sendDataToUsers(data)
103 | }
104 |
105 | gesture.scale = 1
106 |
107 | }
108 | if gesture.state == .ended { }
109 |
110 | }
111 |
112 | //-----------------------
113 | // MARK: - Cube Colouring
114 | //-----------------------
115 |
116 | /// Sets The Current Face Of The Cube
117 | ///
118 | /// - Parameter gesture: UITapGestureRecognizer
119 | @IBAction func selectCubeFace(_ gesture: UITapGestureRecognizer){
120 |
121 | //1. Get The Current Touch Location
122 | let currentTouchLocation = gesture.location(in: self.augmentedRealityView)
123 |
124 | //2. Perform An SCNHitTest
125 | guard let hitTest = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first else { return }
126 |
127 | if let index = BoxFaces(rawValue: hitTest.geometryIndex){
128 |
129 | print("User Has Hit \(index)")
130 |
131 | //2. Stores The Face Index
132 | faceIndex = hitTest.geometryIndex
133 |
134 | //3. Sets The Face Colour
135 | setFaceColourFromGeometryIndex(hitTest.geometryIndex)
136 |
137 | }
138 | }
139 |
140 | /// Replaces The SCNMaterial For The FaceIndex Of The Cube
141 | ///
142 | /// - Parameter index: Int
143 | func setFaceColourFromGeometryIndex(_ index: Int){
144 |
145 | if let validModel = modelNode, let validColour = colourToUse{
146 |
147 | let material = SCNMaterial()
148 | material.diffuse.contents = validColour
149 | validModel.geometry?.replaceMaterial(at: index, with: material)
150 | sendColourData()
151 | colourToUse = nil
152 | faceIndex = nil
153 | }
154 | }
155 |
156 | /// Sets The Face Colour Of The Cube
157 | ///
158 | /// - Parameter sender: UIButton
159 | @objc func setFaceColour(_ sender: UIButton){
160 |
161 | colourToUse = UIColor(cgColor: sender.layer.borderColor!)
162 |
163 | if let validIndex = faceIndex { setFaceColourFromGeometryIndex(validIndex) }
164 | }
165 |
166 | /// Sends The Colour Data
167 | func sendColourData(){
168 |
169 | var colourData = [Int: UIColor]()
170 | guard let validFaceIndex = faceIndex, let validColour = colourToUse else { return }
171 | colourData[validFaceIndex] = validColour
172 |
173 | if let data = try? NSKeyedArchiver.archivedData(withRootObject: colourData, requiringSecureCoding: true){
174 | cloudSession.sendDataToUsers(data)
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/AR ViewControllers/ARDelegation+Updates.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ARDelegation+Updates.swift
3 | // CloudCube
4 | //
5 | // Created by Josh Robbins on 16/06/2018.
6 | // Copyright © 2018 BlackMirrorz. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ARKit
11 |
12 | //--------------------------
13 | // MARK: - ARSessionDelegate
14 | //--------------------------
15 |
16 | extension ViewController: ARSessionDelegate{
17 |
18 | func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
19 |
20 | DispatchQueue.main.async { self.updateFocusSquare() }
21 | }
22 |
23 | func session(_ session: ARSession, didUpdate frame: ARFrame) {
24 |
25 | switch frame.worldMappingStatus {
26 | case .notAvailable, .limited:
27 | shareButton.isUserInteractionEnabled = false
28 | case .extending:
29 | shareButton.isUserInteractionEnabled = !cloudSession.connectedPeers.isEmpty
30 | case .mapped:
31 | shareButton.isUserInteractionEnabled = !cloudSession.connectedPeers.isEmpty
32 | }
33 |
34 | if canShowControls{
35 | mappingStatusLabel.text = frame.worldMappingStatus.description
36 | }
37 |
38 | updateUserSessionInformationFor(frame, trackingState: frame.camera.trackingState)
39 | }
40 |
41 | func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) {
42 |
43 | //1. Update The Seesion Information
44 | updateUserSessionInformationFor(session.currentFrame!, trackingState: camera.trackingState)
45 | }
46 |
47 | func sessionWasInterrupted(_ session: ARSession) { statusLabel.text = "Session Was Interrupted" }
48 |
49 | func sessionInterruptionEnded(_ session: ARSession) { statusLabel.text = "Session Resumed" }
50 |
51 | func session(_ session: ARSession, didFailWithError error: Error) {
52 |
53 | statusLabel.text = "AR Session Failed"
54 | resetSession()
55 | }
56 |
57 | func sessionShouldAttemptRelocalization(_ session: ARSession) -> Bool { return true }
58 | }
59 |
60 | //--------------------------
61 | // MARK: - ARSCNViewDelegate
62 | //--------------------------
63 |
64 | extension ViewController: ARSCNViewDelegate{
65 |
66 | func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
67 |
68 | //1. If Our ARAnchor Matches Our Anchor Name & We Havent Placed Our Cube Then Display It
69 | if anchor.name == CLOUD_ANCHOR_ID{
70 |
71 | if !modelExists{
72 | modelNode = Box(colours: [.red, .green, .blue, .purple, .orange, .cyan], images: nil)
73 | canDisplayFocusSquare = false
74 | node.addChildNode(modelNode!)
75 | modelExists = true
76 |
77 | }
78 |
79 | }
80 | }
81 | }
82 |
83 | //----------------------------
84 | // MARK: - User Status Updates
85 | //----------------------------
86 |
87 | extension ViewController{
88 |
89 | /// Updates The User Regarding The Tracking State Of The ARSession & Whether They Are Connected To Any Peers
90 | ///
91 | /// - Parameters:
92 | /// - frame: ARFrame
93 | /// - trackingState: ARCamera.TrackingState
94 | func updateUserSessionInformationFor(_ frame: ARFrame, trackingState: ARCamera.TrackingState){
95 |
96 | let displayMessage: String
97 |
98 | //1. Adjust The Information Based On The Tracking State
99 | switch trackingState {
100 | case .normal where frame.anchors.isEmpty && cloudSession.connectedPeers.isEmpty:
101 |
102 | displayMessage = "Please Move Around To Map The Environment Or Wait To Join A Shared Session."
103 |
104 | case .normal where !cloudSession.connectedPeers.isEmpty && mapProvider == nil:
105 | let peerNames = cloudSession.connectedPeers.map({ $0.displayName }).joined(separator: ", ")
106 | displayMessage = "You Are Connected With \(peerNames)."
107 |
108 | case .notAvailable:
109 | displayMessage = "Tracking Unavailable"
110 |
111 | case .limited(.excessiveMotion):
112 | displayMessage = "Please Slow Your Movement"
113 |
114 | case .limited(.insufficientFeatures):
115 | displayMessage = "Try To Point At A Flat Surface"
116 |
117 | case .limited(.initializing) where mapProvider != nil,
118 | .limited(.relocalizing) where mapProvider != nil:
119 | displayMessage = "Receiving ARMap From \(mapProvider!.displayName)."
120 |
121 | case .limited(.relocalizing):
122 | displayMessage = "Resuming ARSession"
123 |
124 | case .limited(.initializing):
125 | displayMessage = "Initializing"
126 |
127 | default:
128 |
129 | displayMessage = ""
130 |
131 | }
132 |
133 | if canShowControls{
134 | //2. Update The Display Message Or Hide If Neccessary
135 | statusLabel.text = displayMessage
136 | statusLabel.isHidden = displayMessage.isEmpty
137 | }
138 |
139 | }
140 |
141 | }
142 |
143 | //------------------
144 | //MARK: Focus Square
145 | //------------------
146 |
147 | extension ViewController{
148 |
149 | func updateFocusSquare() {
150 |
151 | //1. If Our Model Has Been Placed Hide The Focus Square
152 | if !canDisplayFocusSquare {
153 | focusSquare.hide()
154 | } else {
155 | focusSquare.unhide()
156 | }
157 |
158 | if let camera = self.augmentedRealitySession.currentFrame?.camera,
159 | case .normal = camera.trackingState,
160 | let result = self.augmentedRealityView.smartHitTest(screenCenter) {
161 | updateQueue.async {
162 |
163 | if self.canDisplayFocusSquare{
164 | self.augmentedRealityView.scene.rootNode.addChildNode(self.focusSquare)
165 | self.focusSquare.state = .detecting(hitTestResult: result, camera: camera)
166 |
167 |
168 | //2. Only Allow Placement Of Our Cube If We Have Detected An ARPlaneAnchor
169 | if !self.focusSquare.isOpen{
170 | self.planeDetected = true
171 | self.placementGesture.isEnabled = true
172 | }else{
173 | self.planeDetected = false
174 | self.placementGesture.isEnabled = false
175 | }
176 | }
177 | }
178 |
179 | } else {
180 |
181 | updateQueue.async {
182 |
183 | if self.canDisplayFocusSquare{
184 | self.focusSquare.state = .initializing
185 | self.augmentedRealityView.pointOfView?.addChildNode(self.focusSquare)
186 | }
187 | }
188 | }
189 | }
190 | }
191 |
192 |
193 |
194 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/AR ViewControllers/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // CloudCube
4 | //
5 | // Created by Josh Robbins on 15/06/2018.
6 | // Copyright © 2018 BlackMirrorz. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import ARKit
11 | import MultipeerConnectivity
12 |
13 | class ViewController: UIViewController {
14 |
15 | @IBOutlet weak var instructionsView: UIView!
16 | var canShowControls = false
17 |
18 | //--------------------
19 | //MARK: - AR Variables
20 | //--------------------
21 |
22 | @IBOutlet var augmentedRealityView: ARSCNView!
23 | let augmentedRealitySession = ARSession()
24 | var configuration = ARWorldTrackingConfiguration()
25 | @IBOutlet weak var statusLabel: UILabel!
26 |
27 | //--------------------------
28 | //MARK: - Apple Focus Square
29 | //--------------------------
30 |
31 | var focusSquare = FocusSquare()
32 | var canDisplayFocusSquare = true
33 | var screenCenter: CGPoint {
34 | let bounds = self.augmentedRealityView.bounds
35 | return CGPoint(x: bounds.midX, y: bounds.midY)
36 | }
37 | let updateQueue = DispatchQueue(label: "blackMirroz")
38 |
39 | //--------------
40 | // MARK: - Model
41 | //--------------
42 | var currentAngleY: Float = 0.0
43 | var modelNode: SCNNode?
44 | var placementGesture: UITapGestureRecognizer!
45 | var planeDetected = false
46 | var modelExists = false
47 | var colourToUse: UIColor?
48 | var faceIndex: Int?
49 |
50 | @IBOutlet var colourButtonHolder: UIVisualEffectView!
51 | @IBOutlet var colourButtons: [UIButton]!
52 |
53 |
54 | //------------------
55 | // MARK: - Multipeer
56 | //------------------
57 |
58 | var mapProvider: MCPeerID?
59 | var cloudSession: ARCloudShare!
60 | let CLOUD_ANCHOR_ID = "blackMirrorzAnchor"
61 | @IBOutlet weak var mappingStatusLabel: UILabel!
62 | @IBOutlet weak var shareButton: UIImageView!
63 | @IBOutlet weak var resetButton: UIButton!
64 |
65 |
66 | //------------------------
67 | // MARK: - View Life Cycle
68 | //------------------------
69 |
70 | override func viewDidLoad() {
71 | super.viewDidLoad()
72 |
73 | //1. Initialize The Sharing Session
74 | cloudSession = ARCloudShare(receivedDataHandler: receivedData)
75 |
76 | //2. Setup Our Placement Gesture Recognizet
77 | setupGestures()
78 |
79 | //3. Add The Colour Changing Function To Our Colour Buttons
80 | colourButtons.forEach { $0.addTarget(self, action: #selector(setFaceColour(_:)), for: .touchUpInside) }
81 |
82 | //4. Show The Instructions View
83 | showInstructionsView()
84 | }
85 |
86 | override func viewDidAppear(_ animated: Bool) { setupARSession() }
87 |
88 | override func viewDidLayoutSubviews() {
89 |
90 | self.view.addSubview(instructionsView)
91 | instructionsView.center = self.view.center
92 | instructionsView.backgroundColor = .clear
93 |
94 | }
95 |
96 | //--------------------------
97 | // MARK: - User Instructions
98 | //--------------------------
99 |
100 | /// Shows The Initial Instructions
101 | func showInstructionsView(){
102 |
103 | canShowControls = false
104 | canDisplayFocusSquare = false
105 | colourButtonHolder.alpha = 0
106 | resetButton.alpha = 0
107 | shareButton.alpha = 0
108 |
109 | UIView.animate(withDuration: 12, animations: { self.instructionsView.alpha = 0 }) { (instructionsHidden) in
110 |
111 | UIView.animate(withDuration: 1, animations: {
112 |
113 | self.colourButtonHolder.alpha = 1
114 | self.resetButton.alpha = 1
115 | self.shareButton.alpha = 1
116 |
117 | }, completion: { (controlsShown) in
118 | self.canShowControls = true
119 | self.canDisplayFocusSquare = true
120 | })
121 | }
122 | }
123 |
124 | //----------------------
125 | // MARK: - Data Handling
126 | //----------------------
127 |
128 | /// Handles The Data Received From Our ARMultipeer Session
129 | ///
130 | /// - Parameters:
131 | /// - data: Data
132 | /// - peer: MCPeerID
133 | func receivedData(_ data: Data, from peer: MCPeerID) {
134 |
135 | //1. Try To UnArchive Our Data As An ARWorldMap
136 | if let unarchivedMap = try? NSKeyedUnarchiver.unarchivedObject(of: ARWorldMap.classForKeyedUnarchiver(), from: data),
137 | let worldMap = unarchivedMap as? ARWorldMap {
138 |
139 | //2. Now A Map Is Available Restart The Session
140 | let configuration = ARWorldTrackingConfiguration()
141 | configuration.planeDetection = .horizontal
142 | configuration.initialWorldMap = worldMap
143 | self.augmentedRealityView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
144 |
145 | mapProvider = peer
146 |
147 | }
148 | //2. Try To Unarchive Our Data As An ARAnchor
149 | else if let unarchivedAnchor = try? NSKeyedUnarchiver.unarchivedObject(of: ARAnchor.classForKeyedUnarchiver(), from: data),
150 | let anchor = unarchivedAnchor as? ARAnchor {
151 |
152 | augmentedRealitySession.add(anchor: anchor)
153 | }
154 |
155 | //3. Try To Unarchive Our Data To Adjust The Scale & Or Rotation Of Our Model
156 | else if let unarchivedData = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data){
157 |
158 | if unarchivedData is Float, let unarchivedRotationData = unarchivedData as? Float, let model = modelNode {
159 |
160 | model.eulerAngles.y = unarchivedRotationData
161 |
162 | }else if unarchivedData is SCNVector3, let unarchivedScaleData = unarchivedData as? SCNVector3, let model = modelNode {
163 |
164 | model.scale = unarchivedScaleData
165 |
166 | }else if unarchivedData is [Int: UIColor], let colourDictionary = unarchivedData as? [Int: UIColor], let _ = modelNode{
167 |
168 | guard let index = colourDictionary.keys.first, let colour = colourDictionary[index] else { return }
169 | colourToUse = colour
170 | setFaceColourFromGeometryIndex(index)
171 | colourToUse = nil
172 | }
173 | else {
174 | print("Unknown Data Recieved From = \(peer)")
175 |
176 | }
177 | }
178 |
179 | }
180 |
181 | //----------------------------
182 | // MARK: - Gesture Recongizers
183 | //----------------------------
184 |
185 | /// Adds The Placement Gesture To Our ARSession
186 | func setupGestures(){
187 |
188 | //1. Setup The Placement Gesture Recognizer & Disable It Until An ARPlaneAnchor Has Been Detected
189 | placementGesture = UITapGestureRecognizer(target: self, action: #selector(placeAnchor(_:)))
190 | self.view.addGestureRecognizer(placementGesture)
191 | placementGesture.isEnabled = false
192 |
193 | //2. Add A Tap Gesture To Our ImageView To Enable Sharing
194 | let tapToShareGesture = UITapGestureRecognizer(target: self, action: #selector(shareWorldMap))
195 | shareButton.addGestureRecognizer(tapToShareGesture)
196 |
197 | //3. Add A Rotation Gesture So We Can Rotate Our Model
198 | let rotationGesture = UIPanGestureRecognizer(target: self, action: #selector(rotateModel(_:)))
199 | self.view.addGestureRecognizer(rotationGesture)
200 |
201 | //3. Add A Scale Gesture So We Can Scale Our Model
202 | let scaleGesture = UIPinchGestureRecognizer(target: self, action: #selector(scaleModel(_:)))
203 | self.view.addGestureRecognizer(scaleGesture)
204 |
205 | //4. Add A Double Tap Gesture To Colourizer The Face Our Cube
206 | let tapToColourize = UITapGestureRecognizer(target: self, action: #selector(selectCubeFace(_:)))
207 | tapToColourize.numberOfTapsRequired = 2
208 | self.view.addGestureRecognizer(tapToColourize)
209 | }
210 |
211 | //-----------------------
212 | //MARK: - ARSetup & Reset
213 | //-----------------------
214 |
215 | /// Runs The ARSession
216 | func setupARSession() {
217 |
218 | augmentedRealityView.session = augmentedRealitySession
219 | configuration.planeDetection = planeDetection(.Horizontal)
220 | configuration.environmentTexturing = .automatic
221 |
222 | augmentedRealityView.debugOptions = debug(.None)
223 | augmentedRealitySession.run(configuration, options: runOptions(.ResetAndRemove))
224 |
225 | augmentedRealityView.delegate = self
226 | augmentedRealitySession.delegate = self
227 |
228 | UIApplication.shared.isIdleTimerDisabled = true
229 | }
230 |
231 | /// Resets The ARSession
232 | @IBAction func resetSession(){
233 |
234 | canDisplayFocusSquare = true
235 | planeDetected = false
236 | placementGesture.isEnabled = false
237 | modelExists = false
238 | modelNode = nil
239 | colourToUse = nil
240 | faceIndex = nil
241 |
242 | let configuration = ARWorldTrackingConfiguration()
243 | configuration.planeDetection = planeDetection(.Horizontal)
244 | augmentedRealitySession.run(configuration, options: runOptions(.ResetAndRemove))
245 | }
246 |
247 |
248 | }
249 |
250 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | AF08729320D3EE7300C800E3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF08729220D3EE7300C800E3 /* AppDelegate.swift */; };
11 | AF08729520D3EE7300C800E3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF08729420D3EE7300C800E3 /* ViewController.swift */; };
12 | AF08729820D3EE7300C800E3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AF08729620D3EE7300C800E3 /* Main.storyboard */; };
13 | AF08729A20D3EE7400C800E3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AF08729920D3EE7400C800E3 /* Assets.xcassets */; };
14 | AF08729D20D3EE7400C800E3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AF08729B20D3EE7400C800E3 /* LaunchScreen.storyboard */; };
15 | AF0872A520D3EEAD00C800E3 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0872A420D3EEAD00C800E3 /* Box.swift */; };
16 | AF0872A720D3EEEC00C800E3 /* ARSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0872A620D3EEEC00C800E3 /* ARSettings.swift */; };
17 | AF41F91020D48AC200AB0012 /* ARCloudShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F90F20D48AC200AB0012 /* ARCloudShare.swift */; };
18 | AF41F91720D48B7800AB0012 /* FocusSquareSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F91220D48B7800AB0012 /* FocusSquareSegment.swift */; };
19 | AF41F91820D48B7800AB0012 /* FocusSquare+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F91320D48B7800AB0012 /* FocusSquare+Extensions.swift */; };
20 | AF41F91920D48B7800AB0012 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F91520D48B7800AB0012 /* Utilities.swift */; };
21 | AF41F91A20D48B7800AB0012 /* FocusSquare.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F91620D48B7800AB0012 /* FocusSquare.swift */; };
22 | AF41F91C20D4909600AB0012 /* ARDelegation+Updates.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F91B20D4909600AB0012 /* ARDelegation+Updates.swift */; };
23 | AF41F91E20D4936B00AB0012 /* BlackMirrorzViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F91D20D4936B00AB0012 /* BlackMirrorzViews.swift */; };
24 | AF41F92220D4B32800AB0012 /* ViewController+Interactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF41F92120D4B32800AB0012 /* ViewController+Interactions.swift */; };
25 | /* End PBXBuildFile section */
26 |
27 | /* Begin PBXFileReference section */
28 | AF08728F20D3EE7300C800E3 /* CloudCube.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CloudCube.app; sourceTree = BUILT_PRODUCTS_DIR; };
29 | AF08729220D3EE7300C800E3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
30 | AF08729420D3EE7300C800E3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
31 | AF08729720D3EE7300C800E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
32 | AF08729920D3EE7400C800E3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
33 | AF08729C20D3EE7400C800E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
34 | AF08729E20D3EE7400C800E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
35 | AF0872A420D3EEAD00C800E3 /* Box.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = ""; };
36 | AF0872A620D3EEEC00C800E3 /* ARSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARSettings.swift; sourceTree = ""; };
37 | AF41F90F20D48AC200AB0012 /* ARCloudShare.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARCloudShare.swift; sourceTree = ""; };
38 | AF41F91220D48B7800AB0012 /* FocusSquareSegment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusSquareSegment.swift; sourceTree = ""; };
39 | AF41F91320D48B7800AB0012 /* FocusSquare+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FocusSquare+Extensions.swift"; sourceTree = ""; };
40 | AF41F91520D48B7800AB0012 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; };
41 | AF41F91620D48B7800AB0012 /* FocusSquare.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusSquare.swift; sourceTree = ""; };
42 | AF41F91B20D4909600AB0012 /* ARDelegation+Updates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ARDelegation+Updates.swift"; sourceTree = ""; };
43 | AF41F91D20D4936B00AB0012 /* BlackMirrorzViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlackMirrorzViews.swift; sourceTree = ""; };
44 | AF41F92120D4B32800AB0012 /* ViewController+Interactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+Interactions.swift"; sourceTree = ""; };
45 | /* End PBXFileReference section */
46 |
47 | /* Begin PBXFrameworksBuildPhase section */
48 | AF08728C20D3EE7300C800E3 /* Frameworks */ = {
49 | isa = PBXFrameworksBuildPhase;
50 | buildActionMask = 2147483647;
51 | files = (
52 | );
53 | runOnlyForDeploymentPostprocessing = 0;
54 | };
55 | /* End PBXFrameworksBuildPhase section */
56 |
57 | /* Begin PBXGroup section */
58 | AF08728620D3EE7300C800E3 = {
59 | isa = PBXGroup;
60 | children = (
61 | AF08729120D3EE7300C800E3 /* CloudCube */,
62 | AF08729020D3EE7300C800E3 /* Products */,
63 | );
64 | sourceTree = "";
65 | };
66 | AF08729020D3EE7300C800E3 /* Products */ = {
67 | isa = PBXGroup;
68 | children = (
69 | AF08728F20D3EE7300C800E3 /* CloudCube.app */,
70 | );
71 | name = Products;
72 | sourceTree = "";
73 | };
74 | AF08729120D3EE7300C800E3 /* CloudCube */ = {
75 | isa = PBXGroup;
76 | children = (
77 | AF41F91120D48B7800AB0012 /* Apple Focus Square */,
78 | AF41F92620D4C1E400AB0012 /* Multipeer */,
79 | AF41F92320D4C1A200AB0012 /* Custom Views */,
80 | AF41F92520D4C1BB00AB0012 /* Custom Nodes */,
81 | AF41F92720D4C1FE00AB0012 /* AR Settings */,
82 | AF41F92420D4C1AF00AB0012 /* AR ViewControllers */,
83 | AF08729220D3EE7300C800E3 /* AppDelegate.swift */,
84 | AF08729620D3EE7300C800E3 /* Main.storyboard */,
85 | AF08729920D3EE7400C800E3 /* Assets.xcassets */,
86 | AF08729B20D3EE7400C800E3 /* LaunchScreen.storyboard */,
87 | AF08729E20D3EE7400C800E3 /* Info.plist */,
88 | );
89 | path = CloudCube;
90 | sourceTree = "";
91 | };
92 | AF41F91120D48B7800AB0012 /* Apple Focus Square */ = {
93 | isa = PBXGroup;
94 | children = (
95 | AF41F91220D48B7800AB0012 /* FocusSquareSegment.swift */,
96 | AF41F91320D48B7800AB0012 /* FocusSquare+Extensions.swift */,
97 | AF41F91420D48B7800AB0012 /* Utilities */,
98 | AF41F91620D48B7800AB0012 /* FocusSquare.swift */,
99 | );
100 | path = "Apple Focus Square";
101 | sourceTree = "";
102 | };
103 | AF41F91420D48B7800AB0012 /* Utilities */ = {
104 | isa = PBXGroup;
105 | children = (
106 | AF41F91520D48B7800AB0012 /* Utilities.swift */,
107 | );
108 | path = Utilities;
109 | sourceTree = "";
110 | };
111 | AF41F92320D4C1A200AB0012 /* Custom Views */ = {
112 | isa = PBXGroup;
113 | children = (
114 | AF41F91D20D4936B00AB0012 /* BlackMirrorzViews.swift */,
115 | );
116 | path = "Custom Views";
117 | sourceTree = "";
118 | };
119 | AF41F92420D4C1AF00AB0012 /* AR ViewControllers */ = {
120 | isa = PBXGroup;
121 | children = (
122 | AF08729420D3EE7300C800E3 /* ViewController.swift */,
123 | AF41F92120D4B32800AB0012 /* ViewController+Interactions.swift */,
124 | AF41F91B20D4909600AB0012 /* ARDelegation+Updates.swift */,
125 | );
126 | path = "AR ViewControllers";
127 | sourceTree = "";
128 | };
129 | AF41F92520D4C1BB00AB0012 /* Custom Nodes */ = {
130 | isa = PBXGroup;
131 | children = (
132 | AF0872A420D3EEAD00C800E3 /* Box.swift */,
133 | );
134 | path = "Custom Nodes";
135 | sourceTree = "";
136 | };
137 | AF41F92620D4C1E400AB0012 /* Multipeer */ = {
138 | isa = PBXGroup;
139 | children = (
140 | AF41F90F20D48AC200AB0012 /* ARCloudShare.swift */,
141 | );
142 | path = Multipeer;
143 | sourceTree = "";
144 | };
145 | AF41F92720D4C1FE00AB0012 /* AR Settings */ = {
146 | isa = PBXGroup;
147 | children = (
148 | AF0872A620D3EEEC00C800E3 /* ARSettings.swift */,
149 | );
150 | path = "AR Settings";
151 | sourceTree = "";
152 | };
153 | /* End PBXGroup section */
154 |
155 | /* Begin PBXNativeTarget section */
156 | AF08728E20D3EE7300C800E3 /* CloudCube */ = {
157 | isa = PBXNativeTarget;
158 | buildConfigurationList = AF0872A120D3EE7400C800E3 /* Build configuration list for PBXNativeTarget "CloudCube" */;
159 | buildPhases = (
160 | AF08728B20D3EE7300C800E3 /* Sources */,
161 | AF08728C20D3EE7300C800E3 /* Frameworks */,
162 | AF08728D20D3EE7300C800E3 /* Resources */,
163 | );
164 | buildRules = (
165 | );
166 | dependencies = (
167 | );
168 | name = CloudCube;
169 | productName = CloudCube;
170 | productReference = AF08728F20D3EE7300C800E3 /* CloudCube.app */;
171 | productType = "com.apple.product-type.application";
172 | };
173 | /* End PBXNativeTarget section */
174 |
175 | /* Begin PBXProject section */
176 | AF08728720D3EE7300C800E3 /* Project object */ = {
177 | isa = PBXProject;
178 | attributes = {
179 | LastSwiftUpdateCheck = 1000;
180 | LastUpgradeCheck = 1000;
181 | ORGANIZATIONNAME = BlackMirrorz;
182 | TargetAttributes = {
183 | AF08728E20D3EE7300C800E3 = {
184 | CreatedOnToolsVersion = 10.0;
185 | };
186 | };
187 | };
188 | buildConfigurationList = AF08728A20D3EE7300C800E3 /* Build configuration list for PBXProject "CloudCube" */;
189 | compatibilityVersion = "Xcode 9.3";
190 | developmentRegion = en;
191 | hasScannedForEncodings = 0;
192 | knownRegions = (
193 | en,
194 | Base,
195 | );
196 | mainGroup = AF08728620D3EE7300C800E3;
197 | productRefGroup = AF08729020D3EE7300C800E3 /* Products */;
198 | projectDirPath = "";
199 | projectRoot = "";
200 | targets = (
201 | AF08728E20D3EE7300C800E3 /* CloudCube */,
202 | );
203 | };
204 | /* End PBXProject section */
205 |
206 | /* Begin PBXResourcesBuildPhase section */
207 | AF08728D20D3EE7300C800E3 /* Resources */ = {
208 | isa = PBXResourcesBuildPhase;
209 | buildActionMask = 2147483647;
210 | files = (
211 | AF08729D20D3EE7400C800E3 /* LaunchScreen.storyboard in Resources */,
212 | AF08729A20D3EE7400C800E3 /* Assets.xcassets in Resources */,
213 | AF08729820D3EE7300C800E3 /* Main.storyboard in Resources */,
214 | );
215 | runOnlyForDeploymentPostprocessing = 0;
216 | };
217 | /* End PBXResourcesBuildPhase section */
218 |
219 | /* Begin PBXSourcesBuildPhase section */
220 | AF08728B20D3EE7300C800E3 /* Sources */ = {
221 | isa = PBXSourcesBuildPhase;
222 | buildActionMask = 2147483647;
223 | files = (
224 | AF41F91020D48AC200AB0012 /* ARCloudShare.swift in Sources */,
225 | AF41F91E20D4936B00AB0012 /* BlackMirrorzViews.swift in Sources */,
226 | AF41F91720D48B7800AB0012 /* FocusSquareSegment.swift in Sources */,
227 | AF41F92220D4B32800AB0012 /* ViewController+Interactions.swift in Sources */,
228 | AF0872A520D3EEAD00C800E3 /* Box.swift in Sources */,
229 | AF08729520D3EE7300C800E3 /* ViewController.swift in Sources */,
230 | AF41F91A20D48B7800AB0012 /* FocusSquare.swift in Sources */,
231 | AF08729320D3EE7300C800E3 /* AppDelegate.swift in Sources */,
232 | AF0872A720D3EEEC00C800E3 /* ARSettings.swift in Sources */,
233 | AF41F91920D48B7800AB0012 /* Utilities.swift in Sources */,
234 | AF41F91820D48B7800AB0012 /* FocusSquare+Extensions.swift in Sources */,
235 | AF41F91C20D4909600AB0012 /* ARDelegation+Updates.swift in Sources */,
236 | );
237 | runOnlyForDeploymentPostprocessing = 0;
238 | };
239 | /* End PBXSourcesBuildPhase section */
240 |
241 | /* Begin PBXVariantGroup section */
242 | AF08729620D3EE7300C800E3 /* Main.storyboard */ = {
243 | isa = PBXVariantGroup;
244 | children = (
245 | AF08729720D3EE7300C800E3 /* Base */,
246 | );
247 | name = Main.storyboard;
248 | sourceTree = "";
249 | };
250 | AF08729B20D3EE7400C800E3 /* LaunchScreen.storyboard */ = {
251 | isa = PBXVariantGroup;
252 | children = (
253 | AF08729C20D3EE7400C800E3 /* Base */,
254 | );
255 | name = LaunchScreen.storyboard;
256 | sourceTree = "";
257 | };
258 | /* End PBXVariantGroup section */
259 |
260 | /* Begin XCBuildConfiguration section */
261 | AF08729F20D3EE7400C800E3 /* Debug */ = {
262 | isa = XCBuildConfiguration;
263 | buildSettings = {
264 | ALWAYS_SEARCH_USER_PATHS = NO;
265 | CLANG_ANALYZER_NONNULL = YES;
266 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
267 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
268 | CLANG_CXX_LIBRARY = "libc++";
269 | CLANG_ENABLE_MODULES = YES;
270 | CLANG_ENABLE_OBJC_ARC = YES;
271 | CLANG_ENABLE_OBJC_WEAK = YES;
272 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
273 | CLANG_WARN_BOOL_CONVERSION = YES;
274 | CLANG_WARN_COMMA = YES;
275 | CLANG_WARN_CONSTANT_CONVERSION = YES;
276 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
277 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
278 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
279 | CLANG_WARN_EMPTY_BODY = YES;
280 | CLANG_WARN_ENUM_CONVERSION = YES;
281 | CLANG_WARN_INFINITE_RECURSION = YES;
282 | CLANG_WARN_INT_CONVERSION = YES;
283 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
284 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
285 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
286 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
287 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
288 | CLANG_WARN_STRICT_PROTOTYPES = YES;
289 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
290 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
291 | CLANG_WARN_UNREACHABLE_CODE = YES;
292 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
293 | CODE_SIGN_IDENTITY = "iPhone Developer";
294 | COPY_PHASE_STRIP = NO;
295 | DEBUG_INFORMATION_FORMAT = dwarf;
296 | ENABLE_STRICT_OBJC_MSGSEND = YES;
297 | ENABLE_TESTABILITY = YES;
298 | GCC_C_LANGUAGE_STANDARD = gnu11;
299 | GCC_DYNAMIC_NO_PIC = NO;
300 | GCC_NO_COMMON_BLOCKS = YES;
301 | GCC_OPTIMIZATION_LEVEL = 0;
302 | GCC_PREPROCESSOR_DEFINITIONS = (
303 | "DEBUG=1",
304 | "$(inherited)",
305 | );
306 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
307 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
308 | GCC_WARN_UNDECLARED_SELECTOR = YES;
309 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
310 | GCC_WARN_UNUSED_FUNCTION = YES;
311 | GCC_WARN_UNUSED_VARIABLE = YES;
312 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
313 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
314 | ONLY_ACTIVE_ARCH = YES;
315 | SDKROOT = iphoneos;
316 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
317 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
318 | };
319 | name = Debug;
320 | };
321 | AF0872A020D3EE7400C800E3 /* Release */ = {
322 | isa = XCBuildConfiguration;
323 | buildSettings = {
324 | ALWAYS_SEARCH_USER_PATHS = NO;
325 | CLANG_ANALYZER_NONNULL = YES;
326 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
327 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
328 | CLANG_CXX_LIBRARY = "libc++";
329 | CLANG_ENABLE_MODULES = YES;
330 | CLANG_ENABLE_OBJC_ARC = YES;
331 | CLANG_ENABLE_OBJC_WEAK = YES;
332 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
333 | CLANG_WARN_BOOL_CONVERSION = YES;
334 | CLANG_WARN_COMMA = YES;
335 | CLANG_WARN_CONSTANT_CONVERSION = YES;
336 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
337 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
338 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
339 | CLANG_WARN_EMPTY_BODY = YES;
340 | CLANG_WARN_ENUM_CONVERSION = YES;
341 | CLANG_WARN_INFINITE_RECURSION = YES;
342 | CLANG_WARN_INT_CONVERSION = YES;
343 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
344 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
345 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
346 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
347 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
348 | CLANG_WARN_STRICT_PROTOTYPES = YES;
349 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
350 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
351 | CLANG_WARN_UNREACHABLE_CODE = YES;
352 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
353 | CODE_SIGN_IDENTITY = "iPhone Developer";
354 | COPY_PHASE_STRIP = NO;
355 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
356 | ENABLE_NS_ASSERTIONS = NO;
357 | ENABLE_STRICT_OBJC_MSGSEND = YES;
358 | GCC_C_LANGUAGE_STANDARD = gnu11;
359 | GCC_NO_COMMON_BLOCKS = YES;
360 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
361 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
362 | GCC_WARN_UNDECLARED_SELECTOR = YES;
363 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
364 | GCC_WARN_UNUSED_FUNCTION = YES;
365 | GCC_WARN_UNUSED_VARIABLE = YES;
366 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
367 | MTL_ENABLE_DEBUG_INFO = NO;
368 | SDKROOT = iphoneos;
369 | SWIFT_COMPILATION_MODE = wholemodule;
370 | SWIFT_OPTIMIZATION_LEVEL = "-O";
371 | VALIDATE_PRODUCT = YES;
372 | };
373 | name = Release;
374 | };
375 | AF0872A220D3EE7400C800E3 /* Debug */ = {
376 | isa = XCBuildConfiguration;
377 | buildSettings = {
378 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
379 | CODE_SIGN_STYLE = Automatic;
380 | DEVELOPMENT_TEAM = DHKEQ8PRAX;
381 | INFOPLIST_FILE = CloudCube/Info.plist;
382 | LD_RUNPATH_SEARCH_PATHS = (
383 | "$(inherited)",
384 | "@executable_path/Frameworks",
385 | );
386 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.BlackMirrorz.CloudCube;
387 | PRODUCT_NAME = "$(TARGET_NAME)";
388 | SWIFT_VERSION = 4.2;
389 | TARGETED_DEVICE_FAMILY = "1,2";
390 | };
391 | name = Debug;
392 | };
393 | AF0872A320D3EE7400C800E3 /* Release */ = {
394 | isa = XCBuildConfiguration;
395 | buildSettings = {
396 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
397 | CODE_SIGN_STYLE = Automatic;
398 | DEVELOPMENT_TEAM = DHKEQ8PRAX;
399 | INFOPLIST_FILE = CloudCube/Info.plist;
400 | LD_RUNPATH_SEARCH_PATHS = (
401 | "$(inherited)",
402 | "@executable_path/Frameworks",
403 | );
404 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.BlackMirrorz.CloudCube;
405 | PRODUCT_NAME = "$(TARGET_NAME)";
406 | SWIFT_VERSION = 4.2;
407 | TARGETED_DEVICE_FAMILY = "1,2";
408 | };
409 | name = Release;
410 | };
411 | /* End XCBuildConfiguration section */
412 |
413 | /* Begin XCConfigurationList section */
414 | AF08728A20D3EE7300C800E3 /* Build configuration list for PBXProject "CloudCube" */ = {
415 | isa = XCConfigurationList;
416 | buildConfigurations = (
417 | AF08729F20D3EE7400C800E3 /* Debug */,
418 | AF0872A020D3EE7400C800E3 /* Release */,
419 | );
420 | defaultConfigurationIsVisible = 0;
421 | defaultConfigurationName = Release;
422 | };
423 | AF0872A120D3EE7400C800E3 /* Build configuration list for PBXNativeTarget "CloudCube" */ = {
424 | isa = XCConfigurationList;
425 | buildConfigurations = (
426 | AF0872A220D3EE7400C800E3 /* Debug */,
427 | AF0872A320D3EE7400C800E3 /* Release */,
428 | );
429 | defaultConfigurationIsVisible = 0;
430 | defaultConfigurationName = Release;
431 | };
432 | /* End XCConfigurationList section */
433 | };
434 | rootObject = AF08728720D3EE7300C800E3 /* Project object */;
435 | }
436 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Apple Focus Square/FocusSquare.swift:
--------------------------------------------------------------------------------
1 | /*
2 | See LICENSE folder for this sample’s licensing information.
3 |
4 | Abstract:
5 | SceneKit node giving the user hints about the status of ARKit world tracking.
6 | */
7 |
8 | import Foundation
9 | import ARKit
10 |
11 | /**
12 | An `SCNNode` which is used to provide uses with visual cues about the status of ARKit world tracking.
13 | - Tag: FocusSquare
14 | */
15 | class FocusSquare: SCNNode {
16 | // MARK: - Types
17 |
18 | enum State: Equatable {
19 | case initializing
20 | case detecting(hitTestResult: ARHitTestResult, camera: ARCamera?)
21 | }
22 |
23 | // MARK: - Configuration Properties
24 |
25 | // Original size of the focus square in meters.
26 | static let size: Float = 0.17
27 |
28 | // Thickness of the focus square lines in meters.
29 | static let thickness: Float = 0.018
30 |
31 | // Scale factor for the focus square when it is closed, w.r.t. the original size.
32 | static let scaleForClosedSquare: Float = 0.97
33 |
34 | // Side length of the focus square segments when it is open (w.r.t. to a 1x1 square).
35 | static let sideLengthForOpenSegments: CGFloat = 0.2
36 |
37 | // Duration of the open/close animation
38 | static let animationDuration = 0.7
39 |
40 | static let primaryColor = #colorLiteral(red: 1, green: 0.8, blue: 0, alpha: 1)
41 |
42 | // Color of the focus square fill.
43 | static let fillColor = #colorLiteral(red: 1, green: 0.9254901961, blue: 0.4117647059, alpha: 1)
44 |
45 | // MARK: - Properties
46 |
47 | /// The most recent position of the focus square based on the current state.
48 | var lastPosition: float3? {
49 | switch state {
50 | case .initializing: return nil
51 | case .detecting(let hitTestResult, _): return hitTestResult.worldTransform.translation
52 | }
53 | }
54 |
55 | var state: State = .initializing {
56 | didSet {
57 | guard state != oldValue else { return }
58 |
59 | switch state {
60 | case .initializing:
61 | displayAsBillboard()
62 |
63 | case let .detecting(hitTestResult, camera):
64 | if let planeAnchor = hitTestResult.anchor as? ARPlaneAnchor {
65 | displayAsClosed(for: hitTestResult, planeAnchor: planeAnchor, camera: camera)
66 | currentPlaneAnchor = planeAnchor
67 | } else {
68 | displayAsOpen(for: hitTestResult, camera: camera)
69 | currentPlaneAnchor = nil
70 | }
71 | }
72 | }
73 | }
74 |
75 | /// Indicates whether the segments of the focus square are disconnected.
76 | public var isOpen = false
77 |
78 | /// Indicates if the square is currently being animated.
79 | private var isAnimating = false
80 |
81 | /// Indicates if the square is currently changing its alignment.
82 | private var isChangingAlignment = false
83 |
84 | /// The focus square's current alignment.
85 | private var currentAlignment: ARPlaneAnchor.Alignment?
86 |
87 | /// The current plane anchor if the focus square is on a plane.
88 | private(set) var currentPlaneAnchor: ARPlaneAnchor?
89 |
90 | /// The focus square's most recent positions.
91 | private var recentFocusSquarePositions: [float3] = []
92 |
93 | /// The focus square's most recent alignments.
94 | private(set) var recentFocusSquareAlignments: [ARPlaneAnchor.Alignment] = []
95 |
96 | /// Previously visited plane anchors.
97 | private var anchorsOfVisitedPlanes: Set = []
98 |
99 | /// List of the segments in the focus square.
100 | private var segments: [FocusSquare.Segment] = []
101 |
102 | /// The primary node that controls the position of other `FocusSquare` nodes.
103 | private let positioningNode = SCNNode()
104 |
105 | // MARK: - Initialization
106 |
107 | override init() {
108 | super.init()
109 | opacity = 0.0
110 |
111 | /*
112 | The focus square consists of eight segments as follows, which can be individually animated.
113 |
114 | s1 s2
115 | _ _
116 | s3 | | s4
117 |
118 | s5 | | s6
119 | - -
120 | s7 s8
121 | */
122 | let s1 = Segment(name: "s1", corner: .topLeft, alignment: .horizontal)
123 | let s2 = Segment(name: "s2", corner: .topRight, alignment: .horizontal)
124 | let s3 = Segment(name: "s3", corner: .topLeft, alignment: .vertical)
125 | let s4 = Segment(name: "s4", corner: .topRight, alignment: .vertical)
126 | let s5 = Segment(name: "s5", corner: .bottomLeft, alignment: .vertical)
127 | let s6 = Segment(name: "s6", corner: .bottomRight, alignment: .vertical)
128 | let s7 = Segment(name: "s7", corner: .bottomLeft, alignment: .horizontal)
129 | let s8 = Segment(name: "s8", corner: .bottomRight, alignment: .horizontal)
130 | segments = [s1, s2, s3, s4, s5, s6, s7, s8]
131 |
132 | let sl: Float = 0.5 // segment length
133 | let c: Float = FocusSquare.thickness / 2 // correction to align lines perfectly
134 | s1.simdPosition += float3(-(sl / 2 - c), -(sl - c), 0)
135 | s2.simdPosition += float3(sl / 2 - c, -(sl - c), 0)
136 | s3.simdPosition += float3(-sl, -sl / 2, 0)
137 | s4.simdPosition += float3(sl, -sl / 2, 0)
138 | s5.simdPosition += float3(-sl, sl / 2, 0)
139 | s6.simdPosition += float3(sl, sl / 2, 0)
140 | s7.simdPosition += float3(-(sl / 2 - c), sl - c, 0)
141 | s8.simdPosition += float3(sl / 2 - c, sl - c, 0)
142 |
143 | positioningNode.eulerAngles.x = .pi / 2 // Horizontal
144 | positioningNode.simdScale = float3(FocusSquare.size * FocusSquare.scaleForClosedSquare)
145 | for segment in segments {
146 | positioningNode.addChildNode(segment)
147 | }
148 | positioningNode.addChildNode(fillPlane)
149 |
150 | // Always render focus square on top of other content.
151 | displayNodeHierarchyOnTop(true)
152 |
153 | addChildNode(positioningNode)
154 |
155 | // Start the focus square as a billboard.
156 | displayAsBillboard()
157 | }
158 |
159 | required init?(coder aDecoder: NSCoder) {
160 | fatalError("\(#function) has not been implemented")
161 | }
162 |
163 | // MARK: - Appearance
164 |
165 | /// Hides the focus square.
166 | func hide() {
167 | guard action(forKey: "hide") == nil else { return }
168 |
169 | displayNodeHierarchyOnTop(false)
170 | runAction(.fadeOut(duration: 0.5), forKey: "hide")
171 | }
172 |
173 | /// Unhides the focus square.
174 | func unhide() {
175 | guard action(forKey: "unhide") == nil else { return }
176 |
177 | displayNodeHierarchyOnTop(true)
178 | runAction(.fadeIn(duration: 0.5), forKey: "unhide")
179 | }
180 |
181 | /// Displays the focus square parallel to the camera plane.
182 | private func displayAsBillboard() {
183 | simdTransform = matrix_identity_float4x4
184 | eulerAngles.x = .pi / 2
185 | simdPosition = float3(0, 0, -0.8)
186 | unhide()
187 | performOpenAnimation()
188 | }
189 |
190 | /// Called when a surface has been detected.
191 | private func displayAsOpen(for hitTestResult: ARHitTestResult, camera: ARCamera?) {
192 | performOpenAnimation()
193 | let position = hitTestResult.worldTransform.translation
194 | recentFocusSquarePositions.append(position)
195 | updateTransform(for: position, hitTestResult: hitTestResult, camera: camera)
196 | }
197 |
198 | /// Called when a plane has been detected.
199 | private func displayAsClosed(for hitTestResult: ARHitTestResult, planeAnchor: ARPlaneAnchor, camera: ARCamera?) {
200 | performCloseAnimation(flash: !anchorsOfVisitedPlanes.contains(planeAnchor))
201 | anchorsOfVisitedPlanes.insert(planeAnchor)
202 | let position = hitTestResult.worldTransform.translation
203 | recentFocusSquarePositions.append(position)
204 | updateTransform(for: position, hitTestResult: hitTestResult, camera: camera)
205 | }
206 |
207 | // MARK: Helper Methods
208 |
209 | /// Update the transform of the focus square to be aligned with the camera.
210 | private func updateTransform(for position: float3, hitTestResult: ARHitTestResult, camera: ARCamera?) {
211 | // Average using several most recent positions.
212 | recentFocusSquarePositions = Array(recentFocusSquarePositions.suffix(10))
213 |
214 | // Move to average of recent positions to avoid jitter.
215 | let average = recentFocusSquarePositions.reduce(float3(0), { $0 + $1 }) / Float(recentFocusSquarePositions.count)
216 | self.simdPosition = average
217 | self.simdScale = float3(scaleBasedOnDistance(camera: camera))
218 |
219 | // Correct y rotation of camera square.
220 | guard let camera = camera else { return }
221 | let tilt = abs(camera.eulerAngles.x)
222 | let threshold1: Float = .pi / 2 * 0.65
223 | let threshold2: Float = .pi / 2 * 0.75
224 | let yaw = atan2f(camera.transform.columns.0.x, camera.transform.columns.1.x)
225 | var angle: Float = 0
226 |
227 | switch tilt {
228 | case 0.. 15 ||
279 | alignment == .vertical && verticalHistory > 10 ||
280 | hitTestResult.anchor is ARPlaneAnchor {
281 | if alignment != currentAlignment {
282 | shouldAnimateAlignmentChange = true
283 | currentAlignment = alignment
284 | recentFocusSquareAlignments.removeAll()
285 | }
286 | } else {
287 | // Alignment is different than most of the history - ignore it
288 | alignment = currentAlignment
289 | return
290 | }
291 |
292 | if alignment == .vertical {
293 | tempNode.simdOrientation = hitTestResult.worldTransform.orientation
294 | shouldAnimateAlignmentChange = true
295 | }
296 |
297 | // Change the focus square's alignment
298 | if shouldAnimateAlignmentChange {
299 | performAlignmentAnimation(to: tempNode.simdOrientation)
300 | } else {
301 | simdOrientation = tempNode.simdOrientation
302 | }
303 | }
304 |
305 | private func normalize(_ angle: Float, forMinimalRotationTo ref: Float) -> Float {
306 | // Normalize angle in steps of 90 degrees such that the rotation to the other angle is minimal
307 | var normalized = angle
308 | while abs(normalized - ref) > .pi / 4 {
309 | if angle > ref {
310 | normalized -= .pi / 2
311 | } else {
312 | normalized += .pi / 2
313 | }
314 | }
315 | return normalized
316 | }
317 |
318 | /**
319 | Reduce visual size change with distance by scaling up when close and down when far away.
320 |
321 | These adjustments result in a scale of 1.0x for a distance of 0.7 m or less
322 | (estimated distance when looking at a table), and a scale of 1.2x
323 | for a distance 1.5 m distance (estimated distance when looking at the floor).
324 | */
325 | private func scaleBasedOnDistance(camera: ARCamera?) -> Float {
326 | guard let camera = camera else { return 1.0 }
327 |
328 | let distanceFromCamera = simd_length(simdWorldPosition - camera.transform.translation)
329 | if distanceFromCamera < 0.7 {
330 | return distanceFromCamera / 0.7
331 | } else {
332 | return 0.25 * distanceFromCamera + 0.825
333 | }
334 | }
335 |
336 | // MARK: Animations
337 |
338 | private func performOpenAnimation() {
339 | guard !isOpen, !isAnimating else { return }
340 | isOpen = true
341 | isAnimating = true
342 |
343 | // Open animation
344 | SCNTransaction.begin()
345 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
346 | SCNTransaction.animationDuration = FocusSquare.animationDuration / 4
347 | positioningNode.opacity = 1.0
348 | for segment in segments {
349 | segment.open()
350 | }
351 | SCNTransaction.completionBlock = {
352 | self.positioningNode.runAction(pulseAction(), forKey: "pulse")
353 | // This is a safe operation because `SCNTransaction`'s completion block is called back on the main thread.
354 | self.isAnimating = false
355 | }
356 | SCNTransaction.commit()
357 |
358 | // Add a scale/bounce animation.
359 | SCNTransaction.begin()
360 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
361 | SCNTransaction.animationDuration = FocusSquare.animationDuration / 4
362 | positioningNode.simdScale = float3(FocusSquare.size)
363 | SCNTransaction.commit()
364 | }
365 |
366 | private func performCloseAnimation(flash: Bool = false) {
367 | guard isOpen, !isAnimating else { return }
368 | isOpen = false
369 | isAnimating = true
370 |
371 | positioningNode.removeAction(forKey: "pulse")
372 | positioningNode.opacity = 1.0
373 |
374 | // Close animation
375 | SCNTransaction.begin()
376 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
377 | SCNTransaction.animationDuration = FocusSquare.animationDuration / 2
378 | positioningNode.opacity = 0.99
379 | SCNTransaction.completionBlock = {
380 | SCNTransaction.begin()
381 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
382 | SCNTransaction.animationDuration = FocusSquare.animationDuration / 4
383 | for segment in self.segments {
384 | segment.close()
385 | }
386 | SCNTransaction.completionBlock = { self.isAnimating = false }
387 | SCNTransaction.commit()
388 | }
389 | SCNTransaction.commit()
390 |
391 | // Scale/bounce animation
392 | positioningNode.addAnimation(scaleAnimation(for: "transform.scale.x"), forKey: "transform.scale.x")
393 | positioningNode.addAnimation(scaleAnimation(for: "transform.scale.y"), forKey: "transform.scale.y")
394 | positioningNode.addAnimation(scaleAnimation(for: "transform.scale.z"), forKey: "transform.scale.z")
395 |
396 | if flash {
397 | let waitAction = SCNAction.wait(duration: FocusSquare.animationDuration * 0.75)
398 | let fadeInAction = SCNAction.fadeOpacity(to: 0.25, duration: FocusSquare.animationDuration * 0.125)
399 | let fadeOutAction = SCNAction.fadeOpacity(to: 0.0, duration: FocusSquare.animationDuration * 0.125)
400 | fillPlane.runAction(SCNAction.sequence([waitAction, fadeInAction, fadeOutAction]))
401 |
402 | let flashSquareAction = flashAnimation(duration: FocusSquare.animationDuration * 0.25)
403 | for segment in segments {
404 | segment.runAction(.sequence([waitAction, flashSquareAction]))
405 | }
406 | }
407 | }
408 |
409 | private func performAlignmentAnimation(to newOrientation: simd_quatf) {
410 | isChangingAlignment = true
411 | SCNTransaction.begin()
412 | SCNTransaction.completionBlock = {
413 | self.isChangingAlignment = false
414 | }
415 | SCNTransaction.animationDuration = 0.5
416 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
417 | simdOrientation = newOrientation
418 | SCNTransaction.commit()
419 | }
420 |
421 | // MARK: Convenience Methods
422 |
423 | private func scaleAnimation(for keyPath: String) -> CAKeyframeAnimation {
424 | let scaleAnimation = CAKeyframeAnimation(keyPath: keyPath)
425 |
426 | let easeOut = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
427 | let easeInOut = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
428 | let linear = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
429 |
430 | let size = FocusSquare.size
431 | let ts = FocusSquare.size * FocusSquare.scaleForClosedSquare
432 | let values = [size, size * 1.15, size * 1.15, ts * 0.97, ts]
433 | let keyTimes: [NSNumber] = [0.00, 0.25, 0.50, 0.75, 1.00]
434 | let timingFunctions = [easeOut, linear, easeOut, easeInOut]
435 |
436 | scaleAnimation.values = values
437 | scaleAnimation.keyTimes = keyTimes
438 | scaleAnimation.timingFunctions = timingFunctions
439 | scaleAnimation.duration = FocusSquare.animationDuration
440 |
441 | return scaleAnimation
442 | }
443 |
444 | /// Sets the rendering order of the `positioningNode` to show on top or under other scene content.
445 | func displayNodeHierarchyOnTop(_ isOnTop: Bool) {
446 | // Recursivley traverses the node's children to update the rendering order depending on the `isOnTop` parameter.
447 | func updateRenderOrder(for node: SCNNode) {
448 | node.renderingOrder = isOnTop ? 2 : 0
449 |
450 | for material in node.geometry?.materials ?? [] {
451 | material.readsFromDepthBuffer = !isOnTop
452 | }
453 |
454 | for child in node.childNodes {
455 | updateRenderOrder(for: child)
456 | }
457 | }
458 |
459 | updateRenderOrder(for: positioningNode)
460 | }
461 |
462 | private lazy var fillPlane: SCNNode = {
463 | let correctionFactor = FocusSquare.thickness / 2 // correction to align lines perfectly
464 | let length = CGFloat(1.0 - FocusSquare.thickness * 2 + correctionFactor)
465 |
466 | let plane = SCNPlane(width: length, height: length)
467 | let node = SCNNode(geometry: plane)
468 | node.name = "fillPlane"
469 | node.opacity = 0.0
470 |
471 | let material = plane.firstMaterial!
472 | material.diffuse.contents = FocusSquare.fillColor
473 | material.isDoubleSided = true
474 | material.ambient.contents = UIColor.black
475 | material.lightingModel = .constant
476 | material.emission.contents = FocusSquare.fillColor
477 |
478 | return node
479 | }()
480 | }
481 |
482 | // MARK: - Animations and Actions
483 |
484 | private func pulseAction() -> SCNAction {
485 | let pulseOutAction = SCNAction.fadeOpacity(to: 0.4, duration: 0.5)
486 | let pulseInAction = SCNAction.fadeOpacity(to: 1.0, duration: 0.5)
487 | pulseOutAction.timingMode = .easeInEaseOut
488 | pulseInAction.timingMode = .easeInEaseOut
489 |
490 | return SCNAction.repeatForever(SCNAction.sequence([pulseOutAction, pulseInAction]))
491 | }
492 |
493 | private func flashAnimation(duration: TimeInterval) -> SCNAction {
494 | let action = SCNAction.customAction(duration: duration) { (node, elapsedTime) -> Void in
495 | // animate color from HSB 48/100/100 to 48/30/100 and back
496 | let elapsedTimePercentage = elapsedTime / CGFloat(duration)
497 | let saturation = 2.8 * (elapsedTimePercentage - 0.5) * (elapsedTimePercentage - 0.5) + 0.3
498 | if let material = node.geometry?.firstMaterial {
499 | material.diffuse.contents = UIColor(hue: 0.1333, saturation: saturation, brightness: 1.0, alpha: 1.0)
500 | }
501 | }
502 | return action
503 | }
504 |
505 |
--------------------------------------------------------------------------------
/CloudCube/CloudCube/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
51 |
70 |
88 |
106 |
124 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
162 |
184 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 | When A Horizontal Plane Has Been Detected, The Foucs Square Will Close, And You Can Place Your Cube By Tapping On The Screen.
254 |
255 | To Share Your World Map & Cube With Your Friends, Press This Button:
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 | You Can Rotate The Cube Using A PanGesture, & Scale It By Using A Pinch Gesture.
269 |
270 | To Change The Colour Of The Cube, Double Tap On Any Of It's Faces & Then Select A Colour From The Buttons At The Bottom!
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
--------------------------------------------------------------------------------