";
194 | };
195 | /* End PBXVariantGroup section */
196 |
197 | /* Begin XCBuildConfiguration section */
198 | F301E0DF231462AB0028AAF1 /* Debug */ = {
199 | isa = XCBuildConfiguration;
200 | buildSettings = {
201 | ALWAYS_SEARCH_USER_PATHS = NO;
202 | CLANG_ANALYZER_NONNULL = YES;
203 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
204 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
205 | CLANG_CXX_LIBRARY = "libc++";
206 | CLANG_ENABLE_MODULES = YES;
207 | CLANG_ENABLE_OBJC_ARC = YES;
208 | CLANG_ENABLE_OBJC_WEAK = YES;
209 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
210 | CLANG_WARN_BOOL_CONVERSION = YES;
211 | CLANG_WARN_COMMA = YES;
212 | CLANG_WARN_CONSTANT_CONVERSION = YES;
213 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
214 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
215 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
216 | CLANG_WARN_EMPTY_BODY = YES;
217 | CLANG_WARN_ENUM_CONVERSION = YES;
218 | CLANG_WARN_INFINITE_RECURSION = YES;
219 | CLANG_WARN_INT_CONVERSION = YES;
220 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
221 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
222 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
223 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
224 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
225 | CLANG_WARN_STRICT_PROTOTYPES = YES;
226 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
227 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
228 | CLANG_WARN_UNREACHABLE_CODE = YES;
229 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
230 | COPY_PHASE_STRIP = NO;
231 | DEBUG_INFORMATION_FORMAT = dwarf;
232 | ENABLE_STRICT_OBJC_MSGSEND = YES;
233 | ENABLE_TESTABILITY = YES;
234 | GCC_C_LANGUAGE_STANDARD = gnu11;
235 | GCC_DYNAMIC_NO_PIC = NO;
236 | GCC_NO_COMMON_BLOCKS = YES;
237 | GCC_OPTIMIZATION_LEVEL = 0;
238 | GCC_PREPROCESSOR_DEFINITIONS = (
239 | "DEBUG=1",
240 | "$(inherited)",
241 | );
242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
244 | GCC_WARN_UNDECLARED_SELECTOR = YES;
245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
246 | GCC_WARN_UNUSED_FUNCTION = YES;
247 | GCC_WARN_UNUSED_VARIABLE = YES;
248 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
249 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
250 | MTL_FAST_MATH = YES;
251 | ONLY_ACTIVE_ARCH = YES;
252 | SDKROOT = iphoneos;
253 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
254 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
255 | };
256 | name = Debug;
257 | };
258 | F301E0E0231462AB0028AAF1 /* Release */ = {
259 | isa = XCBuildConfiguration;
260 | buildSettings = {
261 | ALWAYS_SEARCH_USER_PATHS = NO;
262 | CLANG_ANALYZER_NONNULL = YES;
263 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
264 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
265 | CLANG_CXX_LIBRARY = "libc++";
266 | CLANG_ENABLE_MODULES = YES;
267 | CLANG_ENABLE_OBJC_ARC = YES;
268 | CLANG_ENABLE_OBJC_WEAK = YES;
269 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
270 | CLANG_WARN_BOOL_CONVERSION = YES;
271 | CLANG_WARN_COMMA = YES;
272 | CLANG_WARN_CONSTANT_CONVERSION = YES;
273 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
274 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
275 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
276 | CLANG_WARN_EMPTY_BODY = YES;
277 | CLANG_WARN_ENUM_CONVERSION = YES;
278 | CLANG_WARN_INFINITE_RECURSION = YES;
279 | CLANG_WARN_INT_CONVERSION = YES;
280 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
281 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
282 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
283 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
284 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
285 | CLANG_WARN_STRICT_PROTOTYPES = YES;
286 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
287 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
288 | CLANG_WARN_UNREACHABLE_CODE = YES;
289 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
290 | COPY_PHASE_STRIP = NO;
291 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
292 | ENABLE_NS_ASSERTIONS = NO;
293 | ENABLE_STRICT_OBJC_MSGSEND = YES;
294 | GCC_C_LANGUAGE_STANDARD = gnu11;
295 | GCC_NO_COMMON_BLOCKS = YES;
296 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
297 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
298 | GCC_WARN_UNDECLARED_SELECTOR = YES;
299 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
300 | GCC_WARN_UNUSED_FUNCTION = YES;
301 | GCC_WARN_UNUSED_VARIABLE = YES;
302 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
303 | MTL_ENABLE_DEBUG_INFO = NO;
304 | MTL_FAST_MATH = YES;
305 | SDKROOT = iphoneos;
306 | SWIFT_COMPILATION_MODE = wholemodule;
307 | SWIFT_OPTIMIZATION_LEVEL = "-O";
308 | VALIDATE_PRODUCT = YES;
309 | };
310 | name = Release;
311 | };
312 | F301E0E2231462AB0028AAF1 /* Debug */ = {
313 | isa = XCBuildConfiguration;
314 | buildSettings = {
315 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
316 | CODE_SIGN_STYLE = Automatic;
317 | DEVELOPMENT_ASSET_PATHS = "\"FocusEntity-Example/Preview Content\"";
318 | DEVELOPMENT_TEAM = 278494H572;
319 | ENABLE_PREVIEWS = YES;
320 | INFOPLIST_FILE = "FocusEntity-Example/Info.plist";
321 | LD_RUNPATH_SEARCH_PATHS = (
322 | "$(inherited)",
323 | "@executable_path/Frameworks",
324 | );
325 | PRODUCT_BUNDLE_IDENTIFIER = uk.rocketar.focusentity.example;
326 | PRODUCT_NAME = "$(TARGET_NAME)";
327 | SWIFT_VERSION = 5.0;
328 | TARGETED_DEVICE_FAMILY = "1,2";
329 | };
330 | name = Debug;
331 | };
332 | F301E0E3231462AB0028AAF1 /* Release */ = {
333 | isa = XCBuildConfiguration;
334 | buildSettings = {
335 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
336 | CODE_SIGN_STYLE = Automatic;
337 | DEVELOPMENT_ASSET_PATHS = "\"FocusEntity-Example/Preview Content\"";
338 | DEVELOPMENT_TEAM = 278494H572;
339 | ENABLE_PREVIEWS = YES;
340 | INFOPLIST_FILE = "FocusEntity-Example/Info.plist";
341 | LD_RUNPATH_SEARCH_PATHS = (
342 | "$(inherited)",
343 | "@executable_path/Frameworks",
344 | );
345 | PRODUCT_BUNDLE_IDENTIFIER = uk.rocketar.focusentity.example;
346 | PRODUCT_NAME = "$(TARGET_NAME)";
347 | SWIFT_VERSION = 5.0;
348 | TARGETED_DEVICE_FAMILY = "1,2";
349 | };
350 | name = Release;
351 | };
352 | /* End XCBuildConfiguration section */
353 |
354 | /* Begin XCConfigurationList section */
355 | F301E0C8231462A90028AAF1 /* Build configuration list for PBXProject "FocusEntity-Example" */ = {
356 | isa = XCConfigurationList;
357 | buildConfigurations = (
358 | F301E0DF231462AB0028AAF1 /* Debug */,
359 | F301E0E0231462AB0028AAF1 /* Release */,
360 | );
361 | defaultConfigurationIsVisible = 0;
362 | defaultConfigurationName = Release;
363 | };
364 | F301E0E1231462AB0028AAF1 /* Build configuration list for PBXNativeTarget "FocusEntity-Example" */ = {
365 | isa = XCConfigurationList;
366 | buildConfigurations = (
367 | F301E0E2231462AB0028AAF1 /* Debug */,
368 | F301E0E3231462AB0028AAF1 /* Release */,
369 | );
370 | defaultConfigurationIsVisible = 0;
371 | defaultConfigurationName = Release;
372 | };
373 | /* End XCConfigurationList section */
374 |
375 | /* Begin XCSwiftPackageProductDependency section */
376 | F339DB64275013C700D9A2B2 /* FocusEntity */ = {
377 | isa = XCSwiftPackageProductDependency;
378 | productName = FocusEntity;
379 | };
380 | /* End XCSwiftPackageProductDependency section */
381 | };
382 | rootObject = F301E0C5231462A90028AAF1 /* Project object */;
383 | }
384 |
--------------------------------------------------------------------------------
/FocusEntity-Example/FocusEntity-Example/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // FocusEntity-Example
4 | //
5 | // Created by Max Cobb on 8/26/19.
6 | // Copyright © 2019 Max Cobb. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 | @UIApplicationMain
13 | class AppDelegate: UIResponder, UIApplicationDelegate {
14 |
15 | var window: UIWindow?
16 |
17 | func application(
18 | _ application: UIApplication,
19 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
20 | ) -> Bool {
21 |
22 | // Create the SwiftUI view that provides the window contents.
23 | let contentView = ContentView()
24 |
25 | // Use a UIHostingController as window root view controller.
26 | let window = UIWindow(frame: UIScreen.main.bounds)
27 | window.rootViewController = UIHostingController(rootView: contentView)
28 | self.window = window
29 | window.makeKeyAndVisible()
30 | return true
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/FocusEntity-Example/FocusEntity-Example/Assets.xcassets/Add.imageset/Close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxxfrazer/FocusEntity/5fa521172e447cbfa8c2fc1d6602d9d6bed202c7/FocusEntity-Example/FocusEntity-Example/Assets.xcassets/Add.imageset/Close.png
--------------------------------------------------------------------------------
/FocusEntity-Example/FocusEntity-Example/Assets.xcassets/Add.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Close.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/FocusEntity-Example/FocusEntity-Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/FocusEntity-Example/FocusEntity-Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/FocusEntity-Example/FocusEntity-Example/Assets.xcassets/Open.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Open.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/FocusEntity-Example/FocusEntity-Example/Assets.xcassets/Open.imageset/Open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxxfrazer/FocusEntity/5fa521172e447cbfa8c2fc1d6602d9d6bed202c7/FocusEntity-Example/FocusEntity-Example/Assets.xcassets/Open.imageset/Open.png
--------------------------------------------------------------------------------
/FocusEntity-Example/FocusEntity-Example/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/FocusEntity-Example/FocusEntity-Example/BasicARView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BasicARView.swift
3 | // FocusEntity-Example
4 | //
5 | // Created by Max Cobb on 12/04/2023.
6 | // Copyright © 2023 Max Cobb. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import RealityKit
11 | import FocusEntity
12 | import ARKit
13 |
14 | struct BasicARView: UIViewRepresentable {
15 | typealias UIViewType = ARView
16 | func makeUIView(context: Context) -> ARView {
17 | let arView = ARView(frame: .zero)
18 | let arConfig = ARWorldTrackingConfiguration()
19 | arConfig.planeDetection = [.horizontal, .vertical]
20 | arView.session.run(arConfig)
21 | _ = FocusEntity(on: arView, style: .classic())
22 | return arView
23 | }
24 | func updateUIView(_ uiView: ARView, context: Context) {}
25 | }
26 |
27 | struct BasicARView_Previews: PreviewProvider {
28 | static var previews: some View {
29 | BasicARView()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/FocusEntity-Example/FocusEntity-Example/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // FocusEntity-Example
4 | //
5 | // Created by Max Cobb on 8/26/19.
6 | // Copyright © 2019 Max Cobb. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import RealityKit
11 |
12 | struct ContentView: View {
13 | var body: some View {
14 | BasicARView().edgesIgnoringSafeArea(.all)
15 | // Uncomment the next line for a more complex example
16 | // ARViewContainer().edgesIgnoringSafeArea(.all)
17 | }
18 | }
19 |
20 | struct ARViewContainer: UIViewRepresentable {
21 | func makeUIView(context: Context) -> FocusARView {
22 | FocusARView(frame: .zero)
23 | }
24 | func updateUIView(_ uiView: FocusARView, context: Context) {}
25 | }
26 |
27 | #if DEBUG
28 | struct ContentView_Previews: PreviewProvider {
29 | static var previews: some View {
30 | ContentView()
31 | }
32 | }
33 | #endif
34 |
--------------------------------------------------------------------------------
/FocusEntity-Example/FocusEntity-Example/FocusARView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FocusARView.swift
3 | // FocusEntity-Example
4 | //
5 | // Created by Max Cobb on 8/26/19.
6 | // Copyright © 2019 Max Cobb. All rights reserved.
7 | //
8 |
9 | import RealityKit
10 | import FocusEntity
11 | import Combine
12 | import ARKit
13 |
14 | class FocusARView: ARView {
15 | enum FocusStyleChoices {
16 | case classic
17 | case material
18 | case color
19 | }
20 |
21 | /// Style to be displayed in the example
22 | let focusStyle: FocusStyleChoices = .classic
23 | var focusEntity: FocusEntity?
24 | required init(frame frameRect: CGRect) {
25 | super.init(frame: frameRect)
26 | self.setupConfig()
27 |
28 | switch self.focusStyle {
29 | case .color:
30 | self.focusEntity = FocusEntity(on: self, focus: .plane)
31 | case .material:
32 | do {
33 | let onColor: MaterialColorParameter = try .texture(.load(named: "Add"))
34 | let offColor: MaterialColorParameter = try .texture(.load(named: "Open"))
35 | self.focusEntity = FocusEntity(
36 | on: self,
37 | style: .colored(
38 | onColor: onColor, offColor: offColor,
39 | nonTrackingColor: offColor
40 | )
41 | )
42 | } catch {
43 | self.focusEntity = FocusEntity(on: self, focus: .classic)
44 | print("Unable to load plane textures")
45 | print(error.localizedDescription)
46 | }
47 | default:
48 | self.focusEntity = FocusEntity(on: self, focus: .classic)
49 | }
50 | }
51 |
52 | func setupConfig() {
53 | let config = ARWorldTrackingConfiguration()
54 | config.planeDetection = [.horizontal, .vertical]
55 | session.run(config)
56 | }
57 |
58 | @objc required dynamic init?(coder decoder: NSCoder) {
59 | fatalError("init(coder:) has not been implemented")
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/FocusEntity-Example/FocusEntity-Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | FocusEnt-Clone
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | NSCameraUsageDescription
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 | arkit
33 |
34 | UIStatusBarHidden
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/FocusEntity-Example/FocusEntity-Example/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Max Fraser Cobb
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/LICENSE.origin:
--------------------------------------------------------------------------------
1 | Copyright © 2018 Apple Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
9 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "FocusEntity",
8 | platforms: [.iOS(.v13), .macOS(.v10_15)],
9 | products: [
10 | .library(name: "FocusEntity", targets: ["FocusEntity"])
11 | ],
12 | dependencies: [],
13 | targets: [
14 | .target(name: "FocusEntity", dependencies: [])
15 | ],
16 | swiftLanguageVersions: [.v5]
17 | )
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FocusEntity
2 |
3 | This package is based on [ARKit-FocusNode](https://github.com/maxxfrazer/ARKit-FocusNode), but adapted to work in Apple's framework RealityKit.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | [The Example](./FocusEntity-Example) looks identical to the above GIF, which uses the FocusEntity classic style.
18 |
19 | See the [documentation](https://maxxfrazer.github.io/FocusEntity/documentation/focusentity/) for more.
20 |
21 | ## Minimum Requirements
22 | - Swift 5.2
23 | - iOS 13.0 (RealityKit)
24 | - Xcode 11
25 |
26 | If you're unfamiliar with using RealityKit, I would also recommend reading my articles on [Getting Started with RealityKit](https://medium.com/@maxxfrazer/getting-started-with-realitykit-3b401d6f6f).
27 |
28 | ## Installation
29 |
30 | ### Swift Package Manager
31 |
32 | Add the URL of this repository to your Xcode 11+ Project.
33 |
34 | Go to File > Swift Packages > Add Package Dependency, and paste in this link:
35 | `https://github.com/maxxfrazer/FocusEntity`
36 |
37 | ---
38 | ## Usage
39 |
40 | See the [Example project](./FocusEntity-Example) for a full working example as can be seen in the GIF above
41 |
42 | 1. Install `FocusEntity` with Swift Package Manager
43 |
44 | ```
45 | https://github.com/maxxfrazer/FocusEntity.git
46 | ```
47 |
48 | 2. Create an instance of FocusEntity, referencing your ARView:
49 |
50 | ```swift
51 | let focusSquare = FocusEntity(on: self.arView, focus: .classic)
52 | ```
53 |
54 | And that's it! The FocusEntity should already be tracking around your AR scene. There are options to turn the entity off or change its properties.
55 | Check out [the documentation](https://maxxfrazer.github.io/FocusEntity/documentation/focusentity/) or [example project](FocusEntity-Example) to learn more.
56 |
57 | ---
58 |
59 | Feel free to [send me a tweet](https://twitter.com/maxxfrazer) if you have any problems using FocusEntity, or open an Issue or PR!
60 |
61 |
62 | > The original code to create this repository has been adapted from one of Apple's examples from 2018, [license also included](LICENSE.origin). I have adapted the code to be used and distributed from within a Swift Package, and now further adapted to work with [RealityKit](https://developer.apple.com/documentation/realitykit).
63 |
--------------------------------------------------------------------------------
/Sources/FocusEntity/FocusEntity+Alignment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FocusEntity.swift
3 | // FocusEntity
4 | //
5 | // Created by Max Cobb on 8/26/19.
6 | // Copyright © 2019 Max Cobb. All rights reserved.
7 | //
8 |
9 | import RealityKit
10 | #if canImport(ARKit)
11 | import ARKit
12 | #endif
13 | import Combine
14 |
15 | extension FocusEntity {
16 |
17 | // MARK: Helper Methods
18 |
19 | /// Update the position of the focus square.
20 | internal func updatePosition() {
21 | // Average using several most recent positions.
22 | recentFocusEntityPositions = Array(recentFocusEntityPositions.suffix(10))
23 |
24 | // Move to average of recent positions to avoid jitter.
25 | let average = recentFocusEntityPositions.reduce(
26 | SIMD3.zero, { $0 + $1 }
27 | ) / Float(recentFocusEntityPositions.count)
28 | self.position = average
29 | }
30 |
31 | #if canImport(ARKit)
32 | /// Update the transform of the focus square to be aligned with the camera.
33 | internal func updateTransform(raycastResult: ARRaycastResult) {
34 | self.updatePosition()
35 |
36 | if state != .initializing {
37 | updateAlignment(for: raycastResult)
38 | }
39 | }
40 |
41 | internal func updateAlignment(for raycastResult: ARRaycastResult) {
42 |
43 | var targetAlignment = raycastResult.worldTransform.orientation
44 |
45 | // Determine current alignment
46 | var alignment: ARPlaneAnchor.Alignment?
47 | if let planeAnchor = raycastResult.anchor as? ARPlaneAnchor {
48 | alignment = planeAnchor.alignment
49 | // Catching case when looking at ceiling
50 | if targetAlignment.act([0, 1, 0]).y < -0.9 {
51 | targetAlignment *= simd_quatf(angle: .pi, axis: [0, 1, 0])
52 | }
53 | } else if raycastResult.targetAlignment == .horizontal {
54 | alignment = .horizontal
55 | } else if raycastResult.targetAlignment == .vertical {
56 | alignment = .vertical
57 | }
58 |
59 | // add to list of recent alignments
60 | if alignment != nil {
61 | self.recentFocusEntityAlignments.append(alignment!)
62 | }
63 |
64 | // Average using several most recent alignments.
65 | self.recentFocusEntityAlignments = Array(self.recentFocusEntityAlignments.suffix(20))
66 |
67 | let alignCount = self.recentFocusEntityAlignments.count
68 | let horizontalHistory = recentFocusEntityAlignments.filter({ $0 == .horizontal }).count
69 | let verticalHistory = recentFocusEntityAlignments.filter({ $0 == .vertical }).count
70 |
71 | // Alignment is same as most of the history - change it
72 | if alignment == .horizontal && horizontalHistory > alignCount * 3/4 ||
73 | alignment == .vertical && verticalHistory > alignCount / 2 ||
74 | raycastResult.anchor is ARPlaneAnchor {
75 | if alignment != self.currentAlignment ||
76 | (alignment == .vertical && self.shouldContinueAlignAnim(to: targetAlignment)
77 | ) {
78 | isChangingAlignment = true
79 | self.currentAlignment = alignment
80 | }
81 | } else {
82 | // Alignment is different than most of the history - ignore it
83 | return
84 | }
85 |
86 | // Change the focus entity's alignment
87 | if isChangingAlignment {
88 | // Uses interpolation.
89 | // Needs to be called on every frame that the animation is desired, Not just the first frame.
90 | performAlignmentAnimation(to: targetAlignment)
91 | } else {
92 | orientation = targetAlignment
93 | }
94 | }
95 | #endif
96 |
97 | internal func normalize(_ angle: Float, forMinimalRotationTo ref: Float) -> Float {
98 | // Normalize angle in steps of 90 degrees such that the rotation to the other angle is minimal
99 | var normalized = angle
100 | while abs(normalized - ref) > .pi / 4 {
101 | if angle > ref {
102 | normalized -= .pi / 2
103 | } else {
104 | normalized += .pi / 2
105 | }
106 | }
107 | return normalized
108 | }
109 |
110 | internal func getCamVector() -> (position: SIMD3, direciton: SIMD3)? {
111 | guard let camTransform = self.arView?.cameraTransform else {
112 | return nil
113 | }
114 | let camDirection = camTransform.matrix.columns.2
115 | return (camTransform.translation, -[camDirection.x, camDirection.y, camDirection.z])
116 | }
117 |
118 | #if canImport(ARKit)
119 | /// - Parameters:
120 | /// - Returns: ARRaycastResult if an existing plane geometry or an estimated plane are found, otherwise nil.
121 | internal func smartRaycast() -> ARRaycastResult? {
122 | // Perform the hit test.
123 | guard let (camPos, camDir) = self.getCamVector() else {
124 | return nil
125 | }
126 | for target in self.allowedRaycasts {
127 | let rcQuery = ARRaycastQuery(
128 | origin: camPos, direction: camDir,
129 | allowing: target, alignment: .any
130 | )
131 | let results = self.arView?.session.raycast(rcQuery) ?? []
132 |
133 | // Check for a result matching target
134 | if let result = results.first(
135 | where: { $0.target == target }
136 | ) { return result }
137 | }
138 | return nil
139 | }
140 | #endif
141 |
142 | /// Uses interpolation between orientations to create a smooth `easeOut` orientation adjustment animation.
143 | internal func performAlignmentAnimation(to newOrientation: simd_quatf) {
144 | // Interpolate between current and target orientations.
145 | orientation = simd_slerp(orientation, newOrientation, 0.15)
146 | // This length creates a normalized vector (of length 1) with all 3 components being equal.
147 | self.isChangingAlignment = self.shouldContinueAlignAnim(to: newOrientation)
148 | }
149 |
150 | func shouldContinueAlignAnim(to newOrientation: simd_quatf) -> Bool {
151 | let testVector = simd_float3(repeating: 1 / sqrtf(3))
152 | let point1 = orientation.act(testVector)
153 | let point2 = newOrientation.act(testVector)
154 | let vectorsDot = simd_dot(point1, point2)
155 | // Stop interpolating when the rotations are close enough to each other.
156 | return vectorsDot < 0.999
157 | }
158 |
159 | #if canImport(ARKit)
160 | /**
161 | Reduce visual size change with distance by scaling up when close and down when far away.
162 |
163 | These adjustments result in a scale of 1.0x for a distance of 0.7 m or less
164 | (estimated distance when looking at a table), and a scale of 1.2x
165 | for a distance 1.5 m distance (estimated distance when looking at the floor).
166 | */
167 | internal func scaleBasedOnDistance(camera: ARCamera?) -> Float {
168 | guard let camera = camera else { return 1.0 }
169 |
170 | let distanceFromCamera = simd_length(self.convert(position: .zero, to: nil) - camera.transform.translation)
171 | if distanceFromCamera < 0.7 {
172 | return distanceFromCamera / 0.7
173 | } else {
174 | return 0.25 * distanceFromCamera + 0.825
175 | }
176 | }
177 | #endif
178 | }
179 |
--------------------------------------------------------------------------------
/Sources/FocusEntity/FocusEntity+Classic.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FocusEntity+Classic.swift
3 | // FocusEntity
4 | //
5 | // Created by Max Cobb on 8/28/19.
6 | // Copyright © 2019 Max Cobb. All rights reserved.
7 | //
8 |
9 | import RealityKit
10 |
11 | /// An extension of FocusEntity holding the methods for the "classic" style.
12 | internal extension FocusEntity {
13 |
14 | // MARK: - Configuration Properties
15 |
16 | /// Original size of the focus square in meters. Not currently customizable
17 | static let size: Float = 0.17
18 |
19 | /// Thickness of the focus square lines in meters. Not currently customizable
20 | static let thickness: Float = 0.018
21 |
22 | /// Scale factor for the focus square when it is closed, w.r.t. the original size.
23 | static let scaleForClosedSquare: Float = 0.97
24 |
25 | /// Duration of the open/close animation. Not currently used.
26 | static let animationDuration = 0.7
27 |
28 | // MARK: - Initialization
29 |
30 | func setupClassic(_ classicStyle: ClassicStyle) {
31 | // opacity = 0.0
32 | /*
33 | The focus square consists of eight segments as follows, which can be individually animated.
34 |
35 | s0 s1
36 | _ _
37 | s2 | | s3
38 |
39 | s4 | | s5
40 | - -
41 | s6 s7
42 | */
43 |
44 | let segCorners: [(Corner, Alignment)] = [
45 | (.topLeft, .horizontal), (.topRight, .horizontal),
46 | (.topLeft, .vertical), (.topRight, .vertical),
47 | (.bottomLeft, .vertical), (.bottomRight, .vertical),
48 | (.bottomLeft, .horizontal), (.bottomRight, .horizontal)
49 | ]
50 | self.segments = segCorners.enumerated().map { (index, cornerAlign) -> Segment in
51 | Segment(
52 | name: "s\(index)",
53 | corner: cornerAlign.0,
54 | alignment: cornerAlign.1,
55 | color: classicStyle.color
56 | )
57 | }
58 |
59 | let sl: Float = 0.5 // segment length
60 | let c: Float = FocusEntity.thickness / 2 // correction to align lines perfectly
61 | segments[0].position += [-(sl / 2 - c), 0, -(sl - c)]
62 | segments[1].position += [sl / 2 - c, 0, -(sl - c)]
63 | segments[2].position += [-sl, 0, -sl / 2]
64 | segments[3].position += [sl, 0, -sl / 2]
65 | segments[4].position += [-sl, 0, sl / 2]
66 | segments[5].position += [sl, 0, sl / 2]
67 | segments[6].position += [-(sl / 2 - c), 0, sl - c]
68 | segments[7].position += [sl / 2 - c, 0, sl - c]
69 |
70 | for segment in segments {
71 | self.positioningEntity.addChild(segment)
72 | segment.open()
73 | }
74 |
75 | self.positioningEntity.scale = SIMD3(repeating: FocusEntity.size * FocusEntity.scaleForClosedSquare)
76 | }
77 |
78 | // MARK: Animations
79 |
80 | func offPlaneAniation() {
81 | // Open animation
82 | guard !isOpen else {
83 | return
84 | }
85 | isOpen = true
86 |
87 | for segment in segments {
88 | segment.open()
89 | }
90 | positioningEntity.scale = .init(repeating: FocusEntity.size)
91 | }
92 |
93 | func onPlaneAnimation(newPlane: Bool = false) {
94 | guard isOpen else {
95 | return
96 | }
97 | self.isOpen = false
98 |
99 | // Close animation
100 | for segment in self.segments {
101 | segment.close()
102 | }
103 |
104 | if newPlane {
105 | // New plane animation not implemented
106 | }
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/Sources/FocusEntity/FocusEntity+Colored.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FocusEntity+Colored.swift
3 | // FocusEntity
4 | //
5 | // Created by Max Cobb on 8/26/19.
6 | // Copyright © 2019 Max Cobb. All rights reserved.
7 | //
8 |
9 | import RealityKit
10 |
11 | /// An extension of FocusEntity holding the methods for the "colored" style.
12 | public extension FocusEntity {
13 |
14 | internal func coloredStateChanged() {
15 | guard let coloredStyle = self.focus.coloredStyle else {
16 | return
17 | }
18 | var endColor: MaterialColorParameter
19 | if self.state == .initializing {
20 | endColor = coloredStyle.nonTrackingColor
21 | } else {
22 | endColor = self.onPlane ? coloredStyle.onColor : coloredStyle.offColor
23 | }
24 | if self.fillPlane?.model?.materials.count == 0 {
25 | self.fillPlane?.model?.materials = [SimpleMaterial()]
26 | }
27 | var modelMaterial: Material!
28 | if #available(iOS 15, macOS 12, *) {
29 | switch endColor {
30 | case .color(let uikitColour):
31 | var mat = PhysicallyBasedMaterial()
32 | mat.baseColor = .init(tint: .black.withAlphaComponent(uikitColour.cgColor.alpha))
33 | mat.emissiveColor = .init(color: uikitColour)
34 | mat.emissiveIntensity = 2
35 | modelMaterial = mat
36 | case .texture(let tex):
37 | var mat = UnlitMaterial()
38 | mat.color = .init(tint: .white.withAlphaComponent(0.9999), texture: .init(tex))
39 | modelMaterial = mat
40 | @unknown default: break
41 | }
42 | } else {
43 | var mat = UnlitMaterial(color: .clear)
44 | mat.baseColor = endColor
45 | mat.tintColor = .white.withAlphaComponent(0.9999)
46 | modelMaterial = mat
47 | }
48 | self.fillPlane?.model?.materials[0] = modelMaterial
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/FocusEntity/FocusEntity+Segment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FocusEntity+Segment.swift
3 | // FocusEntity
4 | //
5 | // Created by Max Cobb on 8/28/19.
6 | // Copyright © 2019 Max Cobb. All rights reserved.
7 | //
8 |
9 | import RealityKit
10 |
11 | internal extension FocusEntity {
12 | /*
13 | The focus square consists of eight segments as follows, which can be individually animated.
14 |
15 | s0 s1
16 | _ _
17 | s2 | | s3
18 |
19 | s4 | | s5
20 | - -
21 | s6 s7
22 | */
23 | enum Corner {
24 | case topLeft // s0, s2
25 | case topRight // s1, s3
26 | case bottomRight // s5, s7
27 | case bottomLeft // s4, s6
28 | }
29 |
30 | enum Alignment {
31 | case horizontal // s0, s1, s6, s7
32 | case vertical // s2, s3, s4, s5
33 | }
34 |
35 | enum Direction {
36 | case up, down, left, right
37 |
38 | var reversed: Direction {
39 | switch self {
40 | case .up: return .down
41 | case .down: return .up
42 | case .left: return .right
43 | case .right: return .left
44 | }
45 | }
46 | }
47 |
48 | class Segment: Entity, HasModel {
49 |
50 | // MARK: - Configuration & Initialization
51 |
52 | /// Thickness of the focus square lines in m.
53 | static let thickness: Float = 0.018
54 |
55 | /// Length of the focus square lines in m.
56 | static let length: Float = 0.5 // segment length
57 |
58 | /// Side length of the focus square segments when it is open (w.r.t. to a 1x1 square).
59 | static let openLength: Float = 0.2
60 |
61 | let corner: Corner
62 | let alignment: Alignment
63 | let plane: ModelComponent
64 |
65 | init(name: String, corner: Corner, alignment: Alignment, color: Material.Color) {
66 | self.corner = corner
67 | self.alignment = alignment
68 |
69 | var mat: Material!
70 | if #available(iOS 15.0, *) {
71 | var phMat = PhysicallyBasedMaterial()
72 | phMat.baseColor = .init(tint: .black)
73 | phMat.emissiveColor = .init(color: color)
74 | phMat.emissiveIntensity = 2
75 | mat = phMat
76 | } else {
77 | // Fallback on earlier versions
78 | mat = UnlitMaterial(color: color)
79 | }
80 | plane = ModelComponent(mesh: .generatePlane(width: 1, depth: 1), materials: [mat])
81 |
82 | super.init()
83 |
84 | switch alignment {
85 | case .vertical:
86 | self.scale = [Segment.thickness, 1, Segment.length]
87 | case .horizontal:
88 | self.scale = [Segment.length, 1, Segment.thickness]
89 | }
90 | self.name = name
91 | model = plane
92 | }
93 |
94 | required init() {
95 | fatalError("init() has not been implemented")
96 | }
97 |
98 | // MARK: - Animating Open/Closed
99 |
100 | var openDirection: Direction {
101 | switch (corner, alignment) {
102 | case (.topLeft, .horizontal): return .left
103 | case (.topLeft, .vertical): return .up
104 | case (.topRight, .horizontal): return .right
105 | case (.topRight, .vertical): return .up
106 | case (.bottomLeft, .horizontal): return .left
107 | case (.bottomLeft, .vertical): return .down
108 | case (.bottomRight, .horizontal): return .right
109 | case (.bottomRight, .vertical): return .down
110 | }
111 | }
112 |
113 | func open() {
114 | if alignment == .horizontal {
115 | self.scale[0] = Segment.openLength
116 | } else {
117 | self.scale[2] = Segment.openLength
118 | }
119 |
120 | let offset = Segment.length / 2 - Segment.openLength / 2
121 | updatePosition(withOffset: Float(offset), for: openDirection)
122 | }
123 |
124 | func close() {
125 | let oldLength: Float
126 | if alignment == .horizontal {
127 | oldLength = self.scale[0]
128 | self.scale[0] = Segment.length
129 | } else {
130 | oldLength = self.scale[2]
131 | self.scale[2] = Segment.length
132 | }
133 |
134 | let offset = Segment.length / 2 - oldLength / 2
135 | updatePosition(withOffset: offset, for: openDirection.reversed)
136 | }
137 |
138 | private func updatePosition(withOffset offset: Float, for direction: Direction) {
139 | switch direction {
140 | case .left: position.x -= offset
141 | case .right: position.x += offset
142 | case .up: position.z -= offset
143 | case .down: position.z += offset
144 | }
145 | }
146 |
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Sources/FocusEntity/FocusEntity.docc/FocusEntity.md:
--------------------------------------------------------------------------------
1 | # ``FocusEntity``
2 |
3 | Visualise the camera focus in Augmented Reality.
4 |
5 | ## Overview
6 |
7 | FocusEntity lets you see exactly where the centre of the view will sit in the AR space. To add FocusEntity to your scene:
8 |
9 | ```swift
10 | let focusSquare = FocusEntity(on: <#ARView#>, focus: .classic)
11 | ```
12 |
13 | To make a whole SwiftUI View with a FocusEntity:
14 |
15 | ```swift
16 | struct BasicARView: UIViewRepresentable {
17 | typealias UIViewType = ARView
18 | func makeUIView(context: Context) -> ARView {
19 | let arView = ARView(frame: .zero)
20 | let arConfig = ARWorldTrackingConfiguration()
21 | arConfig.planeDetection = [.horizontal, .vertical]
22 | arView.session.run(arConfig)
23 | _ = FocusEntity(on: arView, style: .classic())
24 | return arView
25 | }
26 | func updateUIView(_ uiView: ARView, context: Context) {}
27 | }
28 | ```
29 |
30 | ## Topics
31 |
32 | ### FocusEntity
33 |
34 | - ``FocusEntity/FocusEntity``
35 | - ``FocusEntityComponent``
36 | - ``HasFocusEntity``
37 |
38 | ### Events
39 |
40 | Use the ``FocusEntityDelegate`` to catch events such as changing the plane anchor or otherwise a change of state.
41 |
42 | - ``FocusEntityDelegate``
43 |
--------------------------------------------------------------------------------
/Sources/FocusEntity/FocusEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FocusEntity.swift
3 | // FocusEntity
4 | //
5 | // Created by Max Cobb on 8/26/19.
6 | // Copyright © 2019 Max Cobb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RealityKit
11 | #if canImport(RealityFoundation)
12 | import RealityFoundation
13 | #endif
14 |
15 | #if os(macOS) || targetEnvironment(simulator)
16 | #warning("FocusEntity: This package is only fully available with physical iOS devices")
17 | #endif
18 |
19 | #if canImport(ARKit)
20 | import ARKit
21 | #endif
22 | import Combine
23 |
24 | public protocol HasFocusEntity: Entity {}
25 |
26 | public extension HasFocusEntity {
27 | var focus: FocusEntityComponent {
28 | get { self.components[FocusEntityComponent.self] ?? .classic }
29 | set { self.components[FocusEntityComponent.self] = newValue }
30 | }
31 | var isOpen: Bool {
32 | get { self.focus.isOpen }
33 | set { self.focus.isOpen = newValue }
34 | }
35 | internal var segments: [FocusEntity.Segment] {
36 | get { self.focus.segments }
37 | set { self.focus.segments = newValue }
38 | }
39 | #if canImport(ARKit)
40 | var allowedRaycasts: [ARRaycastQuery.Target] {
41 | get { self.focus.allowedRaycasts }
42 | set { self.focus.allowedRaycasts = newValue }
43 | }
44 | #endif
45 | }
46 |
47 | public protocol FocusEntityDelegate: AnyObject {
48 | /// Called when the FocusEntity is now in world space
49 | /// *Deprecated*: use ``focusEntity(_:trackingUpdated:oldState:)-4wx6e`` instead.
50 | @available(*, deprecated, message: "use focusEntity(_:trackingUpdated:oldState:) instead")
51 | func toTrackingState()
52 |
53 | /// Called when the FocusEntity is tracking the camera
54 | /// *Deprecated*: use ``focusEntity(_:trackingUpdated:oldState:)-4wx6e`` instead.
55 | @available(*, deprecated, message: "use focusEntity(_:trackingUpdated:oldState:) instead")
56 | func toInitializingState()
57 |
58 | /// When the tracking state of the FocusEntity updates. This will be called every update frame.
59 | /// - Parameters:
60 | /// - focusEntity: FocusEntity object whose tracking state has changed.
61 | /// - trackingState: New tracking state of the focus entity.
62 | /// - oldState: Old tracking state of the focus entity.
63 | func focusEntity(
64 | _ focusEntity: FocusEntity,
65 | trackingUpdated trackingState: FocusEntity.State,
66 | oldState: FocusEntity.State?
67 | )
68 |
69 | /// When the plane this focus entity is tracking changes. If the focus entity moves around within one plane anchor there will be no calls.
70 | /// - Parameters:
71 | /// - focusEntity: FocusEntity object whose anchor has changed.
72 | /// - planeChanged: New anchor the focus entity is tracked to.
73 | /// - oldPlane: Previous anchor the focus entity is tracked to.
74 | func focusEntity(
75 | _ focusEntity: FocusEntity,
76 | planeChanged: ARPlaneAnchor?,
77 | oldPlane: ARPlaneAnchor?
78 | )
79 | }
80 |
81 | public extension FocusEntityDelegate {
82 | func toTrackingState() {}
83 | func toInitializingState() {}
84 | func focusEntity(
85 | _ focusEntity: FocusEntity, trackingUpdated trackingState: FocusEntity.State, oldState: FocusEntity.State? = nil
86 | ) {}
87 | func focusEntity(_ focusEntity: FocusEntity, planeChanged: ARPlaneAnchor?, oldPlane: ARPlaneAnchor?) {}
88 | }
89 |
90 | /**
91 | An `Entity` which is used to provide uses with visual cues about the status of ARKit world tracking.
92 | */
93 | open class FocusEntity: Entity, HasAnchoring, HasFocusEntity {
94 |
95 | internal weak var arView: ARView?
96 |
97 | /// For moving the FocusEntity to a whole new ARView
98 | /// - Parameter view: The destination `ARView`
99 | public func moveTo(view: ARView) {
100 | let wasUpdating = self.isAutoUpdating
101 | self.setAutoUpdate(to: false)
102 | self.arView = view
103 | view.scene.addAnchor(self)
104 | if wasUpdating {
105 | self.setAutoUpdate(to: true)
106 | }
107 | }
108 |
109 | /// Destroy this FocusEntity and its references to any ARViews
110 | /// Without calling this, your ARView could stay in memory.
111 | public func destroy() {
112 | self.setAutoUpdate(to: false)
113 | self.delegate = nil
114 | self.arView = nil
115 | for child in children {
116 | child.removeFromParent()
117 | }
118 | self.removeFromParent()
119 | }
120 |
121 | private var updateCancellable: Cancellable?
122 | public private(set) var isAutoUpdating: Bool = false
123 |
124 | /// Auto update the focus entity using `SceneEvents.Update`.
125 | /// - Parameter autoUpdate: Should update the entity or not.
126 | public func setAutoUpdate(to autoUpdate: Bool) {
127 | guard autoUpdate != self.isAutoUpdating,
128 | !(autoUpdate && self.arView == nil)
129 | else { return }
130 | self.updateCancellable?.cancel()
131 | if autoUpdate {
132 | #if canImport(ARKit)
133 | self.updateCancellable = self.arView?.scene.subscribe(
134 | to: SceneEvents.Update.self, self.updateFocusEntity
135 | )
136 | #endif
137 | }
138 | self.isAutoUpdating = autoUpdate
139 | }
140 | public weak var delegate: FocusEntityDelegate?
141 |
142 | // MARK: - Types
143 | public enum State: Equatable {
144 | case initializing
145 | #if canImport(ARKit)
146 | case tracking(raycastResult: ARRaycastResult, camera: ARCamera?)
147 | #endif
148 | }
149 |
150 | // MARK: - Properties
151 |
152 | /// The most recent position of the focus square based on the current state.
153 | var lastPosition: SIMD3? {
154 | switch state {
155 | case .initializing: return nil
156 | #if canImport(ARKit)
157 | case .tracking(let raycastResult, _): return raycastResult.worldTransform.translation
158 | #endif
159 | }
160 | }
161 |
162 | #if canImport(ARKit)
163 | fileprivate func entityOffPlane(_ raycastResult: ARRaycastResult, _ camera: ARCamera?) {
164 | self.onPlane = false
165 | displayOffPlane(for: raycastResult)
166 | }
167 | #endif
168 |
169 | /// Current state of ``FocusEntity``.
170 | public var state: State = .initializing {
171 | didSet {
172 | guard state != oldValue else { return }
173 |
174 | switch state {
175 | case .initializing:
176 | if oldValue != .initializing {
177 | displayAsBillboard()
178 | self.delegate?.focusEntity(self, trackingUpdated: state, oldState: oldValue)
179 | }
180 | #if canImport(ARKit)
181 | case let .tracking(raycastResult, camera):
182 | let stateChanged = oldValue == .initializing
183 | if stateChanged && self.anchor != nil {
184 | self.anchoring = AnchoringComponent(.world(transform: Transform.identity.matrix))
185 | }
186 | let planeAnchor = raycastResult.anchor as? ARPlaneAnchor
187 | if let planeAnchor = planeAnchor {
188 | entityOnPlane(for: raycastResult, planeAnchor: planeAnchor)
189 | } else {
190 | entityOffPlane(raycastResult, camera)
191 | }
192 | if self.scaleEntityBasedOnDistance,
193 | let cameraTransform = self.arView?.cameraTransform {
194 | self.scale = .one * scaleBasedOnDistance(cameraTransform: cameraTransform)
195 | }
196 |
197 | defer { currentPlaneAnchor = planeAnchor }
198 | if stateChanged {
199 | self.delegate?.focusEntity(self, trackingUpdated: state, oldState: oldValue)
200 | }
201 | #endif
202 | }
203 | }
204 | }
205 |
206 | /**
207 | Reduce visual size change with distance by scaling up when close and down when far away.
208 |
209 | These adjustments result in a scale of 1.0x for a distance of 0.7 m or less
210 | (estimated distance when looking at a table), and a scale of 1.2x
211 | for a distance 1.5 m distance (estimated distance when looking at the floor).
212 | */
213 | private func scaleBasedOnDistance(cameraTransform: Transform) -> Float {
214 | let distanceFromCamera = simd_length(self.position(relativeTo: nil) - cameraTransform.translation)
215 | if distanceFromCamera < 0.7 {
216 | return distanceFromCamera / 0.7
217 | } else {
218 | return 0.25 * distanceFromCamera + 0.825
219 | }
220 | }
221 |
222 | /// Whether FocusEntity is on a plane or not.
223 | public internal(set) var onPlane: Bool = false
224 | /// Indicates if the square is currently being animated.
225 | public internal(set) var isAnimating = false
226 | /// Indicates if the square is currently changing its alignment.
227 | public internal(set) var isChangingAlignment = false
228 |
229 | /// A camera anchor used for placing the focus entity in front of the camera.
230 | internal var cameraAnchor: AnchorEntity!
231 |
232 | #if canImport(ARKit)
233 | /// The focus square's current alignment.
234 | internal var currentAlignment: ARPlaneAnchor.Alignment?
235 |
236 | /// The current plane anchor if the focus square is on a plane.
237 | public internal(set) var currentPlaneAnchor: ARPlaneAnchor? {
238 | didSet {
239 | if (oldValue == nil && self.currentPlaneAnchor == nil) || (currentPlaneAnchor == oldValue) {
240 | return
241 | }
242 | self.delegate?.focusEntity(self, planeChanged: currentPlaneAnchor, oldPlane: oldValue)
243 | }
244 | }
245 |
246 | /// The focus square's most recent alignments.
247 | internal var recentFocusEntityAlignments: [ARPlaneAnchor.Alignment] = []
248 | /// Previously visited plane anchors.
249 | internal var anchorsOfVisitedPlanes: Set = []
250 | #endif
251 | /// The focus square's most recent positions.
252 | internal var recentFocusEntityPositions: [SIMD3] = []
253 | /// The primary node that controls the position of other `FocusEntity` nodes.
254 | internal let positioningEntity = Entity()
255 | internal var fillPlane: ModelEntity?
256 |
257 | /// Modify the scale of the FocusEntity to make it slightly bigger when further away.
258 | public var scaleEntityBasedOnDistance = true
259 |
260 | // MARK: - Initialization
261 |
262 | /// Create a new ``FocusEntity`` instance.
263 | /// - Parameters:
264 | /// - arView: ARView containing the scene where the FocusEntity should be added.
265 | /// - style: Style of the ``FocusEntity``.
266 | public convenience init(on arView: ARView, style: FocusEntityComponent.Style) {
267 | self.init(on: arView, focus: FocusEntityComponent(style: style))
268 | }
269 |
270 | /// Create a new ``FocusEntity`` instance using the full ``FocusEntityComponent`` object.
271 | /// - Parameters:
272 | /// - arView: ARView containing the scene where the FocusEntity should be added.
273 | /// - focus: Main component for the ``FocusEntity``
274 | public required init(on arView: ARView, focus: FocusEntityComponent) {
275 | self.arView = arView
276 | super.init()
277 | self.focus = focus
278 | self.name = "FocusEntity"
279 | self.orientation = simd_quatf(angle: .pi / 2, axis: [1, 0, 0])
280 | self.addChild(self.positioningEntity)
281 |
282 | cameraAnchor = AnchorEntity(.camera)
283 | arView.scene.addAnchor(cameraAnchor)
284 |
285 | // Start the focus square as a billboard.
286 | displayAsBillboard()
287 | self.delegate?.focusEntity(self, trackingUpdated: .initializing, oldState: nil)
288 | arView.scene.addAnchor(self)
289 | self.setAutoUpdate(to: true)
290 | switch self.focus.style {
291 | case .colored(_, _, _, let mesh):
292 | let fillPlane = ModelEntity(mesh: mesh)
293 | self.positioningEntity.addChild(fillPlane)
294 | self.fillPlane = fillPlane
295 | self.coloredStateChanged()
296 | case .classic:
297 | guard let classicStyle = self.focus.classicStyle
298 | else { return }
299 | self.setupClassic(classicStyle)
300 | }
301 | }
302 |
303 | required public init() {
304 | fatalError("init() has not been implemented")
305 | }
306 |
307 | // MARK: - Appearance
308 |
309 | /// Displays the focus square parallel to the camera plane.
310 | private func displayAsBillboard() {
311 | self.onPlane = false
312 | #if canImport(ARKit)
313 | self.currentAlignment = .none
314 | #endif
315 | stateChangedSetup()
316 | }
317 |
318 | /// Places the focus entity in front of the camera instead of on a plane.
319 | private func putInFrontOfCamera() {
320 | // Works better than arView.ray()
321 | let newPosition = cameraAnchor.convert(position: [0, 0, -1], to: nil)
322 | recentFocusEntityPositions.append(newPosition)
323 | updatePosition()
324 | // --//
325 | // Make focus entity face the camera with a smooth animation.
326 | var newRotation = arView?.cameraTransform.rotation ?? simd_quatf()
327 | newRotation *= simd_quatf(angle: .pi / 2, axis: [1, 0, 0])
328 | performAlignmentAnimation(to: newRotation)
329 | }
330 |
331 | #if canImport(ARKit)
332 | /// Called when a surface has been detected.
333 | private func displayOffPlane(for raycastResult: ARRaycastResult) {
334 | self.stateChangedSetup()
335 | let position = raycastResult.worldTransform.translation
336 | if self.currentAlignment != .none {
337 | // It is ready to move over to a new surface.
338 | recentFocusEntityPositions.append(position)
339 | performAlignmentAnimation(to: raycastResult.worldTransform.orientation)
340 | } else {
341 | putInFrontOfCamera()
342 | }
343 | updateTransform(raycastResult: raycastResult)
344 | }
345 |
346 | /// Called when a plane has been detected.
347 | private func entityOnPlane(
348 | for raycastResult: ARRaycastResult, planeAnchor: ARPlaneAnchor
349 | ) {
350 | self.onPlane = true
351 | self.stateChangedSetup(newPlane: !anchorsOfVisitedPlanes.contains(planeAnchor))
352 | anchorsOfVisitedPlanes.insert(planeAnchor)
353 | let position = raycastResult.worldTransform.translation
354 | if self.currentAlignment != .none {
355 | // It is ready to move over to a new surface.
356 | recentFocusEntityPositions.append(position)
357 | } else {
358 | putInFrontOfCamera()
359 | }
360 | updateTransform(raycastResult: raycastResult)
361 | }
362 | #endif
363 |
364 | /// Called whenever the state of the focus entity changes
365 | ///
366 | /// - Parameter newPlane: If the entity is directly on a plane, is it a new plane to track
367 | public func stateChanged(newPlane: Bool = false) {
368 | switch self.focus.style {
369 | case .colored:
370 | self.coloredStateChanged()
371 | case .classic:
372 | if self.onPlane {
373 | self.onPlaneAnimation(newPlane: newPlane)
374 | } else { self.offPlaneAniation() }
375 | }
376 | }
377 |
378 | private func stateChangedSetup(newPlane: Bool = false) {
379 | guard !isAnimating else { return }
380 | self.stateChanged(newPlane: newPlane)
381 | }
382 |
383 | #if canImport(ARKit)
384 | public func updateFocusEntity(event: SceneEvents.Update? = nil) {
385 | // Perform hit testing only when ARKit tracking is in a good state.
386 | guard let camera = self.arView?.session.currentFrame?.camera,
387 | case .normal = camera.trackingState,
388 | let result = self.smartRaycast()
389 | else {
390 | // We should place the focus entity in front of the camera instead of on a plane.
391 | putInFrontOfCamera()
392 | self.state = .initializing
393 | return
394 | }
395 |
396 | self.state = .tracking(raycastResult: result, camera: camera)
397 | }
398 | #endif
399 | }
400 |
--------------------------------------------------------------------------------
/Sources/FocusEntity/FocusEntityComponent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FocusEntity.swift
3 | // FocusEntity
4 | //
5 | // Created by Max Cobb on 8/26/19.
6 | // Copyright © 2019 Max Cobb. All rights reserved.
7 | //
8 |
9 | import RealityKit
10 | #if !os(macOS)
11 | import ARKit
12 | #endif
13 |
14 | internal struct ClassicStyle {
15 | var color: Material.Color
16 | }
17 |
18 | /// When using colored style, first material of a mesh will be replaced with the chosen color
19 | internal struct ColoredStyle {
20 | /// Color when tracking the surface of a known plane
21 | var onColor: MaterialColorParameter
22 | /// Color when tracking an estimated plane
23 | var offColor: MaterialColorParameter
24 | /// Color when no surface tracking is achieved
25 | var nonTrackingColor: MaterialColorParameter
26 | var mesh: MeshResource
27 | }
28 |
29 | public struct FocusEntityComponent: Component {
30 | /// FocusEntityComponent Style, dictating how the FocusEntity will appear in different states
31 | public enum Style {
32 | /// Default style of FocusEntity. Box that's open when not on a plane, closed when on one.
33 | /// - color: Color of the FocusEntity lines, default: `FocusEntityComponent.defaultColor`
34 | case classic(color: Material.Color = FocusEntityComponent.defaultColor)
35 | /// Style that changes based on state of the FocusEntity
36 | /// - onColor: Color when FocusEntity is tracking on a known surface.
37 | /// - offColor: Color when FocusEntity is tracking, but the exact surface isn't known.
38 | /// - nonTrackingColor: Color when FocusEntity is unable to find a plane or estimate a plane.
39 | /// - mesh: Optional mesh for FocusEntity, default is a 0.1m square plane.
40 | case colored(
41 | onColor: MaterialColorParameter,
42 | offColor: MaterialColorParameter,
43 | nonTrackingColor: MaterialColorParameter,
44 | mesh: MeshResource = MeshResource.generatePlane(width: 0.1, depth: 0.1)
45 | )
46 | }
47 |
48 | let style: Style
49 | var classicStyle: ClassicStyle? {
50 | switch self.style {
51 | case .classic(let color):
52 | return ClassicStyle(color: color)
53 | default:
54 | return nil
55 | }
56 | }
57 |
58 | var coloredStyle: ColoredStyle? {
59 | switch self.style {
60 | case .colored(let onColor, let offColor, let nonTrackingColor, let mesh):
61 | return ColoredStyle(
62 | onColor: onColor, offColor: offColor,
63 | nonTrackingColor: nonTrackingColor, mesh: mesh
64 | )
65 | default:
66 | return nil
67 | }
68 | }
69 |
70 | /// Default color of FocusEntity
71 | public static let defaultColor = #colorLiteral(red: 1, green: 0.8, blue: 0, alpha: 1)
72 | /// Default style of FocusEntity, using the FocusEntityComponent.Style.classic with the color FocusEntityComponent.defaultColor.
73 | public static let classic = FocusEntityComponent(style: .classic(color: FocusEntityComponent.defaultColor))
74 | /// Alternative preset for FocusEntity, using FocusEntityComponent.Style.classic.colored,
75 | /// with green, orange and red for the onColor, offColor and nonTrackingColor respectively
76 | public static let plane = FocusEntityComponent(
77 | style: .colored(
78 | onColor: .color(.green),
79 | offColor: .color(.orange),
80 | nonTrackingColor: .color(Material.Color.red.withAlphaComponent(0.2)),
81 | mesh: FocusEntityComponent.defaultPlane
82 | )
83 | )
84 | public internal(set) var isOpen = true
85 | internal var segments: [FocusEntity.Segment] = []
86 | #if !os(macOS)
87 | public var allowedRaycasts: [ARRaycastQuery.Target] = [.existingPlaneGeometry, .estimatedPlane]
88 | #endif
89 |
90 | static var defaultPlane = MeshResource.generatePlane(
91 | width: 0.1, depth: 0.1
92 | )
93 |
94 | /// Create FocusEntityComponent with a given FocusEntityComponent.Style.
95 | /// - Parameter style: FocusEntityComponent Style, dictating how the FocusEntity will appear in different states.
96 | public init(style: Style) {
97 | self.style = style
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/FocusEntity/float4x4+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // float4x4+Extension.swift
3 | // FocusEntity
4 | //
5 | // Created by Max Cobb on 8/26/19.
6 | // Copyright © 2019 Max Cobb. All rights reserved.
7 | //
8 |
9 | import simd
10 |
11 | internal extension float4x4 {
12 | /**
13 | Treats matrix as a (right-hand column-major convention) transform matrix
14 | and factors out the translation component of the transform.
15 | */
16 | var translation: SIMD3 {
17 | get {
18 | let translation = columns.3
19 | return SIMD3(translation.x, translation.y, translation.z)
20 | }
21 | set(newValue) {
22 | columns.3 = SIMD4(newValue.x, newValue.y, newValue.z, columns.3.w)
23 | }
24 | }
25 |
26 | /**
27 | Factors out the orientation component of the transform.
28 | */
29 | var orientation: simd_quatf {
30 | return simd_quaternion(self)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/install_swiftlint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Installs the SwiftLint package.
4 | # Tries to get the precompiled .pkg file from Github, but if that
5 | # fails just recompiles from source.
6 |
7 | set -e
8 |
9 | SWIFTLINT_PKG_PATH="/tmp/SwiftLint.pkg"
10 | SWIFTLINT_PKG_URL="https://github.com/realm/SwiftLint/releases/download/0.31.0/SwiftLint.pkg"
11 |
12 | wget --output-document=$SWIFTLINT_PKG_PATH $SWIFTLINT_PKG_URL
13 |
14 | if [ -f $SWIFTLINT_PKG_PATH ]; then
15 | echo "SwiftLint package exists! Installing it..."
16 | sudo installer -pkg $SWIFTLINT_PKG_PATH -target /
17 | else
18 | echo "SwiftLint package doesn't exist. Compiling from source..." &&
19 | git clone https://github.com/realm/SwiftLint.git /tmp/SwiftLint &&
20 | cd /tmp/SwiftLint &&
21 | git submodule update --init --recursive &&
22 | sudo make install
23 | fi
--------------------------------------------------------------------------------
/media/focusentity-dali.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxxfrazer/FocusEntity/5fa521172e447cbfa8c2fc1d6602d9d6bed202c7/media/focusentity-dali.gif
--------------------------------------------------------------------------------