├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── deploy_docs.yml │ └── swift-build-lint.yml ├── .gitignore ├── .spi.yml ├── .swiftlint.yml ├── FocusEntity-Example ├── FocusEntity-Example.xcodeproj │ └── project.pbxproj └── FocusEntity-Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── Add.imageset │ │ ├── Close.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── Open.imageset │ │ ├── Contents.json │ │ └── Open.png │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── BasicARView.swift │ ├── ContentView.swift │ ├── FocusARView.swift │ ├── Info.plist │ └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── LICENSE ├── LICENSE.origin ├── Package.swift ├── README.md ├── Sources └── FocusEntity │ ├── FocusEntity+Alignment.swift │ ├── FocusEntity+Classic.swift │ ├── FocusEntity+Colored.swift │ ├── FocusEntity+Segment.swift │ ├── FocusEntity.docc │ └── FocusEntity.md │ ├── FocusEntity.swift │ ├── FocusEntityComponent.swift │ └── float4x4+Extension.swift ├── install_swiftlint.sh └── media └── focusentity-dali.gif /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: 'maxxfrazer' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment:** 27 | - iOS Version 28 | - Device Information 29 | - macOS Version 30 | - Xcode Version 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: maxxfrazer 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy_docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy DocC 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main"] 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | # Allow one concurrent deployment 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | deploy: 21 | environment: 22 | name: github-pages 23 | url: ${{ steps.deployment.outputs.page_url }} 24 | runs-on: macos-12 25 | steps: 26 | - name: Checkout 🛎️ 27 | uses: actions/checkout@v3 28 | - name: Build DocC 🛠 29 | run: | 30 | xcodebuild docbuild -scheme FocusEntity -derivedDataPath /tmp/docbuild -destination 'generic/platform=iOS'; 31 | $(xcrun --find docc) process-archive \ 32 | transform-for-static-hosting /tmp/docbuild/Build/Products/Debug-iphoneos/FocusEntity.doccarchive \ 33 | --output-path docs \ 34 | --hosting-base-path FocusEntity; 35 | echo "" > docs/index.html 36 | - name: Upload artifact 📜 37 | uses: actions/upload-pages-artifact@v1 38 | with: 39 | # Upload docs directory 40 | path: 'docs' 41 | - name: Deploy to GitHub Pages 🐙 42 | id: deployment 43 | uses: actions/deploy-pages@v1 -------------------------------------------------------------------------------- /.github/workflows/swift-build-lint.yml: -------------------------------------------------------------------------------- 1 | name: swiftlint 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | build: 13 | runs-on: macos-latest 14 | steps: 15 | - name: Checkout 🛎 16 | uses: actions/checkout@v3 17 | - name: Swift Lint 🧹 18 | run: swiftlint --strict 19 | - name: Test Build 🔨 20 | run: xcodebuild -scheme $SCHEME -destination $DESTINATION 21 | env: 22 | SCHEME: FocusEntity 23 | DESTINATION: generic/platform=iOS 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | **/.DS_Store 3 | **.log 4 | 5 | 6 | # Xcode 7 | # 8 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 9 | 10 | ## Build generated 11 | build/ 12 | DerivedData/ 13 | 14 | ## Various settings 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata/ 24 | 25 | ## Other 26 | *.moved-aside 27 | *.xccheckout 28 | *.xcscmblueprint 29 | 30 | ## Obj-C/Swift specific 31 | *.hmap 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | .build/ 47 | xcshareddata/ 48 | *.xcworkspace/ 49 | *.xcodeproj/ 50 | Package.resolved 51 | 52 | # CocoaPods 53 | # 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | # 58 | Pods/ 59 | 60 | # Carthage 61 | # 62 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 63 | # Carthage/Checkouts 64 | 65 | Carthage/Build 66 | 67 | # fastlane 68 | # 69 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 70 | # screenshots whenever they are needed. 71 | # For more information about the recommended setup visit: 72 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 73 | 74 | fastlane/report.xml 75 | fastlane/Preview.html 76 | fastlane/screenshots 77 | fastlane/test_output 78 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - platform: ios 5 | documentation_targets: [FocusEntity] -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - todo 3 | - type_name 4 | identifier_name: 5 | min_length: 6 | warning: 1 7 | line_length: 8 | ignores_comments: true 9 | -------------------------------------------------------------------------------- /FocusEntity-Example/FocusEntity-Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F301E0D1231462A90028AAF1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F301E0D0231462A90028AAF1 /* AppDelegate.swift */; }; 11 | F301E0D3231462A90028AAF1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F301E0D2231462A90028AAF1 /* ContentView.swift */; }; 12 | F301E0D7231462AB0028AAF1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F301E0D6231462AB0028AAF1 /* Assets.xcassets */; }; 13 | F301E0DA231462AB0028AAF1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F301E0D9231462AB0028AAF1 /* Preview Assets.xcassets */; }; 14 | F301E0DD231462AB0028AAF1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F301E0DB231462AB0028AAF1 /* LaunchScreen.storyboard */; }; 15 | F301E0E5231462D90028AAF1 /* FocusARView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F301E0E4231462D90028AAF1 /* FocusARView.swift */; }; 16 | F339DB65275013C700D9A2B2 /* FocusEntity in Frameworks */ = {isa = PBXBuildFile; productRef = F339DB64275013C700D9A2B2 /* FocusEntity */; }; 17 | F385E0D229E74E97007CF478 /* BasicARView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F385E0D129E74E97007CF478 /* BasicARView.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | F301E0CD231462A90028AAF1 /* FocusEntity-Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "FocusEntity-Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | F301E0D0231462A90028AAF1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23 | F301E0D2231462A90028AAF1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 24 | F301E0D6231462AB0028AAF1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | F301E0D9231462AB0028AAF1 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 26 | F301E0DC231462AB0028AAF1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 27 | F301E0DE231462AB0028AAF1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 28 | F301E0E4231462D90028AAF1 /* FocusARView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusARView.swift; sourceTree = ""; }; 29 | F339DB622750138A00D9A2B2 /* FocusEntity */ = {isa = PBXFileReference; lastKnownFileType = folder; name = FocusEntity; path = ..; sourceTree = ""; }; 30 | F385E0D129E74E97007CF478 /* BasicARView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicARView.swift; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | F301E0CA231462A90028AAF1 /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | F339DB65275013C700D9A2B2 /* FocusEntity in Frameworks */, 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXFrameworksBuildPhase section */ 43 | 44 | /* Begin PBXGroup section */ 45 | F301E0C4231462A90028AAF1 = { 46 | isa = PBXGroup; 47 | children = ( 48 | F339DB612750138A00D9A2B2 /* Packages */, 49 | F301E0CF231462A90028AAF1 /* FocusEntity-Example */, 50 | F301E0CE231462A90028AAF1 /* Products */, 51 | F339DB63275013C700D9A2B2 /* Frameworks */, 52 | ); 53 | sourceTree = ""; 54 | }; 55 | F301E0CE231462A90028AAF1 /* Products */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | F301E0CD231462A90028AAF1 /* FocusEntity-Example.app */, 59 | ); 60 | name = Products; 61 | sourceTree = ""; 62 | }; 63 | F301E0CF231462A90028AAF1 /* FocusEntity-Example */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | F301E0D0231462A90028AAF1 /* AppDelegate.swift */, 67 | F301E0D2231462A90028AAF1 /* ContentView.swift */, 68 | F385E0D129E74E97007CF478 /* BasicARView.swift */, 69 | F301E0E4231462D90028AAF1 /* FocusARView.swift */, 70 | F301E0D6231462AB0028AAF1 /* Assets.xcassets */, 71 | F301E0DB231462AB0028AAF1 /* LaunchScreen.storyboard */, 72 | F301E0DE231462AB0028AAF1 /* Info.plist */, 73 | F301E0D8231462AB0028AAF1 /* Preview Content */, 74 | ); 75 | path = "FocusEntity-Example"; 76 | sourceTree = ""; 77 | }; 78 | F301E0D8231462AB0028AAF1 /* Preview Content */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | F301E0D9231462AB0028AAF1 /* Preview Assets.xcassets */, 82 | ); 83 | path = "Preview Content"; 84 | sourceTree = ""; 85 | }; 86 | F339DB612750138A00D9A2B2 /* Packages */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | F339DB622750138A00D9A2B2 /* FocusEntity */, 90 | ); 91 | name = Packages; 92 | sourceTree = ""; 93 | }; 94 | F339DB63275013C700D9A2B2 /* Frameworks */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | ); 98 | name = Frameworks; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXNativeTarget section */ 104 | F301E0CC231462A90028AAF1 /* FocusEntity-Example */ = { 105 | isa = PBXNativeTarget; 106 | buildConfigurationList = F301E0E1231462AB0028AAF1 /* Build configuration list for PBXNativeTarget "FocusEntity-Example" */; 107 | buildPhases = ( 108 | F301E0C9231462A90028AAF1 /* Sources */, 109 | F301E0CA231462A90028AAF1 /* Frameworks */, 110 | F301E0CB231462A90028AAF1 /* Resources */, 111 | ); 112 | buildRules = ( 113 | ); 114 | dependencies = ( 115 | ); 116 | name = "FocusEntity-Example"; 117 | packageProductDependencies = ( 118 | F339DB64275013C700D9A2B2 /* FocusEntity */, 119 | ); 120 | productName = "FocusEntity-Example"; 121 | productReference = F301E0CD231462A90028AAF1 /* FocusEntity-Example.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | /* End PBXNativeTarget section */ 125 | 126 | /* Begin PBXProject section */ 127 | F301E0C5231462A90028AAF1 /* Project object */ = { 128 | isa = PBXProject; 129 | attributes = { 130 | LastSwiftUpdateCheck = 1100; 131 | LastUpgradeCheck = 1100; 132 | ORGANIZATIONNAME = "Max Cobb"; 133 | TargetAttributes = { 134 | F301E0CC231462A90028AAF1 = { 135 | CreatedOnToolsVersion = 11.0; 136 | }; 137 | }; 138 | }; 139 | buildConfigurationList = F301E0C8231462A90028AAF1 /* Build configuration list for PBXProject "FocusEntity-Example" */; 140 | compatibilityVersion = "Xcode 9.3"; 141 | developmentRegion = en; 142 | hasScannedForEncodings = 0; 143 | knownRegions = ( 144 | en, 145 | Base, 146 | ); 147 | mainGroup = F301E0C4231462A90028AAF1; 148 | packageReferences = ( 149 | ); 150 | productRefGroup = F301E0CE231462A90028AAF1 /* Products */; 151 | projectDirPath = ""; 152 | projectRoot = ""; 153 | targets = ( 154 | F301E0CC231462A90028AAF1 /* FocusEntity-Example */, 155 | ); 156 | }; 157 | /* End PBXProject section */ 158 | 159 | /* Begin PBXResourcesBuildPhase section */ 160 | F301E0CB231462A90028AAF1 /* Resources */ = { 161 | isa = PBXResourcesBuildPhase; 162 | buildActionMask = 2147483647; 163 | files = ( 164 | F301E0DD231462AB0028AAF1 /* LaunchScreen.storyboard in Resources */, 165 | F301E0DA231462AB0028AAF1 /* Preview Assets.xcassets in Resources */, 166 | F301E0D7231462AB0028AAF1 /* Assets.xcassets in Resources */, 167 | ); 168 | runOnlyForDeploymentPostprocessing = 0; 169 | }; 170 | /* End PBXResourcesBuildPhase section */ 171 | 172 | /* Begin PBXSourcesBuildPhase section */ 173 | F301E0C9231462A90028AAF1 /* Sources */ = { 174 | isa = PBXSourcesBuildPhase; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | F385E0D229E74E97007CF478 /* BasicARView.swift in Sources */, 178 | F301E0D3231462A90028AAF1 /* ContentView.swift in Sources */, 179 | F301E0D1231462A90028AAF1 /* AppDelegate.swift in Sources */, 180 | F301E0E5231462D90028AAF1 /* FocusARView.swift in Sources */, 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | /* End PBXSourcesBuildPhase section */ 185 | 186 | /* Begin PBXVariantGroup section */ 187 | F301E0DB231462AB0028AAF1 /* LaunchScreen.storyboard */ = { 188 | isa = PBXVariantGroup; 189 | children = ( 190 | F301E0DC231462AB0028AAF1 /* Base */, 191 | ); 192 | name = LaunchScreen.storyboard; 193 | sourceTree = ""; 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 --------------------------------------------------------------------------------