├── .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 |
--------------------------------------------------------------------------------