├── .gitattributes ├── ARPaint.xcodeproj ├── .xcodesamplecode.plist └── project.pbxproj ├── ARPaint ├── AppDelegate.swift ├── Base.lproj │ └── Main.storyboard ├── Resources │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── ARKit-120.png │ │ │ ├── ARKit-121.png │ │ │ ├── ARKit-152.png │ │ │ ├── ARKit-167.png │ │ │ ├── ARKit-180.png │ │ │ ├── ARKit-40.png │ │ │ ├── ARKit-41.png │ │ │ ├── ARKit-58.png │ │ │ ├── ARKit-59.png │ │ │ ├── ARKit-60.png │ │ │ ├── ARKit-76.png │ │ │ ├── ARKit-80.png │ │ │ ├── ARKit-81.png │ │ │ ├── ARKit-87.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── buttonring.imageset │ │ │ ├── Contents.json │ │ │ ├── ring@2x.png │ │ │ └── ring@3x.png │ │ ├── restart.imageset │ │ │ ├── Contents.json │ │ │ ├── refresh@2x.png │ │ │ └── refresh@3x.png │ │ ├── restartPressed.imageset │ │ │ ├── Contents.json │ │ │ ├── refreshPressed@2x.png │ │ │ └── refreshPressed@3x.png │ │ ├── shutter.imageset │ │ │ ├── Contents.json │ │ │ ├── shutter@2x.png │ │ │ └── shutter@3x.png │ │ └── shutterPressed.imageset │ │ │ ├── Contents.json │ │ │ ├── shutterPressed@2px.png │ │ │ └── shutterPressed@3x.png │ ├── Info.plist │ ├── LaunchScreen.storyboard │ ├── Main.storyboard │ ├── Models.scnassets │ │ └── sharedImages │ │ │ ├── environment.jpg │ │ │ └── environment_blur.exr │ └── wood-matrial │ │ ├── wood-diffuse.jpg │ │ ├── wood-normal.png │ │ └── wood-specular.jpg ├── UI Elements │ ├── Focus Squares │ │ ├── FocusSquare.swift │ │ └── FocusSquareSegment.swift │ └── Plane.swift ├── Utilities │ ├── ARSCNView+HitTests.swift │ ├── SceneExtensions.swift │ ├── TextManager.swift │ └── Utilities.swift ├── ViewController+Actions.swift ├── ViewController.swift └── Virtual Objects │ ├── PointNode.swift │ └── VirtualObjectManager.swift ├── Configuration └── SampleCode.xcconfig └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /ARPaint.xcodeproj/.xcodesamplecode.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ARPaint.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1488323F1EFC378A0043E0AB /* ViewController+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1488323E1EFC378A0043E0AB /* ViewController+Actions.swift */; }; 11 | 148832411EFC3B250043E0AB /* FocusSquareSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148832401EFC3B250043E0AB /* FocusSquareSegment.swift */; }; 12 | 148832491EFC3E520043E0AB /* ARSCNView+HitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148832481EFC3E520043E0AB /* ARSCNView+HitTests.swift */; }; 13 | 3929C48C1E89EC2C00E00B60 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3929C48B1E89EC2C00E00B60 /* AppDelegate.swift */; }; 14 | 3929C48E1E89EC2C00E00B60 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3929C48D1E89EC2C00E00B60 /* ViewController.swift */; }; 15 | 3929C4931E89EC2C00E00B60 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3929C4921E89EC2C00E00B60 /* Assets.xcassets */; }; 16 | 3929C49F1E89EE4400E00B60 /* Models.scnassets in Resources */ = {isa = PBXBuildFile; fileRef = 3929C49E1E89EE4400E00B60 /* Models.scnassets */; }; 17 | 392BB82C1EA19A7C00FBBAC8 /* Plane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392BB82B1EA19A7C00FBBAC8 /* Plane.swift */; }; 18 | 39C0B95A1E9C6B1F003FCA0C /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C0B9581E9C6B1F003FCA0C /* Utilities.swift */; }; 19 | 39D8DC1B1E8ADAE600386B05 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 39D8DC1A1E8ADAE600386B05 /* LaunchScreen.storyboard */; }; 20 | 5D1D54DC1F26425C00DD9921 /* PointNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1D54DB1F2641C300DD9921 /* PointNode.swift */; }; 21 | 5D1D54E01F26435800DD9921 /* wood-normal.png in Resources */ = {isa = PBXBuildFile; fileRef = 5D1D54DD1F26435700DD9921 /* wood-normal.png */; }; 22 | 5D1D54E11F26435800DD9921 /* wood-diffuse.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 5D1D54DE1F26435800DD9921 /* wood-diffuse.jpg */; }; 23 | 5D1D54E21F26435800DD9921 /* wood-specular.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 5D1D54DF1F26435800DD9921 /* wood-specular.jpg */; }; 24 | 5D4C7E871F2BAFA30087471B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5D4C7E851F2BAF9C0087471B /* Main.storyboard */; }; 25 | C41366B91EB80DD400EC054A /* TextManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41366B81EB80DD400EC054A /* TextManager.swift */; }; 26 | C42A71BD1E9C0AF200944367 /* FocusSquare.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42A71BC1E9C0AF200944367 /* FocusSquare.swift */; }; 27 | C46356671EF9CC500080C8D9 /* SceneExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46356661EF9CC480080C8D9 /* SceneExtensions.swift */; }; 28 | C46356681EF9CC530080C8D9 /* VirtualObjectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46356651EF9CB1A0080C8D9 /* VirtualObjectManager.swift */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 11063F3B1EBE651B0033EE6D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 33 | 1488323E1EFC378A0043E0AB /* ViewController+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+Actions.swift"; sourceTree = ""; }; 34 | 148832401EFC3B250043E0AB /* FocusSquareSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusSquareSegment.swift; sourceTree = ""; }; 35 | 148832481EFC3E520043E0AB /* ARSCNView+HitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ARSCNView+HitTests.swift"; sourceTree = ""; }; 36 | 3929C4881E89EC2C00E00B60 /* ARPaint.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ARPaint.app; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 3929C48B1E89EC2C00E00B60 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 38 | 3929C48D1E89EC2C00E00B60 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 39 | 3929C4921E89EC2C00E00B60 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 40 | 3929C49E1E89EE4400E00B60 /* Models.scnassets */ = {isa = PBXFileReference; lastKnownFileType = wrapper.scnassets; path = Models.scnassets; sourceTree = ""; }; 41 | 392BB82B1EA19A7C00FBBAC8 /* Plane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Plane.swift; sourceTree = ""; }; 42 | 39C0B9581E9C6B1F003FCA0C /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; 43 | 39D8DC1A1E8ADAE600386B05 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 44 | 5D1D54DB1F2641C300DD9921 /* PointNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointNode.swift; sourceTree = ""; }; 45 | 5D1D54DD1F26435700DD9921 /* wood-normal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "wood-normal.png"; sourceTree = ""; }; 46 | 5D1D54DE1F26435800DD9921 /* wood-diffuse.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "wood-diffuse.jpg"; sourceTree = ""; }; 47 | 5D1D54DF1F26435800DD9921 /* wood-specular.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "wood-specular.jpg"; sourceTree = ""; }; 48 | 5D4C7E861F2BAF9C0087471B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = ARPaint/Base.lproj/Main.storyboard; sourceTree = SOURCE_ROOT; }; 49 | 5DB31FE11F2AEB0200069EC4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | 7497A16BF0220DFA28C77519 /* SampleCode.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = SampleCode.xcconfig; path = Configuration/SampleCode.xcconfig; sourceTree = ""; }; 51 | C41366B81EB80DD400EC054A /* TextManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextManager.swift; sourceTree = ""; }; 52 | C42A71BC1E9C0AF200944367 /* FocusSquare.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusSquare.swift; sourceTree = ""; }; 53 | C46356651EF9CB1A0080C8D9 /* VirtualObjectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualObjectManager.swift; sourceTree = ""; }; 54 | C46356661EF9CC480080C8D9 /* SceneExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneExtensions.swift; sourceTree = ""; }; 55 | /* End PBXFileReference section */ 56 | 57 | /* Begin PBXFrameworksBuildPhase section */ 58 | 3929C4851E89EC2C00E00B60 /* Frameworks */ = { 59 | isa = PBXFrameworksBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | ); 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | /* End PBXFrameworksBuildPhase section */ 66 | 67 | /* Begin PBXGroup section */ 68 | 113B64D01ECA81CA0071CBD1 /* UI Elements */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 392BB82B1EA19A7C00FBBAC8 /* Plane.swift */, 72 | 148832471EFC3C9F0043E0AB /* Focus Squares */, 73 | ); 74 | path = "UI Elements"; 75 | sourceTree = ""; 76 | }; 77 | 148832471EFC3C9F0043E0AB /* Focus Squares */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | C42A71BC1E9C0AF200944367 /* FocusSquare.swift */, 81 | 148832401EFC3B250043E0AB /* FocusSquareSegment.swift */, 82 | ); 83 | path = "Focus Squares"; 84 | sourceTree = ""; 85 | }; 86 | 1488324A1EFC3E9C0043E0AB /* Utilities */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 148832481EFC3E520043E0AB /* ARSCNView+HitTests.swift */, 90 | C46356661EF9CC480080C8D9 /* SceneExtensions.swift */, 91 | C41366B81EB80DD400EC054A /* TextManager.swift */, 92 | 39C0B9581E9C6B1F003FCA0C /* Utilities.swift */, 93 | ); 94 | path = Utilities; 95 | sourceTree = ""; 96 | }; 97 | 1488324C1EFC3ED00043E0AB /* Resources */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 5D1D54E31F27FB7200DD9921 /* wood-matrial */, 101 | 3929C49E1E89EE4400E00B60 /* Models.scnassets */, 102 | 3929C4921E89EC2C00E00B60 /* Assets.xcassets */, 103 | 5DB31FE11F2AEB0200069EC4 /* Info.plist */, 104 | 39D8DC1A1E8ADAE600386B05 /* LaunchScreen.storyboard */, 105 | 5D4C7E851F2BAF9C0087471B /* Main.storyboard */, 106 | ); 107 | path = Resources; 108 | sourceTree = ""; 109 | }; 110 | 3929C47F1E89EC2C00E00B60 = { 111 | isa = PBXGroup; 112 | children = ( 113 | 11063F3B1EBE651B0033EE6D /* README.md */, 114 | 3929C48A1E89EC2C00E00B60 /* ARPaint */, 115 | 3929C4891E89EC2C00E00B60 /* Products */, 116 | 935B13DEAF6BCE493F535540 /* Configuration */, 117 | ); 118 | sourceTree = ""; 119 | }; 120 | 3929C4891E89EC2C00E00B60 /* Products */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | 3929C4881E89EC2C00E00B60 /* ARPaint.app */, 124 | ); 125 | name = Products; 126 | sourceTree = ""; 127 | }; 128 | 3929C48A1E89EC2C00E00B60 /* ARPaint */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 3929C48B1E89EC2C00E00B60 /* AppDelegate.swift */, 132 | 3929C48D1E89EC2C00E00B60 /* ViewController.swift */, 133 | 1488323E1EFC378A0043E0AB /* ViewController+Actions.swift */, 134 | C40DF57B1EC2330700D9D59E /* Virtual Objects */, 135 | 113B64D01ECA81CA0071CBD1 /* UI Elements */, 136 | 1488324A1EFC3E9C0043E0AB /* Utilities */, 137 | 1488324C1EFC3ED00043E0AB /* Resources */, 138 | ); 139 | path = ARPaint; 140 | sourceTree = ""; 141 | }; 142 | 5D1D54E31F27FB7200DD9921 /* wood-matrial */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 5D1D54DE1F26435800DD9921 /* wood-diffuse.jpg */, 146 | 5D1D54DD1F26435700DD9921 /* wood-normal.png */, 147 | 5D1D54DF1F26435800DD9921 /* wood-specular.jpg */, 148 | ); 149 | path = "wood-matrial"; 150 | sourceTree = ""; 151 | }; 152 | 935B13DEAF6BCE493F535540 /* Configuration */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | 7497A16BF0220DFA28C77519 /* SampleCode.xcconfig */, 156 | ); 157 | name = Configuration; 158 | sourceTree = ""; 159 | }; 160 | C40DF57B1EC2330700D9D59E /* Virtual Objects */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | 5D1D54DB1F2641C300DD9921 /* PointNode.swift */, 164 | C46356651EF9CB1A0080C8D9 /* VirtualObjectManager.swift */, 165 | ); 166 | path = "Virtual Objects"; 167 | sourceTree = ""; 168 | }; 169 | /* End PBXGroup section */ 170 | 171 | /* Begin PBXNativeTarget section */ 172 | 3929C4871E89EC2C00E00B60 /* ARPaint */ = { 173 | isa = PBXNativeTarget; 174 | buildConfigurationList = 3929C49A1E89EC2C00E00B60 /* Build configuration list for PBXNativeTarget "ARPaint" */; 175 | buildPhases = ( 176 | 3929C4841E89EC2C00E00B60 /* Sources */, 177 | 3929C4851E89EC2C00E00B60 /* Frameworks */, 178 | 3929C4861E89EC2C00E00B60 /* Resources */, 179 | ); 180 | buildRules = ( 181 | ); 182 | dependencies = ( 183 | ); 184 | name = ARPaint; 185 | productName = ARKitExample; 186 | productReference = 3929C4881E89EC2C00E00B60 /* ARPaint.app */; 187 | productType = "com.apple.product-type.application"; 188 | }; 189 | /* End PBXNativeTarget section */ 190 | 191 | /* Begin PBXProject section */ 192 | 3929C4801E89EC2C00E00B60 /* Project object */ = { 193 | isa = PBXProject; 194 | attributes = { 195 | LastSwiftUpdateCheck = 0900; 196 | LastUpgradeCheck = 0900; 197 | ORGANIZATIONNAME = Apple; 198 | TargetAttributes = { 199 | 3929C4871E89EC2C00E00B60 = { 200 | CreatedOnToolsVersion = 9.0; 201 | DevelopmentTeam = MDBBH5S24M; 202 | LastSwiftMigration = 0900; 203 | ProvisioningStyle = Automatic; 204 | }; 205 | }; 206 | }; 207 | buildConfigurationList = 3929C4831E89EC2C00E00B60 /* Build configuration list for PBXProject "ARPaint" */; 208 | compatibilityVersion = "Xcode 3.2"; 209 | developmentRegion = English; 210 | hasScannedForEncodings = 0; 211 | knownRegions = ( 212 | en, 213 | Base, 214 | ); 215 | mainGroup = 3929C47F1E89EC2C00E00B60; 216 | productRefGroup = 3929C4891E89EC2C00E00B60 /* Products */; 217 | projectDirPath = ""; 218 | projectRoot = ""; 219 | targets = ( 220 | 3929C4871E89EC2C00E00B60 /* ARPaint */, 221 | ); 222 | }; 223 | /* End PBXProject section */ 224 | 225 | /* Begin PBXResourcesBuildPhase section */ 226 | 3929C4861E89EC2C00E00B60 /* Resources */ = { 227 | isa = PBXResourcesBuildPhase; 228 | buildActionMask = 2147483647; 229 | files = ( 230 | 5D4C7E871F2BAFA30087471B /* Main.storyboard in Resources */, 231 | 3929C49F1E89EE4400E00B60 /* Models.scnassets in Resources */, 232 | 5D1D54E11F26435800DD9921 /* wood-diffuse.jpg in Resources */, 233 | 39D8DC1B1E8ADAE600386B05 /* LaunchScreen.storyboard in Resources */, 234 | 5D1D54E01F26435800DD9921 /* wood-normal.png in Resources */, 235 | 5D1D54E21F26435800DD9921 /* wood-specular.jpg in Resources */, 236 | 3929C4931E89EC2C00E00B60 /* Assets.xcassets in Resources */, 237 | ); 238 | runOnlyForDeploymentPostprocessing = 0; 239 | }; 240 | /* End PBXResourcesBuildPhase section */ 241 | 242 | /* Begin PBXSourcesBuildPhase section */ 243 | 3929C4841E89EC2C00E00B60 /* Sources */ = { 244 | isa = PBXSourcesBuildPhase; 245 | buildActionMask = 2147483647; 246 | files = ( 247 | 1488323F1EFC378A0043E0AB /* ViewController+Actions.swift in Sources */, 248 | 3929C48E1E89EC2C00E00B60 /* ViewController.swift in Sources */, 249 | C41366B91EB80DD400EC054A /* TextManager.swift in Sources */, 250 | 39C0B95A1E9C6B1F003FCA0C /* Utilities.swift in Sources */, 251 | 392BB82C1EA19A7C00FBBAC8 /* Plane.swift in Sources */, 252 | 5D1D54DC1F26425C00DD9921 /* PointNode.swift in Sources */, 253 | C46356671EF9CC500080C8D9 /* SceneExtensions.swift in Sources */, 254 | 148832491EFC3E520043E0AB /* ARSCNView+HitTests.swift in Sources */, 255 | 148832411EFC3B250043E0AB /* FocusSquareSegment.swift in Sources */, 256 | C42A71BD1E9C0AF200944367 /* FocusSquare.swift in Sources */, 257 | 3929C48C1E89EC2C00E00B60 /* AppDelegate.swift in Sources */, 258 | C46356681EF9CC530080C8D9 /* VirtualObjectManager.swift in Sources */, 259 | ); 260 | runOnlyForDeploymentPostprocessing = 0; 261 | }; 262 | /* End PBXSourcesBuildPhase section */ 263 | 264 | /* Begin PBXVariantGroup section */ 265 | 5D4C7E851F2BAF9C0087471B /* Main.storyboard */ = { 266 | isa = PBXVariantGroup; 267 | children = ( 268 | 5D4C7E861F2BAF9C0087471B /* Base */, 269 | ); 270 | name = Main.storyboard; 271 | sourceTree = ""; 272 | }; 273 | /* End PBXVariantGroup section */ 274 | 275 | /* Begin XCBuildConfiguration section */ 276 | 3929C4981E89EC2C00E00B60 /* Debug */ = { 277 | isa = XCBuildConfiguration; 278 | baseConfigurationReference = 7497A16BF0220DFA28C77519 /* SampleCode.xcconfig */; 279 | buildSettings = { 280 | ALWAYS_SEARCH_USER_PATHS = NO; 281 | CLANG_ANALYZER_NONNULL = YES; 282 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 283 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 284 | CLANG_CXX_LIBRARY = "libc++"; 285 | CLANG_ENABLE_MODULES = YES; 286 | CLANG_ENABLE_OBJC_ARC = YES; 287 | CLANG_WARN_BOOL_CONVERSION = YES; 288 | CLANG_WARN_CONSTANT_CONVERSION = YES; 289 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 290 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 291 | CLANG_WARN_EMPTY_BODY = YES; 292 | CLANG_WARN_ENUM_CONVERSION = YES; 293 | CLANG_WARN_INFINITE_RECURSION = YES; 294 | CLANG_WARN_INT_CONVERSION = YES; 295 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 296 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 297 | CLANG_WARN_UNREACHABLE_CODE = YES; 298 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 299 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 300 | COPY_PHASE_STRIP = NO; 301 | DEBUG_INFORMATION_FORMAT = dwarf; 302 | ENABLE_STRICT_OBJC_MSGSEND = YES; 303 | ENABLE_TESTABILITY = YES; 304 | GCC_C_LANGUAGE_STANDARD = gnu99; 305 | GCC_DYNAMIC_NO_PIC = NO; 306 | GCC_NO_COMMON_BLOCKS = YES; 307 | GCC_OPTIMIZATION_LEVEL = 0; 308 | GCC_PREPROCESSOR_DEFINITIONS = ( 309 | "DEBUG=1", 310 | "$(inherited)", 311 | ); 312 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 313 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 314 | GCC_WARN_UNDECLARED_SELECTOR = YES; 315 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 316 | GCC_WARN_UNUSED_FUNCTION = YES; 317 | GCC_WARN_UNUSED_VARIABLE = YES; 318 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 319 | MTL_ENABLE_DEBUG_INFO = YES; 320 | ONLY_ACTIVE_ARCH = YES; 321 | SDKROOT = iphoneos; 322 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 323 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 324 | TARGETED_DEVICE_FAMILY = "1,2"; 325 | }; 326 | name = Debug; 327 | }; 328 | 3929C4991E89EC2C00E00B60 /* Release */ = { 329 | isa = XCBuildConfiguration; 330 | baseConfigurationReference = 7497A16BF0220DFA28C77519 /* SampleCode.xcconfig */; 331 | buildSettings = { 332 | ALWAYS_SEARCH_USER_PATHS = NO; 333 | CLANG_ANALYZER_NONNULL = YES; 334 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 335 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 336 | CLANG_CXX_LIBRARY = "libc++"; 337 | CLANG_ENABLE_MODULES = YES; 338 | CLANG_ENABLE_OBJC_ARC = YES; 339 | CLANG_WARN_BOOL_CONVERSION = YES; 340 | CLANG_WARN_CONSTANT_CONVERSION = YES; 341 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 342 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 343 | CLANG_WARN_EMPTY_BODY = YES; 344 | CLANG_WARN_ENUM_CONVERSION = YES; 345 | CLANG_WARN_INFINITE_RECURSION = YES; 346 | CLANG_WARN_INT_CONVERSION = YES; 347 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 348 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 349 | CLANG_WARN_UNREACHABLE_CODE = YES; 350 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 351 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 352 | COPY_PHASE_STRIP = NO; 353 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 354 | ENABLE_NS_ASSERTIONS = NO; 355 | ENABLE_STRICT_OBJC_MSGSEND = YES; 356 | GCC_C_LANGUAGE_STANDARD = gnu99; 357 | GCC_NO_COMMON_BLOCKS = YES; 358 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 359 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 360 | GCC_WARN_UNDECLARED_SELECTOR = YES; 361 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 362 | GCC_WARN_UNUSED_FUNCTION = YES; 363 | GCC_WARN_UNUSED_VARIABLE = YES; 364 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 365 | MTL_ENABLE_DEBUG_INFO = NO; 366 | SDKROOT = iphoneos; 367 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 368 | TARGETED_DEVICE_FAMILY = "1,2"; 369 | VALIDATE_PRODUCT = YES; 370 | }; 371 | name = Release; 372 | }; 373 | 3929C49B1E89EC2C00E00B60 /* Debug */ = { 374 | isa = XCBuildConfiguration; 375 | baseConfigurationReference = 7497A16BF0220DFA28C77519 /* SampleCode.xcconfig */; 376 | buildSettings = { 377 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 378 | CODE_SIGN_IDENTITY = "-"; 379 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 380 | CODE_SIGN_STYLE = Automatic; 381 | DEVELOPMENT_TEAM = MDBBH5S24M; 382 | INFOPLIST_FILE = "$(SRCROOT)/ARPaint/Resources/Info.plist"; 383 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 384 | PRODUCT_BUNDLE_IDENTIFIER = "com.badrit.apple-samplecode.arkit-sample-app"; 385 | PRODUCT_NAME = "$(TARGET_NAME)"; 386 | PROVISIONING_PROFILE_SPECIFIER = ""; 387 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 388 | SWIFT_VERSION = 4.0; 389 | }; 390 | name = Debug; 391 | }; 392 | 3929C49C1E89EC2C00E00B60 /* Release */ = { 393 | isa = XCBuildConfiguration; 394 | baseConfigurationReference = 7497A16BF0220DFA28C77519 /* SampleCode.xcconfig */; 395 | buildSettings = { 396 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 397 | CODE_SIGN_IDENTITY = "-"; 398 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 399 | CODE_SIGN_STYLE = Automatic; 400 | DEVELOPMENT_TEAM = MDBBH5S24M; 401 | INFOPLIST_FILE = "$(SRCROOT)/ARPaint/Resources/Info.plist"; 402 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 403 | PRODUCT_BUNDLE_IDENTIFIER = "com.badrit.apple-samplecode.arkit-sample-app"; 404 | PRODUCT_NAME = "$(TARGET_NAME)"; 405 | PROVISIONING_PROFILE_SPECIFIER = ""; 406 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 407 | SWIFT_VERSION = 4.0; 408 | }; 409 | name = Release; 410 | }; 411 | /* End XCBuildConfiguration section */ 412 | 413 | /* Begin XCConfigurationList section */ 414 | 3929C4831E89EC2C00E00B60 /* Build configuration list for PBXProject "ARPaint" */ = { 415 | isa = XCConfigurationList; 416 | buildConfigurations = ( 417 | 3929C4981E89EC2C00E00B60 /* Debug */, 418 | 3929C4991E89EC2C00E00B60 /* Release */, 419 | ); 420 | defaultConfigurationIsVisible = 0; 421 | defaultConfigurationName = Release; 422 | }; 423 | 3929C49A1E89EC2C00E00B60 /* Build configuration list for PBXNativeTarget "ARPaint" */ = { 424 | isa = XCConfigurationList; 425 | buildConfigurations = ( 426 | 3929C49B1E89EC2C00E00B60 /* Debug */, 427 | 3929C49C1E89EC2C00E00B60 /* Release */, 428 | ); 429 | defaultConfigurationIsVisible = 0; 430 | defaultConfigurationName = Release; 431 | }; 432 | /* End XCConfigurationList section */ 433 | }; 434 | rootObject = 3929C4801E89EC2C00E00B60 /* Project object */; 435 | } 436 | -------------------------------------------------------------------------------- /ARPaint/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | Empty application delegate class. 6 | */ 7 | 8 | import UIKit 9 | 10 | @UIApplicationMain 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | // Nothing to do here. See ViewController for primary app features. 16 | 17 | } 18 | 19 | -------------------------------------------------------------------------------- /ARPaint/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 | 40 | 56 | 65 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-120.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-121.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-121.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-152.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-167.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-180.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-40.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-41.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-58.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-59.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-60.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-76.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-80.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-81.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-81.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/ARKit-87.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "ARKit-40.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "ARKit-60.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "ARKit-59.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "ARKit-87.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "ARKit-81.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "ARKit-121.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "ARKit-120.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "ARKit-180.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "idiom" : "ipad", 53 | "size" : "20x20", 54 | "scale" : "1x" 55 | }, 56 | { 57 | "size" : "20x20", 58 | "idiom" : "ipad", 59 | "filename" : "ARKit-41.png", 60 | "scale" : "2x" 61 | }, 62 | { 63 | "idiom" : "ipad", 64 | "size" : "29x29", 65 | "scale" : "1x" 66 | }, 67 | { 68 | "size" : "29x29", 69 | "idiom" : "ipad", 70 | "filename" : "ARKit-58.png", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "40x40", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "size" : "40x40", 80 | "idiom" : "ipad", 81 | "filename" : "ARKit-80.png", 82 | "scale" : "2x" 83 | }, 84 | { 85 | "size" : "76x76", 86 | "idiom" : "ipad", 87 | "filename" : "ARKit-76.png", 88 | "scale" : "1x" 89 | }, 90 | { 91 | "size" : "76x76", 92 | "idiom" : "ipad", 93 | "filename" : "ARKit-152.png", 94 | "scale" : "2x" 95 | }, 96 | { 97 | "size" : "83.5x83.5", 98 | "idiom" : "ipad", 99 | "filename" : "ARKit-167.png", 100 | "scale" : "2x" 101 | }, 102 | { 103 | "idiom" : "ios-marketing", 104 | "size" : "1024x1024", 105 | "scale" : "1x" 106 | } 107 | ], 108 | "info" : { 109 | "version" : 1, 110 | "author" : "xcode" 111 | } 112 | } -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/buttonring.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ring@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ring@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/buttonring.imageset/ring@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/buttonring.imageset/ring@2x.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/buttonring.imageset/ring@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/buttonring.imageset/ring@3x.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/restart.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "refresh@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "refresh@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/restart.imageset/refresh@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/restart.imageset/refresh@2x.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/restart.imageset/refresh@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/restart.imageset/refresh@3x.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/restartPressed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "refreshPressed@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "refreshPressed@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/restartPressed.imageset/refreshPressed@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/restartPressed.imageset/refreshPressed@2x.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/restartPressed.imageset/refreshPressed@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/restartPressed.imageset/refreshPressed@3x.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/shutter.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "shutter@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "shutter@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/shutter.imageset/shutter@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/shutter.imageset/shutter@2x.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/shutter.imageset/shutter@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/shutter.imageset/shutter@3x.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/shutterPressed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "shutterPressed@2px.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "shutterPressed@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/shutterPressed.imageset/shutterPressed@2px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/shutterPressed.imageset/shutterPressed@2px.png -------------------------------------------------------------------------------- /ARPaint/Resources/Assets.xcassets/shutterPressed.imageset/shutterPressed@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Assets.xcassets/shutterPressed.imageset/shutterPressed@3x.png -------------------------------------------------------------------------------- /ARPaint/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ARPaint 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSCameraUsageDescription 26 | The camera is used for augmenting reality 27 | NSLocationWhenInUseUsageDescription 28 | The location is used for augmenting reality 29 | NSPhotoLibraryAddUsageDescription 30 | Save photos of your ARExperience 31 | NSPhotoLibraryUsageDescription 32 | Save photos of your AR experience 33 | UILaunchStoryboardName 34 | LaunchScreen 35 | UIMainStoryboardFile 36 | Main 37 | UIRequiredDeviceCapabilities 38 | 39 | armv7 40 | 41 | UIRequiresFullScreen 42 | 43 | UIStatusBarHidden 44 | 45 | UISupportedInterfaceOrientations 46 | 47 | UIInterfaceOrientationPortrait 48 | 49 | UISupportedInterfaceOrientations~ipad 50 | 51 | UIInterfaceOrientationPortrait 52 | 53 | UIViewControllerBasedStatusBarAppearance 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /ARPaint/Resources/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 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ARPaint/Resources/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 | 40 | 56 | 65 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /ARPaint/Resources/Models.scnassets/sharedImages/environment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Models.scnassets/sharedImages/environment.jpg -------------------------------------------------------------------------------- /ARPaint/Resources/Models.scnassets/sharedImages/environment_blur.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/Models.scnassets/sharedImages/environment_blur.exr -------------------------------------------------------------------------------- /ARPaint/Resources/wood-matrial/wood-diffuse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/wood-matrial/wood-diffuse.jpg -------------------------------------------------------------------------------- /ARPaint/Resources/wood-matrial/wood-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/wood-matrial/wood-normal.png -------------------------------------------------------------------------------- /ARPaint/Resources/wood-matrial/wood-specular.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oabdelkarim/ARPaint/f8b10acbccaecdfa190854d7ebd407db5031c721/ARPaint/Resources/wood-matrial/wood-specular.jpg -------------------------------------------------------------------------------- /ARPaint/UI Elements/Focus Squares/FocusSquare.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | SceneKit node wrapper shows UI in the AR scene for placing virtual objects. 6 | */ 7 | 8 | import Foundation 9 | import ARKit 10 | 11 | /// - Tag: FocusSquare 12 | class FocusSquare: SCNNode { 13 | 14 | // MARK: - Focus Square Configuration Properties 15 | 16 | // Original size of the focus square in m. 17 | private let focusSquareSize: Float = 0.17 18 | 19 | // Thickness of the focus square lines in m. 20 | private let focusSquareThickness: Float = 0.018 21 | 22 | // Scale factor for the focus square when it is closed, w.r.t. the original size. 23 | private let scaleForClosedSquare: Float = 0.97 24 | 25 | // Side length of the focus square segments when it is open (w.r.t. to a 1x1 square). 26 | private let sideLengthForOpenSquareSegments: CGFloat = 0.2 27 | 28 | // Duration of the open/close animation 29 | private let animationDuration = 0.7 30 | 31 | // Color of the focus square 32 | static let primaryColor = #colorLiteral(red: 1, green: 0.8, blue: 0, alpha: 1) // base yellow 33 | static let primaryColorLight = #colorLiteral(red: 1, green: 0.9254901961, blue: 0.4117647059, alpha: 1) // light yellow 34 | 35 | // For scale adapdation based on the camera distance, see the `scaleBasedOnDistance(camera:)` method. 36 | 37 | // MARK: - Position Properties 38 | 39 | var lastPositionOnPlane: float3? 40 | var lastPosition: float3? 41 | 42 | // MARK: - Other Properties 43 | 44 | private var isOpen = false 45 | private var isAnimating = false 46 | 47 | // use average of recent positions to avoid jitter 48 | private var recentFocusSquarePositions: [float3] = [] 49 | private var anchorsOfVisitedPlanes: Set = [] 50 | 51 | // MARK: - Initialization 52 | 53 | override init() { 54 | super.init() 55 | self.opacity = 0.0 56 | self.addChildNode(focusSquareNode) 57 | open() 58 | } 59 | 60 | required init?(coder aDecoder: NSCoder) { 61 | fatalError("init(coder:) has not been implemented") 62 | } 63 | 64 | // MARK: - Appearence 65 | 66 | func update(for position: float3, planeAnchor: ARPlaneAnchor?, camera: ARCamera?) { 67 | lastPosition = position 68 | if let anchor = planeAnchor { 69 | close(flash: !anchorsOfVisitedPlanes.contains(anchor)) 70 | lastPositionOnPlane = position 71 | anchorsOfVisitedPlanes.insert(anchor) 72 | } else { 73 | open() 74 | } 75 | updateTransform(for: position, camera: camera) 76 | } 77 | 78 | func hide() { 79 | if self.opacity == 1.0 { 80 | self.renderOnTop(false) 81 | self.runAction(.fadeOut(duration: 0.5)) 82 | } 83 | } 84 | 85 | func unhide() { 86 | if self.opacity == 0.0 { 87 | self.renderOnTop(true) 88 | self.runAction(.fadeIn(duration: 0.5)) 89 | } 90 | } 91 | 92 | // MARK: - Private 93 | 94 | private func updateTransform(for position: float3, camera: ARCamera?) { 95 | // add to list of recent positions 96 | recentFocusSquarePositions.append(position) 97 | 98 | // remove anything older than the last 8 99 | recentFocusSquarePositions.keepLast(8) 100 | 101 | // move to average of recent positions to avoid jitter 102 | if let average = recentFocusSquarePositions.average { 103 | self.simdPosition = average 104 | self.setUniformScale(scaleBasedOnDistance(camera: camera)) 105 | } 106 | 107 | // Correct y rotation of camera square 108 | if let camera = camera { 109 | let tilt = abs(camera.eulerAngles.x) 110 | let threshold1: Float = .pi / 2 * 0.65 111 | let threshold2: Float = .pi / 2 * 0.75 112 | let yaw = atan2f(camera.transform.columns.0.x, camera.transform.columns.1.x) 113 | var angle: Float = 0 114 | 115 | switch tilt { 116 | case 0.. Float { 130 | // Normalize angle in steps of 90 degrees such that the rotation to the other angle is minimal 131 | var normalized = angle 132 | while abs(normalized - ref) > .pi / 4 { 133 | if angle > ref { 134 | normalized -= .pi / 2 135 | } else { 136 | normalized += .pi / 2 137 | } 138 | } 139 | return normalized 140 | } 141 | 142 | /// Reduce visual size change with distance by scaling up when close and down when far away. 143 | /// 144 | /// These adjustments result in a scale of 1.0x for a distance of 0.7 m or less 145 | /// (estimated distance when looking at a table), and a scale of 1.2x 146 | /// for a distance 1.5 m distance (estimated distance when looking at the floor). 147 | private func scaleBasedOnDistance(camera: ARCamera?) -> Float { 148 | guard let camera = camera else { return 1.0 } 149 | 150 | let distanceFromCamera = simd_length(self.simdWorldPosition - camera.transform.translation) 151 | if distanceFromCamera < 0.7 { 152 | return distanceFromCamera / 0.7 153 | } else { 154 | return 0.25 * distanceFromCamera + 0.825 155 | } 156 | } 157 | 158 | private func pulseAction() -> SCNAction { 159 | let pulseOutAction = SCNAction.fadeOpacity(to: 0.4, duration: 0.5) 160 | let pulseInAction = SCNAction.fadeOpacity(to: 1.0, duration: 0.5) 161 | pulseOutAction.timingMode = .easeInEaseOut 162 | pulseInAction.timingMode = .easeInEaseOut 163 | 164 | return SCNAction.repeatForever(SCNAction.sequence([pulseOutAction, pulseInAction])) 165 | } 166 | 167 | private func stopPulsing(for node: SCNNode?) { 168 | node?.removeAction(forKey: "pulse") 169 | node?.opacity = 1.0 170 | } 171 | 172 | private func open() { 173 | if isOpen || isAnimating { 174 | return 175 | } 176 | 177 | // Open animation 178 | SCNTransaction.begin() 179 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) 180 | SCNTransaction.animationDuration = animationDuration / 4 181 | focusSquareNode.opacity = 1.0 182 | self.segments.forEach { segment in segment.open() } 183 | SCNTransaction.completionBlock = { self.focusSquareNode.runAction(self.pulseAction(), forKey: "pulse") } 184 | SCNTransaction.commit() 185 | 186 | // Scale/bounce animation 187 | SCNTransaction.begin() 188 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) 189 | SCNTransaction.animationDuration = animationDuration / 4 190 | focusSquareNode.setUniformScale(focusSquareSize) 191 | SCNTransaction.commit() 192 | 193 | isOpen = true 194 | } 195 | 196 | private func close(flash: Bool = false) { 197 | if !isOpen || isAnimating { 198 | return 199 | } 200 | 201 | isAnimating = true 202 | 203 | stopPulsing(for: focusSquareNode) 204 | 205 | // Close animation 206 | SCNTransaction.begin() 207 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) 208 | SCNTransaction.animationDuration = self.animationDuration / 2 209 | focusSquareNode.opacity = 0.99 210 | SCNTransaction.completionBlock = { 211 | SCNTransaction.begin() 212 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) 213 | SCNTransaction.animationDuration = self.animationDuration / 4 214 | self.segments.forEach { segment in segment.close() } 215 | SCNTransaction.completionBlock = { self.isAnimating = false } 216 | SCNTransaction.commit() 217 | } 218 | SCNTransaction.commit() 219 | 220 | // Scale/bounce animation 221 | focusSquareNode.addAnimation(scaleAnimation(for: "transform.scale.x"), forKey: "transform.scale.x") 222 | focusSquareNode.addAnimation(scaleAnimation(for: "transform.scale.y"), forKey: "transform.scale.y") 223 | focusSquareNode.addAnimation(scaleAnimation(for: "transform.scale.z"), forKey: "transform.scale.z") 224 | 225 | // Flash 226 | if flash { 227 | let waitAction = SCNAction.wait(duration: animationDuration * 0.75) 228 | let fadeInAction = SCNAction.fadeOpacity(to: 0.25, duration: animationDuration * 0.125) 229 | let fadeOutAction = SCNAction.fadeOpacity(to: 0.0, duration: animationDuration * 0.125) 230 | fillPlane.runAction(SCNAction.sequence([waitAction, fadeInAction, fadeOutAction])) 231 | 232 | let flashSquareAction = flashAnimation(duration: animationDuration * 0.25) 233 | segments.forEach { segment in 234 | segment.runAction(SCNAction.sequence([waitAction, flashSquareAction])) 235 | } 236 | } 237 | 238 | isOpen = false 239 | } 240 | 241 | private func flashAnimation(duration: TimeInterval) -> SCNAction { 242 | let action = SCNAction.customAction(duration: duration) { (node, elapsedTime) -> Void in 243 | // animate color from HSB 48/100/100 to 48/30/100 and back 244 | let elapsedTimePercentage = elapsedTime / CGFloat(duration) 245 | let saturation = 2.8 * (elapsedTimePercentage - 0.5) * (elapsedTimePercentage - 0.5) + 0.3 246 | if let material = node.geometry?.firstMaterial { 247 | material.diffuse.contents = UIColor(hue: 0.1333, saturation: saturation, brightness: 1.0, alpha: 1.0) 248 | } 249 | } 250 | return action 251 | } 252 | 253 | private func scaleAnimation(for keyPath: String) -> CAKeyframeAnimation { 254 | let scaleAnimation = CAKeyframeAnimation(keyPath: keyPath) 255 | 256 | let easeOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) 257 | let easeInOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) 258 | let linear = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) 259 | 260 | let fs = focusSquareSize 261 | let ts = focusSquareSize * scaleForClosedSquare 262 | let values = [fs, fs * 1.15, fs * 1.15, ts * 0.97, ts] 263 | let keyTimes: [NSNumber] = [0.00, 0.25, 0.50, 0.75, 1.00] 264 | let timingFunctions = [easeOut, linear, easeOut, easeInOut] 265 | 266 | scaleAnimation.values = values 267 | scaleAnimation.keyTimes = keyTimes 268 | scaleAnimation.timingFunctions = timingFunctions 269 | scaleAnimation.duration = animationDuration 270 | 271 | return scaleAnimation 272 | } 273 | 274 | private var segments: [FocusSquare.Segment] = [] 275 | 276 | private lazy var fillPlane: SCNNode = { 277 | let c = focusSquareThickness / 2 // correction to align lines perfectly 278 | let plane = SCNPlane(width: CGFloat(1.0 - focusSquareThickness * 2 + c), 279 | height: CGFloat(1.0 - focusSquareThickness * 2 + c)) 280 | let node = SCNNode(geometry: plane) 281 | node.name = "fillPlane" 282 | node.opacity = 0.0 283 | 284 | let material = plane.firstMaterial! 285 | material.diffuse.contents = FocusSquare.primaryColorLight 286 | material.isDoubleSided = true 287 | material.ambient.contents = UIColor.black 288 | material.lightingModel = .constant 289 | material.emission.contents = FocusSquare.primaryColorLight 290 | 291 | return node 292 | }() 293 | 294 | private lazy var focusSquareNode: SCNNode = { 295 | /* 296 | The focus square consists of eight segments as follows, which can be individually animated. 297 | 298 | s1 s2 299 | _ _ 300 | s3 | | s4 301 | 302 | s5 | | s6 303 | - - 304 | s7 s8 305 | */ 306 | let s1 = Segment(name: "s1", corner: .topLeft, alignment: .horizontal) 307 | let s2 = Segment(name: "s2", corner: .topRight, alignment: .horizontal) 308 | let s3 = Segment(name: "s3", corner: .topLeft, alignment: .vertical) 309 | let s4 = Segment(name: "s4", corner: .topRight, alignment: .vertical) 310 | let s5 = Segment(name: "s5", corner: .bottomLeft, alignment: .vertical) 311 | let s6 = Segment(name: "s6", corner: .bottomRight, alignment: .vertical) 312 | let s7 = Segment(name: "s7", corner: .bottomLeft, alignment: .horizontal) 313 | let s8 = Segment(name: "s8", corner: .bottomRight, alignment: .horizontal) 314 | 315 | let sl: Float = 0.5 // segment length 316 | let c: Float = focusSquareThickness / 2 // correction to align lines perfectly 317 | s1.simdPosition += float3(-(sl / 2 - c), -(sl - c), 0) 318 | s2.simdPosition += float3(sl / 2 - c, -(sl - c), 0) 319 | s3.simdPosition += float3(-sl, -sl / 2, 0) 320 | s4.simdPosition += float3(sl, -sl / 2, 0) 321 | s5.simdPosition += float3(-sl, sl / 2, 0) 322 | s6.simdPosition += float3(sl, sl / 2, 0) 323 | s7.simdPosition += float3(-(sl / 2 - c), sl - c, 0) 324 | s8.simdPosition += float3(sl / 2 - c, sl - c, 0) 325 | 326 | let planeNode = SCNNode() 327 | planeNode.eulerAngles.x = .pi / 2 // Horizontal 328 | planeNode.setUniformScale(focusSquareSize * scaleForClosedSquare) 329 | planeNode.addChildNode(s1) 330 | planeNode.addChildNode(s2) 331 | planeNode.addChildNode(s3) 332 | planeNode.addChildNode(s4) 333 | planeNode.addChildNode(s5) 334 | planeNode.addChildNode(s6) 335 | planeNode.addChildNode(s7) 336 | planeNode.addChildNode(s8) 337 | planeNode.addChildNode(fillPlane) 338 | segments = [s1, s2, s3, s4, s5, s6, s7, s8] 339 | isOpen = false 340 | 341 | // Always render focus square on top 342 | planeNode.renderOnTop(true) 343 | 344 | return planeNode 345 | }() 346 | } 347 | 348 | -------------------------------------------------------------------------------- /ARPaint/UI Elements/Focus Squares/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 | enum Alignment { 30 | case horizontal // s1, s2, s7, s8 31 | case vertical // s3, s4, s5, s6 32 | } 33 | enum Direction { 34 | case up, down, left, right 35 | 36 | var reversed: Direction { 37 | switch self { 38 | case .up: return .down 39 | case .down: return .up 40 | case .left: return .right 41 | case .right: return .left 42 | } 43 | } 44 | } 45 | 46 | class Segment: SCNNode { 47 | 48 | // MARK: - Configuration & Initialization 49 | 50 | /// Thickness of the focus square lines in m. 51 | static let thickness: Float = 0.018 52 | 53 | /// Length of the focus square lines in m. 54 | static let length: Float = 0.5 // segment length 55 | 56 | /// Side length of the focus square segments when it is open (w.r.t. to a 1x1 square). 57 | static let openLength: Float = 0.2 58 | 59 | let corner: Corner 60 | let alignment: Alignment 61 | 62 | init(name: String, corner: Corner, alignment: Alignment) { 63 | self.corner = corner 64 | self.alignment = alignment 65 | super.init() 66 | self.name = name 67 | 68 | switch alignment { 69 | case .vertical: 70 | geometry = SCNPlane(width: CGFloat(FocusSquare.Segment.thickness), 71 | height: CGFloat(FocusSquare.Segment.length)) 72 | case .horizontal: 73 | geometry = SCNPlane(width: CGFloat(FocusSquare.Segment.length), 74 | height: CGFloat(FocusSquare.Segment.thickness)) 75 | } 76 | 77 | let material = geometry!.firstMaterial! 78 | material.diffuse.contents = FocusSquare.primaryColor 79 | material.isDoubleSided = true 80 | material.ambient.contents = UIColor.black 81 | material.lightingModel = .constant 82 | material.emission.contents = FocusSquare.primaryColor 83 | } 84 | 85 | required init?(coder aDecoder: NSCoder) { 86 | fatalError("init(coder:) has not been implemented") 87 | } 88 | 89 | // MARK: - Animating Open/Closed 90 | 91 | var openDirection: Direction { 92 | switch (corner, alignment) { 93 | case (.topLeft, .horizontal): return .left 94 | case (.topLeft, .vertical): return .up 95 | case (.topRight, .horizontal): return .right 96 | case (.topRight, .vertical): return .up 97 | case (.bottomLeft, .horizontal): return .left 98 | case (.bottomLeft, .vertical): return .down 99 | case (.bottomRight, .horizontal): return .right 100 | case (.bottomRight, .vertical): return .down 101 | } 102 | } 103 | 104 | func open() { 105 | guard let plane = self.geometry as? SCNPlane else { return } 106 | let direction = openDirection 107 | 108 | if alignment == .horizontal { 109 | plane.width = CGFloat(FocusSquare.Segment.openLength) 110 | } else { 111 | plane.height = CGFloat(FocusSquare.Segment.openLength) 112 | } 113 | 114 | let offset = FocusSquare.Segment.length / 2 - FocusSquare.Segment.openLength / 2 115 | switch direction { 116 | case .left: self.position.x -= offset 117 | case .right: self.position.x += offset 118 | case .up: self.position.y -= offset 119 | case .down: self.position.y += offset 120 | } 121 | } 122 | 123 | func close() { 124 | guard let plane = self.geometry as? SCNPlane else { return } 125 | let direction = openDirection.reversed 126 | 127 | let oldLength: Float 128 | if alignment == .horizontal { 129 | oldLength = Float(plane.width) 130 | plane.width = CGFloat(FocusSquare.Segment.length) 131 | } else { 132 | oldLength = Float(plane.height) 133 | plane.height = CGFloat(FocusSquare.Segment.length) 134 | } 135 | 136 | let offset = FocusSquare.Segment.length / 2 - oldLength / 2 137 | switch direction { 138 | case .left: self.position.x -= offset 139 | case .right: self.position.x += offset 140 | case .up: self.position.y -= offset 141 | case .down: self.position.y += offset 142 | } 143 | } 144 | 145 | } 146 | } 147 | 148 | -------------------------------------------------------------------------------- /ARPaint/UI Elements/Plane.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | SceneKit node wrapper for plane geometry detected in AR. 6 | */ 7 | 8 | import Foundation 9 | import ARKit 10 | 11 | class Plane: SCNNode { 12 | 13 | // MARK: - Properties 14 | 15 | var anchor: ARPlaneAnchor 16 | var focusSquare: FocusSquare? 17 | 18 | // MARK: - Initialization 19 | 20 | init(_ anchor: ARPlaneAnchor) { 21 | self.anchor = anchor 22 | super.init() 23 | } 24 | 25 | required init?(coder aDecoder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | // MARK: - ARKit 30 | 31 | func update(_ anchor: ARPlaneAnchor) { 32 | self.anchor = anchor 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /ARPaint/Utilities/ARSCNView+HitTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | File info 6 | */ 7 | 8 | import ARKit 9 | 10 | extension ARSCNView { 11 | 12 | // MARK: - Types 13 | 14 | struct HitTestRay { 15 | let origin: float3 16 | let direction: float3 17 | } 18 | 19 | struct FeatureHitTestResult { 20 | let position: float3 21 | let distanceToRayOrigin: Float 22 | let featureHit: float3 23 | let featureDistanceToHitResult: Float 24 | } 25 | 26 | func unprojectPoint(_ point: float3) -> float3 { 27 | return float3(self.unprojectPoint(SCNVector3(point))) 28 | } 29 | 30 | // MARK: - Hit Tests 31 | 32 | func hitTestRayFromScreenPos(_ point: CGPoint) -> HitTestRay? { 33 | 34 | guard let frame = self.session.currentFrame else { 35 | return nil 36 | } 37 | 38 | let cameraPos = frame.camera.transform.translation 39 | 40 | // Note: z: 1.0 will unproject() the screen position to the far clipping plane. 41 | let positionVec = float3(x: Float(point.x), y: Float(point.y), z: 1.0) 42 | let screenPosOnFarClippingPlane = self.unprojectPoint(positionVec) 43 | 44 | let rayDirection = simd_normalize(screenPosOnFarClippingPlane - cameraPos) 45 | return HitTestRay(origin: cameraPos, direction: rayDirection) 46 | } 47 | 48 | func hitTestWithInfiniteHorizontalPlane(_ point: CGPoint, _ pointOnPlane: float3) -> float3? { 49 | 50 | guard let ray = hitTestRayFromScreenPos(point) else { 51 | return nil 52 | } 53 | 54 | // Do not intersect with planes above the camera or if the ray is almost parallel to the plane. 55 | if ray.direction.y > -0.03 { 56 | return nil 57 | } 58 | 59 | // Return the intersection of a ray from the camera through the screen position with a horizontal plane 60 | // at height (Y axis). 61 | return rayIntersectionWithHorizontalPlane(rayOrigin: ray.origin, direction: ray.direction, planeY: pointOnPlane.y) 62 | } 63 | 64 | func hitTestWithFeatures(_ point: CGPoint, coneOpeningAngleInDegrees: Float, 65 | minDistance: Float = 0, 66 | maxDistance: Float = Float.greatestFiniteMagnitude, 67 | maxResults: Int = 1) -> [FeatureHitTestResult] { 68 | 69 | var results = [FeatureHitTestResult]() 70 | 71 | guard let features = self.session.currentFrame?.rawFeaturePoints else { 72 | return results 73 | } 74 | 75 | guard let ray = hitTestRayFromScreenPos(point) else { 76 | return results 77 | } 78 | 79 | let maxAngleInDeg = min(coneOpeningAngleInDegrees, 360) / 2 80 | let maxAngle = (maxAngleInDeg / 180) * .pi 81 | 82 | let points = features.__points 83 | 84 | for i in 0...features.__count { 85 | 86 | let feature = points.advanced(by: Int(i)) 87 | let featurePos = feature.pointee 88 | 89 | let originToFeature = featurePos - ray.origin 90 | 91 | let crossProduct = simd_cross(originToFeature, ray.direction) 92 | let featureDistanceFromResult = simd_length(crossProduct) 93 | 94 | let hitTestResult = ray.origin + (ray.direction * simd_dot(ray.direction, originToFeature)) 95 | let hitTestResultDistance = simd_length(hitTestResult - ray.origin) 96 | 97 | if hitTestResultDistance < minDistance || hitTestResultDistance > maxDistance { 98 | // Skip this feature - it is too close or too far away. 99 | continue 100 | } 101 | 102 | let originToFeatureNormalized = simd_normalize(originToFeature) 103 | let angleBetweenRayAndFeature = acos(simd_dot(ray.direction, originToFeatureNormalized)) 104 | 105 | if angleBetweenRayAndFeature > maxAngle { 106 | // Skip this feature - is is outside of the hit test cone. 107 | continue 108 | } 109 | 110 | // All tests passed: Add the hit against this feature to the results. 111 | results.append(FeatureHitTestResult(position: hitTestResult, 112 | distanceToRayOrigin: hitTestResultDistance, 113 | featureHit: featurePos, 114 | featureDistanceToHitResult: featureDistanceFromResult)) 115 | } 116 | 117 | // Sort the results by feature distance to the ray. 118 | results = results.sorted(by: { (first, second) -> Bool in 119 | return first.distanceToRayOrigin < second.distanceToRayOrigin 120 | }) 121 | 122 | // Cap the list to maxResults. 123 | var cappedResults = [FeatureHitTestResult]() 124 | var i = 0 125 | while i < maxResults && i < results.count { 126 | cappedResults.append(results[i]) 127 | i += 1 128 | } 129 | 130 | return cappedResults 131 | } 132 | 133 | func hitTestWithFeatures(_ point: CGPoint) -> [FeatureHitTestResult] { 134 | 135 | var results = [FeatureHitTestResult]() 136 | 137 | guard let ray = hitTestRayFromScreenPos(point) else { 138 | return results 139 | } 140 | 141 | if let result = self.hitTestFromOrigin(origin: ray.origin, direction: ray.direction) { 142 | results.append(result) 143 | } 144 | 145 | return results 146 | } 147 | 148 | func hitTestFromOrigin(origin: float3, direction: float3) -> FeatureHitTestResult? { 149 | 150 | guard let features = self.session.currentFrame?.rawFeaturePoints else { 151 | return nil 152 | } 153 | 154 | let points = features.__points 155 | 156 | // Determine the point from the whole point cloud which is closest to the hit test ray. 157 | var closestFeaturePoint = origin 158 | var minDistance = Float.greatestFiniteMagnitude 159 | 160 | for i in 0...features.__count { 161 | let feature = points.advanced(by: Int(i)) 162 | let featurePos = feature.pointee 163 | 164 | let originVector = origin - featurePos 165 | let crossProduct = simd_cross(originVector, direction) 166 | let featureDistanceFromResult = simd_length(crossProduct) 167 | 168 | if featureDistanceFromResult < minDistance { 169 | closestFeaturePoint = featurePos 170 | minDistance = featureDistanceFromResult 171 | } 172 | } 173 | 174 | // Compute the point along the ray that is closest to the selected feature. 175 | let originToFeature = closestFeaturePoint - origin 176 | let hitTestResult = origin + (direction * simd_dot(direction, originToFeature)) 177 | let hitTestResultDistance = simd_length(hitTestResult - origin) 178 | 179 | return FeatureHitTestResult(position: hitTestResult, 180 | distanceToRayOrigin: hitTestResultDistance, 181 | featureHit: closestFeaturePoint, 182 | featureDistanceToHitResult: minDistance) 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /ARPaint/Utilities/SceneExtensions.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | Configures the scene. 6 | */ 7 | 8 | import Foundation 9 | import ARKit 10 | 11 | // MARK: - AR scene view extensions 12 | 13 | extension ARSCNView { 14 | 15 | func setup() { 16 | antialiasingMode = .multisampling4X 17 | automaticallyUpdatesLighting = false 18 | 19 | preferredFramesPerSecond = 60 20 | contentScaleFactor = 1.3 21 | 22 | if let camera = pointOfView?.camera { 23 | camera.wantsHDR = true 24 | camera.wantsExposureAdaptation = true 25 | camera.exposureOffset = -1 26 | camera.minimumExposure = -1 27 | camera.maximumExposure = 3 28 | } 29 | } 30 | } 31 | 32 | // MARK: - Scene extensions 33 | 34 | extension SCNScene { 35 | func enableEnvironmentMapWithIntensity(_ intensity: CGFloat, queue: DispatchQueue) { 36 | queue.async { 37 | if self.lightingEnvironment.contents == nil { 38 | if let environmentMap = UIImage(named: "Models.scnassets/sharedImages/environment_blur.exr") { 39 | self.lightingEnvironment.contents = environmentMap 40 | } 41 | } 42 | self.lightingEnvironment.intensity = intensity 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ARPaint/Utilities/TextManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | Utility class for showing messages above the AR view. 6 | */ 7 | 8 | import Foundation 9 | import ARKit 10 | 11 | enum MessageType { 12 | case trackingStateEscalation 13 | case planeEstimation 14 | case contentPlacement 15 | case focusSquare 16 | } 17 | 18 | extension ARCamera.TrackingState { 19 | var presentationString: String { 20 | switch self { 21 | case .notAvailable: 22 | return "TRACKING UNAVAILABLE" 23 | case .normal: 24 | return "TRACKING NORMAL" 25 | case .limited(let reason): 26 | switch reason { 27 | case .excessiveMotion: 28 | return "TRACKING LIMITED\nToo much camera movement" 29 | case .insufficientFeatures: 30 | return "TRACKING LIMITED\nNot enough surface detail" 31 | case .initializing: 32 | return "Initializing AR Session" 33 | default: 34 | return "" 35 | } 36 | } 37 | } 38 | var recommendation: String? { 39 | switch self { 40 | case .limited(.excessiveMotion): 41 | return "Try slowing down your movement, or reset the session." 42 | case .limited(.insufficientFeatures): 43 | return "Try pointing at a flat surface, or reset the session." 44 | default: 45 | return nil 46 | } 47 | } 48 | } 49 | 50 | class TextManager { 51 | 52 | // MARK: - Properties 53 | 54 | private var viewController: ViewController! 55 | 56 | // Timer for hiding messages 57 | private var messageHideTimer: Timer? 58 | 59 | // Timers for showing scheduled messages 60 | private var focusSquareMessageTimer: Timer? 61 | private var planeEstimationMessageTimer: Timer? 62 | private var contentPlacementMessageTimer: Timer? 63 | 64 | // Timer for tracking state escalation 65 | private var trackingStateFeedbackEscalationTimer: Timer? 66 | 67 | let blurEffectViewTag = 100 68 | var schedulingMessagesBlocked = false 69 | var alertController: UIAlertController? 70 | 71 | // MARK: - Initialization 72 | 73 | init(viewController: ViewController) { 74 | self.viewController = viewController 75 | } 76 | 77 | // MARK: - Message Handling 78 | 79 | func showMessage(_ text: String, autoHide: Bool = true) { 80 | DispatchQueue.main.async { 81 | // cancel any previous hide timer 82 | self.messageHideTimer?.invalidate() 83 | 84 | // set text 85 | self.viewController.messageLabel.text = text 86 | 87 | // make sure status is showing 88 | self.showHideMessage(hide: false, animated: true) 89 | 90 | if autoHide { 91 | // Compute an appropriate amount of time to display the on screen message. 92 | // According to https://en.wikipedia.org/wiki/Words_per_minute, adults read 93 | // about 200 words per minute and the average English word is 5 characters 94 | // long. So 1000 characters per minute / 60 = 15 characters per second. 95 | // We limit the duration to a range of 1-10 seconds. 96 | let charCount = text.characters.count 97 | let displayDuration: TimeInterval = min(10, Double(charCount) / 15.0 + 1.0) 98 | self.messageHideTimer = Timer.scheduledTimer(withTimeInterval: displayDuration, 99 | repeats: false, 100 | block: { [weak self] ( _ ) in 101 | self?.showHideMessage(hide: true, animated: true) 102 | }) 103 | } 104 | } 105 | } 106 | 107 | func scheduleMessage(_ text: String, inSeconds seconds: TimeInterval, messageType: MessageType) { 108 | // Do not schedule a new message if a feedback escalation alert is still on screen. 109 | guard !schedulingMessagesBlocked else { 110 | return 111 | } 112 | 113 | var timer: Timer? 114 | switch messageType { 115 | case .contentPlacement: timer = contentPlacementMessageTimer 116 | case .focusSquare: timer = focusSquareMessageTimer 117 | case .planeEstimation: timer = planeEstimationMessageTimer 118 | case .trackingStateEscalation: timer = trackingStateFeedbackEscalationTimer 119 | } 120 | 121 | if timer != nil { 122 | timer!.invalidate() 123 | timer = nil 124 | } 125 | timer = Timer.scheduledTimer(withTimeInterval: seconds, 126 | repeats: false, 127 | block: { [weak self] ( _ ) in 128 | self?.showMessage(text) 129 | timer?.invalidate() 130 | timer = nil 131 | }) 132 | switch messageType { 133 | case .contentPlacement: contentPlacementMessageTimer = timer 134 | case .focusSquare: focusSquareMessageTimer = timer 135 | case .planeEstimation: planeEstimationMessageTimer = timer 136 | case .trackingStateEscalation: trackingStateFeedbackEscalationTimer = timer 137 | } 138 | } 139 | 140 | func cancelScheduledMessage(forType messageType: MessageType) { 141 | var timer: Timer? 142 | switch messageType { 143 | case .contentPlacement: timer = contentPlacementMessageTimer 144 | case .focusSquare: timer = focusSquareMessageTimer 145 | case .planeEstimation: timer = planeEstimationMessageTimer 146 | case .trackingStateEscalation: timer = trackingStateFeedbackEscalationTimer 147 | } 148 | 149 | if timer != nil { 150 | timer!.invalidate() 151 | timer = nil 152 | } 153 | } 154 | 155 | func cancelAllScheduledMessages() { 156 | cancelScheduledMessage(forType: .contentPlacement) 157 | cancelScheduledMessage(forType: .planeEstimation) 158 | cancelScheduledMessage(forType: .trackingStateEscalation) 159 | cancelScheduledMessage(forType: .focusSquare) 160 | } 161 | 162 | // MARK: - ARKit 163 | 164 | func showTrackingQualityInfo(for trackingState: ARCamera.TrackingState, autoHide: Bool) { 165 | showMessage(trackingState.presentationString, autoHide: autoHide) 166 | } 167 | 168 | func escalateFeedback(for trackingState: ARCamera.TrackingState, inSeconds seconds: TimeInterval) { 169 | if self.trackingStateFeedbackEscalationTimer != nil { 170 | self.trackingStateFeedbackEscalationTimer!.invalidate() 171 | self.trackingStateFeedbackEscalationTimer = nil 172 | } 173 | 174 | self.trackingStateFeedbackEscalationTimer = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false, block: { _ in 175 | self.trackingStateFeedbackEscalationTimer?.invalidate() 176 | self.trackingStateFeedbackEscalationTimer = nil 177 | 178 | if let recommendation = trackingState.recommendation { 179 | self.showMessage(trackingState.presentationString + "\n" + recommendation, autoHide: false) 180 | } else { 181 | self.showMessage(trackingState.presentationString, autoHide: false) 182 | } 183 | }) 184 | } 185 | 186 | // MARK: - Alert View 187 | 188 | func showAlert(title: String, message: String, actions: [UIAlertAction]? = nil) { 189 | alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) 190 | if let actions = actions { 191 | for action in actions { 192 | alertController!.addAction(action) 193 | } 194 | } else { 195 | alertController!.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) 196 | } 197 | DispatchQueue.main.async { 198 | self.viewController.present(self.alertController!, animated: true, completion: nil) 199 | } 200 | } 201 | 202 | func dismissPresentedAlert() { 203 | DispatchQueue.main.async { 204 | self.alertController?.dismiss(animated: true, completion: nil) 205 | } 206 | } 207 | 208 | // MARK: - Background Blur 209 | 210 | func blurBackground() { 211 | let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.light) 212 | let blurEffectView = UIVisualEffectView(effect: blurEffect) 213 | blurEffectView.frame = viewController.view.bounds 214 | blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 215 | blurEffectView.tag = blurEffectViewTag 216 | viewController.view.addSubview(blurEffectView) 217 | } 218 | 219 | func unblurBackground() { 220 | for view in viewController.view.subviews { 221 | if let blurView = view as? UIVisualEffectView, blurView.tag == blurEffectViewTag { 222 | blurView.removeFromSuperview() 223 | } 224 | } 225 | } 226 | 227 | // MARK: - Panel Visibility 228 | 229 | private func showHideMessage(hide: Bool, animated: Bool) { 230 | if !animated { 231 | viewController.messageLabel.isHidden = hide 232 | return 233 | } 234 | 235 | UIView.animate(withDuration: 0.2, 236 | delay: 0, 237 | options: [.allowUserInteraction, .beginFromCurrentState], 238 | animations: { 239 | self.viewController.messageLabel.isHidden = hide 240 | self.updateMessagePanelVisibility() 241 | }, completion: nil) 242 | } 243 | 244 | private func updateMessagePanelVisibility() { 245 | // Show and hide the panel depending whether there is something to show. 246 | viewController.messagePanel.isHidden = viewController.messageLabel.isHidden 247 | } 248 | 249 | } 250 | 251 | -------------------------------------------------------------------------------- /ARPaint/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: - Collection extensions 12 | extension Array where Iterator.Element == Float { 13 | var average: Float? { 14 | guard !self.isEmpty else { 15 | return nil 16 | } 17 | 18 | let sum = self.reduce(Float(0)) { current, next in 19 | return current + next 20 | } 21 | return sum / Float(self.count) 22 | } 23 | } 24 | 25 | extension Array where Iterator.Element == float3 { 26 | var average: float3? { 27 | guard !self.isEmpty else { 28 | return nil 29 | } 30 | 31 | let sum = self.reduce(float3(0)) { current, next in 32 | return current + next 33 | } 34 | return sum / Float(self.count) 35 | } 36 | } 37 | 38 | extension RangeReplaceableCollection where IndexDistance == Int { 39 | mutating func keepLast(_ elementsToKeep: Int) { 40 | if count > elementsToKeep { 41 | self.removeFirst(count - elementsToKeep) 42 | } 43 | } 44 | } 45 | 46 | // MARK: - SCNNode extension 47 | 48 | extension SCNNode { 49 | 50 | func setUniformScale(_ scale: Float) { 51 | self.simdScale = float3(scale, scale, scale) 52 | } 53 | 54 | func renderOnTop(_ enable: Bool) { 55 | self.renderingOrder = enable ? 2 : 0 56 | if let geom = self.geometry { 57 | for material in geom.materials { 58 | material.readsFromDepthBuffer = enable ? false : true 59 | } 60 | } 61 | for child in self.childNodes { 62 | child.renderOnTop(enable) 63 | } 64 | } 65 | } 66 | 67 | // MARK: - float4x4 extensions 68 | 69 | extension float4x4 { 70 | /// Treats matrix as a (right-hand column-major convention) transform matrix 71 | /// and factors out the translation component of the transform. 72 | var translation: float3 { 73 | let translation = self.columns.3 74 | return float3(translation.x, translation.y, translation.z) 75 | } 76 | } 77 | 78 | // MARK: - SCNMaterial extensions 79 | 80 | extension SCNMaterial { 81 | 82 | static func material(withDiffuse diffuse: Any?, respondsToLighting: Bool = true) -> SCNMaterial { 83 | let material = SCNMaterial() 84 | material.diffuse.contents = diffuse 85 | material.isDoubleSided = true 86 | if respondsToLighting { 87 | material.locksAmbientWithDiffuse = true 88 | } else { 89 | material.ambient.contents = UIColor.black 90 | material.lightingModel = .constant 91 | material.emission.contents = diffuse 92 | } 93 | return material 94 | } 95 | } 96 | 97 | // MARK: - CGPoint extensions 98 | 99 | extension CGPoint { 100 | 101 | init(_ size: CGSize) { 102 | self.x = size.width 103 | self.y = size.height 104 | } 105 | 106 | init(_ vector: SCNVector3) { 107 | self.x = CGFloat(vector.x) 108 | self.y = CGFloat(vector.y) 109 | } 110 | 111 | func distanceTo(_ point: CGPoint) -> CGFloat { 112 | return (self - point).length() 113 | } 114 | 115 | func length() -> CGFloat { 116 | return sqrt(self.x * self.x + self.y * self.y) 117 | } 118 | 119 | func midpoint(_ point: CGPoint) -> CGPoint { 120 | return (self + point) / 2 121 | } 122 | static func + (left: CGPoint, right: CGPoint) -> CGPoint { 123 | return CGPoint(x: left.x + right.x, y: left.y + right.y) 124 | } 125 | 126 | static func - (left: CGPoint, right: CGPoint) -> CGPoint { 127 | return CGPoint(x: left.x - right.x, y: left.y - right.y) 128 | } 129 | 130 | static func += (left: inout CGPoint, right: CGPoint) { 131 | left = left + right 132 | } 133 | 134 | static func -= (left: inout CGPoint, right: CGPoint) { 135 | left = left - right 136 | } 137 | 138 | static func / (left: CGPoint, right: CGFloat) -> CGPoint { 139 | return CGPoint(x: left.x / right, y: left.y / right) 140 | } 141 | 142 | static func * (left: CGPoint, right: CGFloat) -> CGPoint { 143 | return CGPoint(x: left.x * right, y: left.y * right) 144 | } 145 | 146 | static func /= (left: inout CGPoint, right: CGFloat) { 147 | left = left / right 148 | } 149 | 150 | static func *= (left: inout CGPoint, right: CGFloat) { 151 | left = left * right 152 | } 153 | } 154 | 155 | // MARK: - CGSize extensions 156 | 157 | extension CGSize { 158 | init(_ point: CGPoint) { 159 | self.width = point.x 160 | self.height = point.y 161 | } 162 | 163 | static func + (left: CGSize, right: CGSize) -> CGSize { 164 | return CGSize(width: left.width + right.width, height: left.height + right.height) 165 | } 166 | 167 | static func - (left: CGSize, right: CGSize) -> CGSize { 168 | return CGSize(width: left.width - right.width, height: left.height - right.height) 169 | } 170 | 171 | static func += (left: inout CGSize, right: CGSize) { 172 | left = left + right 173 | } 174 | 175 | static func -= (left: inout CGSize, right: CGSize) { 176 | left = left - right 177 | } 178 | 179 | static func / (left: CGSize, right: CGFloat) -> CGSize { 180 | return CGSize(width: left.width / right, height: left.height / right) 181 | } 182 | 183 | static func * (left: CGSize, right: CGFloat) -> CGSize { 184 | return CGSize(width: left.width * right, height: left.height * right) 185 | } 186 | 187 | static func /= (left: inout CGSize, right: CGFloat) { 188 | left = left / right 189 | } 190 | 191 | static func *= (left: inout CGSize, right: CGFloat) { 192 | left = left * right 193 | } 194 | } 195 | 196 | // MARK: - CGRect extensions 197 | 198 | extension CGRect { 199 | var mid: CGPoint { 200 | return CGPoint(x: midX, y: midY) 201 | } 202 | } 203 | 204 | func rayIntersectionWithHorizontalPlane(rayOrigin: float3, direction: float3, planeY: Float) -> float3? { 205 | 206 | let direction = simd_normalize(direction) 207 | 208 | // Special case handling: Check if the ray is horizontal as well. 209 | if direction.y == 0 { 210 | if rayOrigin.y == planeY { 211 | // The ray is horizontal and on the plane, thus all points on the ray intersect with the plane. 212 | // Therefore we simply return the ray origin. 213 | return rayOrigin 214 | } else { 215 | // The ray is parallel to the plane and never intersects. 216 | return nil 217 | } 218 | } 219 | 220 | // The distance from the ray's origin to the intersection point on the plane is: 221 | // (pointOnPlane - rayOrigin) dot planeNormal 222 | // -------------------------------------------- 223 | // direction dot planeNormal 224 | 225 | // Since we know that horizontal planes have normal (0, 1, 0), we can simplify this to: 226 | let dist = (planeY - rayOrigin.y) / direction.y 227 | 228 | // Do not return intersections behind the ray's origin. 229 | if dist < 0 { 230 | return nil 231 | } 232 | 233 | // Return the intersection point. 234 | return rayOrigin + (direction * dist) 235 | } 236 | 237 | 238 | // MARK: - float3 extensions 239 | 240 | extension float3 { 241 | func length() -> Float { 242 | return sqrtf(x * x + y * y + z * z) 243 | } 244 | 245 | static func + (left: float3, right: float3) -> float3 { 246 | return float3(left.x + right.x, left.y + right.y, left.z + right.z) 247 | } 248 | 249 | static func - (left: float3, right: float3) -> float3 { 250 | return float3(left.x - right.x, left.y - right.y, left.z - right.z) 251 | } 252 | 253 | } 254 | 255 | // MARK: - SCNVector3 extensions 256 | 257 | extension SCNVector3 { 258 | 259 | func length() -> Float { 260 | return sqrtf(x * x + y * y + z * z) 261 | } 262 | 263 | static func + (left: SCNVector3, right: SCNVector3) -> SCNVector3 { 264 | return SCNVector3Make(left.x + right.x, left.y + right.y, left.z + right.z) 265 | } 266 | 267 | static func - (left: SCNVector3, right: SCNVector3) -> SCNVector3 { 268 | return SCNVector3Make(left.x - right.x, left.y - right.y, left.z - right.z) 269 | } 270 | 271 | } 272 | 273 | 274 | -------------------------------------------------------------------------------- /ARPaint/ViewController+Actions.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | File info 6 | */ 7 | 8 | import UIKit 9 | import SceneKit 10 | 11 | extension ViewController { 12 | 13 | enum SegueIdentifier: String { 14 | case showSettings 15 | } 16 | 17 | // MARK: - Interface Actions 18 | 19 | @IBAction func restartExperience(_ sender: Any) { 20 | 21 | guard restartExperienceButtonIsEnabled else { return } 22 | 23 | DispatchQueue.main.async { 24 | self.restartExperienceButtonIsEnabled = false 25 | 26 | self.textManager.cancelAllScheduledMessages() 27 | self.textManager.dismissPresentedAlert() 28 | self.textManager.showMessage("STARTING A NEW SESSION") 29 | 30 | self.virtualObjectManager.removeAllVirtualObjects() 31 | self.focusSquare?.isHidden = true 32 | 33 | self.resetTracking() 34 | 35 | self.restartExperienceButton.setImage(#imageLiteral(resourceName: "restart"), for: []) 36 | 37 | // Show the focus square after a short delay to ensure all plane anchors have been deleted. 38 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { 39 | self.setupFocusSquare() 40 | }) 41 | 42 | // Disable Restart button for a while in order to give the session enough time to restart. 43 | DispatchQueue.main.asyncAfter(deadline: .now() + 5.0, execute: { 44 | self.restartExperienceButtonIsEnabled = true 45 | }) 46 | } 47 | } 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /ARPaint/ViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | Main view controller for the AR experience. 6 | */ 7 | 8 | import ARKit 9 | import Foundation 10 | import SceneKit 11 | import UIKit 12 | import Vision 13 | 14 | class ViewController: UIViewController, ARSCNViewDelegate { 15 | 16 | // MARK: - ARKit Config Properties 17 | 18 | var screenCenter: CGPoint? 19 | var trackingFallbackTimer: Timer? 20 | 21 | let session = ARSession() 22 | 23 | let standardConfiguration: ARWorldTrackingConfiguration = { 24 | let configuration = ARWorldTrackingConfiguration() 25 | configuration.planeDetection = .horizontal 26 | return configuration 27 | }() 28 | 29 | // MARK: - Virtual Object Manipulation Properties 30 | 31 | var dragOnInfinitePlanesEnabled = false 32 | var virtualObjectManager: VirtualObjectManager! 33 | 34 | // MARK: - Other Properties 35 | 36 | var textManager: TextManager! 37 | var restartExperienceButtonIsEnabled = true 38 | 39 | // MARK: - UI Elements 40 | 41 | var spinner: UIActivityIndicatorView? 42 | 43 | @IBOutlet var sceneView: ARSCNView! 44 | @IBOutlet weak var messagePanel: UIView! 45 | @IBOutlet weak var messageLabel: UILabel! 46 | @IBOutlet weak var restartExperienceButton: UIButton! 47 | 48 | @IBOutlet weak var drawButton: UIButton! 49 | @IBAction func drawAction() { 50 | drawButton.isSelected = !drawButton.isSelected 51 | inDrawMode = drawButton.isSelected 52 | in3DMode = false 53 | } 54 | 55 | @IBOutlet weak var threeDMagicButton: UIButton! 56 | @IBAction func threeDMagicAction(_ button: UIButton) { 57 | threeDMagicButton.isSelected = !threeDMagicButton.isSelected 58 | in3DMode = threeDMagicButton.isSelected 59 | inDrawMode = false 60 | 61 | trackImageInitialOrigin = nil 62 | } 63 | 64 | // MARK: - Queues 65 | 66 | static let serialQueue = DispatchQueue(label: "com.apple.arkitexample.serialSceneKitQueue") 67 | // Create instance variable for more readable access inside class 68 | let serialQueue: DispatchQueue = ViewController.serialQueue 69 | 70 | // MARK: - View Controller Life Cycle 71 | 72 | override func viewDidLoad() { 73 | super.viewDidLoad() 74 | 75 | setupUIControls() 76 | setupScene() 77 | 78 | let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapAction)) 79 | view.addGestureRecognizer(tapGestureRecognizer) 80 | } 81 | 82 | override func viewDidAppear(_ animated: Bool) { 83 | super.viewDidAppear(animated) 84 | 85 | // Prevent the screen from being dimmed after a while. 86 | UIApplication.shared.isIdleTimerDisabled = true 87 | 88 | if ARWorldTrackingConfiguration.isSupported { 89 | // Start the ARSession. 90 | resetTracking() 91 | } else { 92 | // This device does not support 6DOF world tracking. 93 | let sessionErrorMsg = "This app requires world tracking. World tracking is only available on iOS devices with A9 processor or newer. " + 94 | "Please quit the application." 95 | displayErrorMessage(title: "Unsupported platform", message: sessionErrorMsg, allowRestart: false) 96 | } 97 | } 98 | 99 | override func viewWillDisappear(_ animated: Bool) { 100 | super.viewWillDisappear(animated) 101 | session.pause() 102 | } 103 | 104 | // MARK: - Setup 105 | 106 | func setupScene() { 107 | virtualObjectManager = VirtualObjectManager() 108 | 109 | // set up scene view 110 | sceneView.setup() 111 | sceneView.delegate = self 112 | sceneView.session = session 113 | // sceneView.showsStatistics = true 114 | 115 | sceneView.scene.enableEnvironmentMapWithIntensity(25, queue: serialQueue) 116 | 117 | setupFocusSquare() 118 | 119 | DispatchQueue.main.async { 120 | self.screenCenter = self.sceneView.bounds.mid 121 | } 122 | } 123 | 124 | func setupUIControls() { 125 | textManager = TextManager(viewController: self) 126 | 127 | // Set appearance of message output panel 128 | messagePanel.layer.cornerRadius = 3.0 129 | messagePanel.clipsToBounds = true 130 | messagePanel.isHidden = true 131 | messageLabel.text = "" 132 | } 133 | 134 | // MARK: - ARSCNViewDelegate 135 | 136 | func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { 137 | updateFocusSquare() 138 | 139 | // If light estimation is enabled, update the intensity of the model's lights and the environment map 140 | if let lightEstimate = self.session.currentFrame?.lightEstimate { 141 | self.sceneView.scene.enableEnvironmentMapWithIntensity(lightEstimate.ambientIntensity / 40, queue: serialQueue) 142 | } else { 143 | self.sceneView.scene.enableEnvironmentMapWithIntensity(40, queue: serialQueue) 144 | } 145 | 146 | 147 | // Setup a dot that represents the virtual pen's tippoint 148 | if (self.virtualPenTip == nil) { 149 | self.virtualPenTip = PointNode(color: UIColor.red) 150 | self.sceneView.scene.rootNode.addChildNode(self.virtualPenTip!) 151 | } 152 | 153 | // Track the thumbnail 154 | guard let pixelBuffer = self.sceneView.session.currentFrame?.capturedImage, 155 | let observation = self.lastObservation else { 156 | return 157 | } 158 | let request = VNTrackObjectRequest(detectedObjectObservation: observation) { [unowned self] request, error in 159 | self.handle(request, error: error) 160 | } 161 | request.trackingLevel = .accurate 162 | do { 163 | try self.handler.perform([request], on: pixelBuffer) 164 | } 165 | catch { 166 | print(error) 167 | } 168 | 169 | // Draw 170 | if let lastFingerWorldPos = self.lastFingerWorldPos { 171 | 172 | // Update virtual pen position 173 | self.virtualPenTip?.isHidden = false 174 | self.virtualPenTip?.simdPosition = lastFingerWorldPos 175 | 176 | // Draw new point 177 | if (self.inDrawMode && !self.virtualObjectManager.pointNodeExistAt(pos: lastFingerWorldPos)){ 178 | let newPoint = PointNode() 179 | self.sceneView.scene.rootNode.addChildNode(newPoint) 180 | self.virtualObjectManager.loadVirtualObject(newPoint, to: lastFingerWorldPos) 181 | } 182 | 183 | // Convert drawing to 3D 184 | if (self.in3DMode ) { 185 | if self.trackImageInitialOrigin != nil { 186 | DispatchQueue.main.async { 187 | let newH = 0.4 * (self.trackImageInitialOrigin!.y - self.trackImageBoundingBox!.origin.y) / self.sceneView.frame.height 188 | self.virtualObjectManager.setNewHeight(newHeight: newH) 189 | } 190 | } 191 | else { 192 | self.trackImageInitialOrigin = self.trackImageBoundingBox?.origin 193 | } 194 | } 195 | 196 | } 197 | 198 | } 199 | 200 | func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { 201 | if let planeAnchor = anchor as? ARPlaneAnchor { 202 | serialQueue.async { 203 | self.addPlane(node: node, anchor: planeAnchor) 204 | self.virtualObjectManager.checkIfObjectShouldMoveOntoPlane(anchor: planeAnchor, planeAnchorNode: node) 205 | } 206 | } 207 | } 208 | 209 | func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { 210 | if let planeAnchor = anchor as? ARPlaneAnchor { 211 | serialQueue.async { 212 | self.updatePlane(anchor: planeAnchor) 213 | self.virtualObjectManager.checkIfObjectShouldMoveOntoPlane(anchor: planeAnchor, planeAnchorNode: node) 214 | } 215 | } 216 | } 217 | 218 | func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor) { 219 | if let planeAnchor = anchor as? ARPlaneAnchor { 220 | serialQueue.async { 221 | self.removePlane(anchor: planeAnchor) 222 | } 223 | } 224 | } 225 | 226 | func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { 227 | textManager.showTrackingQualityInfo(for: camera.trackingState, autoHide: true) 228 | 229 | switch camera.trackingState { 230 | case .notAvailable: 231 | fallthrough 232 | case .limited: 233 | textManager.escalateFeedback(for: camera.trackingState, inSeconds: 3.0) 234 | case .normal: 235 | textManager.cancelScheduledMessage(forType: .trackingStateEscalation) 236 | } 237 | } 238 | 239 | func session(_ session: ARSession, didFailWithError error: Error) { 240 | 241 | guard let arError = error as? ARError else { return } 242 | 243 | let nsError = error as NSError 244 | var sessionErrorMsg = "\(nsError.localizedDescription) \(nsError.localizedFailureReason ?? "")" 245 | if let recoveryOptions = nsError.localizedRecoveryOptions { 246 | for option in recoveryOptions { 247 | sessionErrorMsg.append("\(option).") 248 | } 249 | } 250 | 251 | let isRecoverable = (arError.code == .worldTrackingFailed) 252 | if isRecoverable { 253 | sessionErrorMsg += "\nYou can try resetting the session or quit the application." 254 | } else { 255 | sessionErrorMsg += "\nThis is an unrecoverable error that requires to quit the application." 256 | } 257 | 258 | displayErrorMessage(title: "We're sorry!", message: sessionErrorMsg, allowRestart: isRecoverable) 259 | } 260 | 261 | func sessionWasInterrupted(_ session: ARSession) { 262 | textManager.blurBackground() 263 | textManager.showAlert(title: "Session Interrupted", message: "The session will be reset after the interruption has ended.") 264 | } 265 | 266 | func sessionInterruptionEnded(_ session: ARSession) { 267 | textManager.unblurBackground() 268 | session.run(standardConfiguration, options: [.resetTracking, .removeExistingAnchors]) 269 | restartExperience(self) 270 | textManager.showMessage("RESETTING SESSION") 271 | } 272 | 273 | // MARK: - Planes 274 | 275 | var planes = [ARPlaneAnchor: Plane]() 276 | 277 | func addPlane(node: SCNNode, anchor: ARPlaneAnchor) { 278 | 279 | let plane = Plane(anchor) 280 | planes[anchor] = plane 281 | node.addChildNode(plane) 282 | 283 | textManager.cancelScheduledMessage(forType: .planeEstimation) 284 | textManager.showMessage("SURFACE DETECTED") 285 | if virtualObjectManager.pointNodes.isEmpty { 286 | textManager.scheduleMessage("TAP + TO PLACE AN OBJECT", inSeconds: 7.5, messageType: .contentPlacement) 287 | } 288 | } 289 | 290 | func updatePlane(anchor: ARPlaneAnchor) { 291 | if let plane = planes[anchor] { 292 | plane.update(anchor) 293 | } 294 | } 295 | 296 | func removePlane(anchor: ARPlaneAnchor) { 297 | if let plane = planes.removeValue(forKey: anchor) { 298 | plane.removeFromParentNode() 299 | } 300 | } 301 | 302 | func resetTracking() { 303 | session.run(standardConfiguration, options: [.resetTracking, .removeExistingAnchors]) 304 | 305 | textManager.scheduleMessage("FIND A SURFACE TO PLACE AN OBJECT", 306 | inSeconds: 7.5, 307 | messageType: .planeEstimation) 308 | 309 | trackImageInitialOrigin = nil 310 | inDrawMode = false 311 | in3DMode = false 312 | lastFingerWorldPos = nil 313 | drawButton.isSelected = false 314 | threeDMagicButton.isSelected = false 315 | self.virtualPenTip?.isHidden = true 316 | 317 | } 318 | 319 | // MARK: - Focus Square 320 | 321 | var focusSquare: FocusSquare? 322 | 323 | func setupFocusSquare() { 324 | serialQueue.async { 325 | self.focusSquare?.isHidden = true 326 | self.focusSquare?.removeFromParentNode() 327 | self.focusSquare = FocusSquare() 328 | self.sceneView.scene.rootNode.addChildNode(self.focusSquare!) 329 | } 330 | 331 | textManager.scheduleMessage("TRY MOVING LEFT OR RIGHT", inSeconds: 5.0, messageType: .focusSquare) 332 | } 333 | 334 | func updateFocusSquare() { 335 | guard let screenCenter = screenCenter else { return } 336 | 337 | DispatchQueue.main.async { 338 | if self.virtualObjectManager.pointNodes.count > 0 { 339 | self.focusSquare?.hide() 340 | } else { 341 | self.focusSquare?.unhide() 342 | } 343 | 344 | let (worldPos, planeAnchor, _) = self.virtualObjectManager.worldPositionFromScreenPosition(screenCenter, 345 | in: self.sceneView, 346 | objectPos: self.focusSquare?.simdPosition) 347 | if let worldPos = worldPos { 348 | self.serialQueue.async { 349 | self.focusSquare?.update(for: worldPos, planeAnchor: planeAnchor, camera: self.session.currentFrame?.camera) 350 | } 351 | self.textManager.cancelScheduledMessage(forType: .focusSquare) 352 | } 353 | } 354 | } 355 | 356 | // MARK: - Error handling 357 | 358 | func displayErrorMessage(title: String, message: String, allowRestart: Bool = false) { 359 | // Blur the background. 360 | textManager.blurBackground() 361 | 362 | if allowRestart { 363 | // Present an alert informing about the error that has occurred. 364 | let restartAction = UIAlertAction(title: "Reset", style: .default) { _ in 365 | self.textManager.unblurBackground() 366 | self.restartExperience(self) 367 | } 368 | textManager.showAlert(title: title, message: message, actions: [restartAction]) 369 | } else { 370 | textManager.showAlert(title: title, message: message, actions: []) 371 | } 372 | } 373 | 374 | // MARK: - ARPaint methods 375 | 376 | var inDrawMode = false 377 | var in3DMode = false 378 | var lastFingerWorldPos: float3? 379 | 380 | var virtualPenTip: PointNode? 381 | 382 | 383 | // MARK: Object tracking 384 | 385 | private var handler = VNSequenceRequestHandler() 386 | fileprivate var lastObservation: VNDetectedObjectObservation? 387 | var trackImageBoundingBox: CGRect? 388 | var trackImageInitialOrigin: CGPoint? 389 | let trackImageSize = CGFloat(20) 390 | 391 | @objc private func tapAction(recognizer: UITapGestureRecognizer) { 392 | 393 | handler = VNSequenceRequestHandler() 394 | 395 | lastObservation = nil 396 | let tapLocation = recognizer.location(in: view) 397 | 398 | // Set up the rect in the image in view coordinate space that we will track 399 | let trackImageBoundingBoxOrigin = CGPoint(x: tapLocation.x - trackImageSize / 2, y: tapLocation.y - trackImageSize / 2) 400 | trackImageBoundingBox = CGRect(origin: trackImageBoundingBoxOrigin, size: CGSize(width: trackImageSize, height: trackImageSize)) 401 | 402 | let t = CGAffineTransform(scaleX: 1.0 / self.view.frame.size.width, y: 1.0 / self.view.frame.size.height) 403 | let normalizedTrackImageBoundingBox = trackImageBoundingBox!.applying(t) 404 | 405 | // Transfrom the rect from view space to image space 406 | guard let fromViewToCameraImageTransform = self.sceneView.session.currentFrame?.displayTransform(for: UIInterfaceOrientation.portrait, viewportSize: self.sceneView.frame.size).inverted() else { 407 | return 408 | } 409 | var trackImageBoundingBoxInImage = normalizedTrackImageBoundingBox.applying(fromViewToCameraImageTransform) 410 | trackImageBoundingBoxInImage.origin.y = 1 - trackImageBoundingBoxInImage.origin.y // Image space uses bottom left as origin while view space uses top left 411 | 412 | lastObservation = VNDetectedObjectObservation(boundingBox: trackImageBoundingBoxInImage) 413 | 414 | } 415 | 416 | fileprivate func handle(_ request: VNRequest, error: Error?) { 417 | DispatchQueue.main.async { 418 | guard let newObservation = request.results?.first as? VNDetectedObjectObservation else { 419 | return 420 | } 421 | self.lastObservation = newObservation 422 | 423 | // check the confidence level before updating the UI 424 | guard newObservation.confidence >= 0.3 else { 425 | // hide the pen when we lose accuracy so the user knows something is wrong 426 | self.virtualPenTip?.isHidden = true 427 | self.lastObservation = nil 428 | return 429 | } 430 | 431 | var trackImageBoundingBoxInImage = newObservation.boundingBox 432 | 433 | // Transfrom the rect from image space to view space 434 | trackImageBoundingBoxInImage.origin.y = 1 - trackImageBoundingBoxInImage.origin.y 435 | guard let fromCameraImageToViewTransform = self.sceneView.session.currentFrame?.displayTransform(for: UIInterfaceOrientation.portrait, viewportSize: self.sceneView.frame.size) else { 436 | return 437 | } 438 | let normalizedTrackImageBoundingBox = trackImageBoundingBoxInImage.applying(fromCameraImageToViewTransform) 439 | let t = CGAffineTransform(scaleX: self.view.frame.size.width, y: self.view.frame.size.height) 440 | let unnormalizedTrackImageBoundingBox = normalizedTrackImageBoundingBox.applying(t) 441 | self.trackImageBoundingBox = unnormalizedTrackImageBoundingBox 442 | 443 | // Get the projection if the location of the tracked image from image space to the nearest detected plane 444 | if let trackImageOrigin = self.trackImageBoundingBox?.origin { 445 | (self.lastFingerWorldPos, _, _) = self.virtualObjectManager.worldPositionFromScreenPosition(CGPoint(x: trackImageOrigin.x - 20.0, y: trackImageOrigin.y + 40.0), in: self.sceneView, objectPos: nil, infinitePlane: false) 446 | } 447 | 448 | } 449 | } 450 | 451 | 452 | 453 | 454 | 455 | } 456 | -------------------------------------------------------------------------------- /ARPaint/Virtual Objects/PointNode.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | The virtual chair. 6 | */ 7 | 8 | import Foundation 9 | import SceneKit 10 | 11 | let POINT_SIZE = CGFloat(0.003) 12 | let POINT_HEIGHT = CGFloat(0.00001) 13 | 14 | class PointNode: SCNNode { 15 | 16 | static var boxGeo: SCNBox? 17 | 18 | override init() { 19 | super.init() 20 | 21 | if PointNode.boxGeo == nil { 22 | PointNode.boxGeo = SCNBox(width: POINT_SIZE, height: POINT_HEIGHT, length: POINT_SIZE, chamferRadius: 0.001) 23 | 24 | // Setup the material of the point 25 | let material = PointNode.boxGeo!.firstMaterial 26 | material?.lightingModel = SCNMaterial.LightingModel.blinn 27 | material?.diffuse.contents = UIImage(named: "wood-diffuse.jpg") 28 | material?.normal.contents = UIImage(named: "wood-normal.png") 29 | material?.specular.contents = UIImage(named: "wood-specular.jpg") 30 | } 31 | 32 | let object = SCNNode(geometry: PointNode.boxGeo!) 33 | object.transform = SCNMatrix4MakeTranslation(0.0, Float(POINT_HEIGHT) / 2.0, 0.0) 34 | 35 | self.addChildNode(object) 36 | 37 | } 38 | 39 | init(color: UIColor) { 40 | super.init() 41 | 42 | let boxGeo = SCNBox(width: POINT_SIZE, height: POINT_HEIGHT * 2.0, length: POINT_SIZE, chamferRadius: 0.001) 43 | boxGeo.firstMaterial?.diffuse.contents = UIColor.red 44 | 45 | let object = SCNNode(geometry: boxGeo) 46 | object.transform = SCNMatrix4MakeTranslation(0.0, Float(POINT_HEIGHT * 2.0) / 2.0, 0.0) 47 | 48 | self.addChildNode(object) 49 | 50 | } 51 | 52 | func setNewHeight(newHeight: CGFloat) { 53 | PointNode.boxGeo?.height = newHeight 54 | let firstChild = self.childNodes[0] 55 | firstChild.transform = SCNMatrix4MakeTranslation(0.0, Float(newHeight / 2.0), 0.0) 56 | } 57 | 58 | func resetHeight() { 59 | PointNode.boxGeo?.height = POINT_HEIGHT 60 | let firstChild = self.childNodes[0] 61 | firstChild.transform = SCNMatrix4MakeTranslation(0.0, Float(POINT_HEIGHT / 2.0), 0.0) 62 | } 63 | 64 | func getChildBoundingBox() -> (v1: SCNVector3, v2: SCNVector3) { 65 | let firstChild = self.childNodes[0] 66 | return (firstChild.boundingBox.max, firstChild.boundingBox.min) 67 | } 68 | 69 | required init?(coder aDecoder: NSCoder) { 70 | fatalError("init(coder:) has not been implemented") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ARPaint/Virtual Objects/VirtualObjectManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | A type which controls the manipulation of virtual objects. 6 | */ 7 | 8 | import Foundation 9 | import ARKit 10 | 11 | class VirtualObjectManager { 12 | 13 | weak var delegate: VirtualObjectManagerDelegate? 14 | 15 | var pointNodes = [PointNode]() 16 | 17 | func removeAllVirtualObjects() { 18 | for object in pointNodes { 19 | unloadVirtualObject(object) 20 | } 21 | pointNodes.removeAll() 22 | } 23 | 24 | private func unloadVirtualObject(_ object: PointNode) { 25 | ViewController.serialQueue.async { 26 | object.removeFromParentNode() 27 | } 28 | } 29 | 30 | // MARK: - Loading object 31 | 32 | func loadVirtualObject(_ object: PointNode, to position: float3) { 33 | self.pointNodes.append(object) 34 | self.delegate?.virtualObjectManager(self, willLoad: object) 35 | object.simdPosition = position 36 | } 37 | 38 | func pointNodeExistAt(pos: float3) -> Bool { 39 | 40 | if (pointNodes.count == 0) { 41 | return false 42 | } 43 | 44 | let (v1, v2) = pointNodes[0].getChildBoundingBox() 45 | 46 | let nodeLengthInWorld = ( 47 | pointNodes[0].convertPosition( 48 | SCNVector3(v1.x - v2.x, 0, v1.z - v2.z), to: pointNodes[0].parent) 49 | - pointNodes[0].convertPosition( SCNVector3(0, 0, 0), to: pointNodes[0].parent) 50 | ).length() 51 | 52 | 53 | for point in pointNodes { 54 | let distance = (point.simdPosition - pos).length() 55 | if (distance < (nodeLengthInWorld / 2.0)){ 56 | return true 57 | } 58 | } 59 | 60 | return false 61 | } 62 | 63 | func setNewHeight(newHeight: CGFloat) { 64 | if (newHeight > 0) { 65 | for pointNode in self.pointNodes { 66 | pointNode.setNewHeight(newHeight: newHeight) 67 | } 68 | } 69 | } 70 | 71 | func resetHeight() { 72 | for pointNode in self.pointNodes { 73 | pointNode.resetHeight() 74 | } 75 | } 76 | 77 | func checkIfObjectShouldMoveOntoPlane(anchor: ARPlaneAnchor, planeAnchorNode: SCNNode) { 78 | for object in pointNodes { 79 | // Get the object's position in the plane's coordinate system. 80 | let objectPos = planeAnchorNode.convertPosition(object.position, from: object.parent) 81 | 82 | if objectPos.y == 0 { 83 | return; // The object is already on the plane - nothing to do here. 84 | } 85 | 86 | // Add 10% tolerance to the corners of the plane. 87 | let tolerance: Float = 0.1 88 | 89 | let minX: Float = anchor.center.x - anchor.extent.x / 2 - anchor.extent.x * tolerance 90 | let maxX: Float = anchor.center.x + anchor.extent.x / 2 + anchor.extent.x * tolerance 91 | let minZ: Float = anchor.center.z - anchor.extent.z / 2 - anchor.extent.z * tolerance 92 | let maxZ: Float = anchor.center.z + anchor.extent.z / 2 + anchor.extent.z * tolerance 93 | 94 | if objectPos.x < minX || objectPos.x > maxX || objectPos.z < minZ || objectPos.z > maxZ { 95 | return 96 | } 97 | 98 | // Move the object onto the plane if it is near it (within 5 centimeters). 99 | let verticalAllowance: Float = 0.05 100 | let epsilon: Float = 0.001 // Do not bother updating if the different is less than a mm. 101 | let distanceToPlane = abs(objectPos.y) 102 | if distanceToPlane > epsilon && distanceToPlane < verticalAllowance { 103 | delegate?.virtualObjectManager(self, didMoveObjectOntoNearbyPlane: object) 104 | 105 | SCNTransaction.begin() 106 | SCNTransaction.animationDuration = CFTimeInterval(distanceToPlane * 500) // Move 2 mm per second. 107 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) 108 | object.position.y = anchor.transform.columns.3.y 109 | SCNTransaction.commit() 110 | } 111 | } 112 | } 113 | 114 | func worldPositionFromScreenPosition(_ position: CGPoint, 115 | in sceneView: ARSCNView, 116 | objectPos: float3?, 117 | infinitePlane: Bool = false) -> (position: float3?, planeAnchor: ARPlaneAnchor?, hitAPlane: Bool) { 118 | 119 | //let dragOnInfinitePlanesEnabled = UserDefaults.standard.bool(for: .dragOnInfinitePlanes) 120 | 121 | // ------------------------------------------------------------------------------- 122 | // 1. Always do a hit test against exisiting plane anchors first. 123 | // (If any such anchors exist & only within their extents.) 124 | 125 | let planeHitTestResults = sceneView.hitTest(position, types: .existingPlaneUsingExtent) 126 | if let result = planeHitTestResults.first { 127 | 128 | let planeHitTestPosition = result.worldTransform.translation 129 | let planeAnchor = result.anchor 130 | 131 | // Return immediately - this is the best possible outcome. 132 | return (planeHitTestPosition, planeAnchor as? ARPlaneAnchor, true) 133 | } 134 | /* 135 | // ------------------------------------------------------------------------------- 136 | // 2. Collect more information about the environment by hit testing against 137 | // the feature point cloud, but do not return the result yet. 138 | 139 | var featureHitTestPosition: float3? 140 | var highQualityFeatureHitTestResult = false 141 | 142 | let highQualityfeatureHitTestResults = sceneView.hitTestWithFeatures(position, coneOpeningAngleInDegrees: 18, minDistance: 0.2, maxDistance: 2.0) 143 | 144 | if !highQualityfeatureHitTestResults.isEmpty { 145 | let result = highQualityfeatureHitTestResults[0] 146 | featureHitTestPosition = result.position 147 | highQualityFeatureHitTestResult = true 148 | } 149 | 150 | // ------------------------------------------------------------------------------- 151 | // 3. If desired or necessary (no good feature hit test result): Hit test 152 | // against an infinite, horizontal plane (ignoring the real world). 153 | 154 | if (infinitePlane && dragOnInfinitePlanesEnabled) || !highQualityFeatureHitTestResult { 155 | 156 | if let pointOnPlane = objectPos { 157 | let pointOnInfinitePlane = sceneView.hitTestWithInfiniteHorizontalPlane(position, pointOnPlane) 158 | if pointOnInfinitePlane != nil { 159 | return (pointOnInfinitePlane, nil, true) 160 | } 161 | } 162 | } 163 | 164 | // ------------------------------------------------------------------------------- 165 | // 4. If available, return the result of the hit test against high quality 166 | // features if the hit tests against infinite planes were skipped or no 167 | // infinite plane was hit. 168 | 169 | if highQualityFeatureHitTestResult { 170 | return (featureHitTestPosition, nil, false) 171 | } 172 | 173 | // ------------------------------------------------------------------------------- 174 | // 5. As a last resort, perform a second, unfiltered hit test against features. 175 | // If there are no features in the scene, the result returned here will be nil. 176 | 177 | let unfilteredFeatureHitTestResults = sceneView.hitTestWithFeatures(position) 178 | if !unfilteredFeatureHitTestResults.isEmpty { 179 | let result = unfilteredFeatureHitTestResults[0] 180 | return (result.position, nil, false) 181 | }*/ 182 | 183 | return (nil, nil, false) 184 | } 185 | } 186 | 187 | // MARK: - Delegate 188 | 189 | protocol VirtualObjectManagerDelegate: class { 190 | func virtualObjectManager(_ manager: VirtualObjectManager, willLoad object: PointNode) 191 | func virtualObjectManager(_ manager: VirtualObjectManager, didLoad object: PointNode) 192 | func virtualObjectManager(_ manager: VirtualObjectManager, transformDidChangeFor object: PointNode) 193 | func virtualObjectManager(_ manager: VirtualObjectManager, didMoveObjectOntoNearbyPlane object: PointNode) 194 | func virtualObjectManager(_ manager: VirtualObjectManager, couldNotPlace object: PointNode) 195 | } 196 | // Optional protocol methods 197 | extension VirtualObjectManagerDelegate { 198 | func virtualObjectManager(_ manager: VirtualObjectManager, transformDidChangeFor object: PointNode) {} 199 | func virtualObjectManager(_ manager: VirtualObjectManager, didMoveObjectOntoNearbyPlane object: PointNode) {} 200 | } 201 | -------------------------------------------------------------------------------- /Configuration/SampleCode.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // SampleCode.xcconfig 3 | // 4 | 5 | // The `SAMPLE_CODE_DISAMBIGUATOR` configuration is to make it easier to build 6 | // and run a sample code project. Once you set your project's development team, 7 | // you'll have a unique bundle identifier. This is because the bundle identifier 8 | // is derived based on the 'SAMPLE_CODE_DISAMBIGUATOR' value. Do not use this 9 | // approach in your own projects—it's only useful for sample code projects because 10 | // they are frequently downloaded and don't have a development team set. 11 | SAMPLE_CODE_DISAMBIGUATOR=${DEVELOPMENT_TEAM} 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ARPaint 2 | 3 | ARPaint demonstrates how to draw in the air with bare fingers using ARKit and Vision libraries introduced in iOS 11. 4 | 5 | [Watch Demo Video Here](https://www.youtube.com/watch?v=gb9E0n8m5pE&feature=youtu.be) 6 | 7 | 8 | ## How it Works 9 | 10 | Read this article: [`iOS ARKit Tutorial: Drawing in the Air with Bare Fingers`](https://www.toptal.com/swift/ios-arkit-tutorial-drawing-in-air-with-fingers#annex-exclusively-prodigious-devs) for detailed description of how this code work and how to get started with ARKit. 11 | 12 | ## Getting Started 13 | 14 | ARKit is available on any iOS 11 device, but the world tracking features that enable high-quality AR experiences require a device with the A9 or later processor. 15 | --------------------------------------------------------------------------------