├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── deploy_to_cocoapods.yml │ ├── podlint.yml │ ├── swift-build.yml │ └── swift-linter.yml ├── .gitignore ├── .swiftlint.yml ├── Example ├── DrawingWithSCNLine.xcodeproj │ └── project.pbxproj ├── DrawingWithSCNLine │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ ├── ViewController+ARSCNViewDelegate.swift │ ├── ViewController+Drawing.swift │ ├── ViewController.swift │ └── art.scnassets └── Podfile ├── LICENSE ├── Package.swift ├── README.md ├── SCNLine.podspec ├── SCNLine.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Sources └── SCNLine │ ├── Info.plist │ ├── SCNGeometry+Extensions.swift │ ├── SCNLine.h │ ├── SCNLineNode.swift │ └── SCNVector3+Extensions.swift ├── Tests └── SCNLineTests │ └── SCNLineTests.swift └── media ├── lines-drawing-1.gif ├── lines-hello-lightoff.gif └── lines-hello-lighton.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: '' 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 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.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_to_cocoapods.yml: -------------------------------------------------------------------------------- 1 | name: deploy_to_cocoapods 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: macOS-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Install Cocoapods 17 | run: gem install cocoapods 18 | 19 | - name: Deploy to Cocoapods 20 | env: 21 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 22 | run: | 23 | set -eo pipefail; 24 | export LIB_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`); 25 | pod trunk push SCNLine.podspec; 26 | -------------------------------------------------------------------------------- /.github/workflows/podlint.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: "Pod Lint" 3 | jobs: 4 | podlint: 5 | runs-on: macOS-latest 6 | steps: 7 | - uses: actions/checkout@master 8 | - name: Pod Lint 9 | run: | 10 | pod lib lint 11 | -------------------------------------------------------------------------------- /.github/workflows/swift-build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | tags: 8 | - "!*" 9 | pull_request: 10 | branches: 11 | - "*" 12 | 13 | jobs: 14 | build: 15 | runs-on: macOS-11 16 | steps: 17 | - uses: actions/checkout@v1 18 | - name: Build Package 19 | run: | 20 | xcodebuild -scheme $SCHEME -destination $DESTINATION | xcpretty 21 | env: 22 | SCHEME: SCNLine 23 | DESTINATION: generic/platform=iOS 24 | -------------------------------------------------------------------------------- /.github/workflows/swift-linter.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: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: GitHub Action for SwiftLint 17 | uses: norio-nomura/action-swiftlint@3.2.1 18 | with: 19 | args: --strict 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | .DS_Store 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 41 | # Package.resolved 42 | .build/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | Pods/ 51 | Podfile.lock 52 | *.xcworkspace 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # fastlane 62 | # 63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 64 | # screenshots whenever they are needed. 65 | # For more information about the recommended setup visit: 66 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 67 | 68 | fastlane/report.xml 69 | fastlane/Preview.html 70 | fastlane/screenshots/**/*.png 71 | fastlane/test_output 72 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: # paths to ignore during linting. Takes precedence over `included`. 2 | - Carthage 3 | - Pods 4 | identifier_name: 5 | min_length: 6 | warning: 0 7 | large_tuple: 8 | warning: 6 9 | function_body_length: 10 | warning: 70 11 | line_length: 12 | ignores_comments: true 13 | disabled_rules: 14 | - todo 15 | -------------------------------------------------------------------------------- /Example/DrawingWithSCNLine.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 008A74C721C54F370066FB87 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 008A74C621C54F370066FB87 /* AppDelegate.swift */; }; 11 | 008A74C921C54F370066FB87 /* art.scnassets in Resources */ = {isa = PBXBuildFile; fileRef = 008A74C821C54F370066FB87 /* art.scnassets */; }; 12 | 008A74CB21C54F370066FB87 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 008A74CA21C54F370066FB87 /* ViewController.swift */; }; 13 | 008A74CD21C54F370066FB87 /* ViewController+ARSCNViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 008A74CC21C54F370066FB87 /* ViewController+ARSCNViewDelegate.swift */; }; 14 | 008A74CF21C54F3A0066FB87 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 008A74CE21C54F3A0066FB87 /* Assets.xcassets */; }; 15 | 008A74D221C54F3A0066FB87 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 008A74D021C54F3A0066FB87 /* LaunchScreen.storyboard */; }; 16 | 008A74DA21C54FE90066FB87 /* ViewController+Drawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 008A74D921C54FE90066FB87 /* ViewController+Drawing.swift */; }; 17 | 883A482A9C08A38252B63441 /* Pods_DrawingWithSCNLine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ACA251F27AE4B16A60E95E1 /* Pods_DrawingWithSCNLine.framework */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 008A74C321C54F370066FB87 /* DrawingWithSCNLine.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DrawingWithSCNLine.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 008A74C621C54F370066FB87 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23 | 008A74C821C54F370066FB87 /* art.scnassets */ = {isa = PBXFileReference; lastKnownFileType = text; path = art.scnassets; sourceTree = ""; }; 24 | 008A74CA21C54F370066FB87 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | 008A74CC21C54F370066FB87 /* ViewController+ARSCNViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+ARSCNViewDelegate.swift"; sourceTree = ""; }; 26 | 008A74CE21C54F3A0066FB87 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 27 | 008A74D121C54F3A0066FB87 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 28 | 008A74D321C54F3A0066FB87 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | 008A74D921C54FE90066FB87 /* ViewController+Drawing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+Drawing.swift"; sourceTree = ""; }; 30 | 24D3352DCC93E34A83387064 /* Pods-DrawingWithSCNLine.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DrawingWithSCNLine.release.xcconfig"; path = "Target Support Files/Pods-DrawingWithSCNLine/Pods-DrawingWithSCNLine.release.xcconfig"; sourceTree = ""; }; 31 | 313C49C0341C27D73595E5A0 /* Pods-DrawingWithSCNLine.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DrawingWithSCNLine.debug.xcconfig"; path = "Target Support Files/Pods-DrawingWithSCNLine/Pods-DrawingWithSCNLine.debug.xcconfig"; sourceTree = ""; }; 32 | 7ACA251F27AE4B16A60E95E1 /* Pods_DrawingWithSCNLine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DrawingWithSCNLine.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | /* End PBXFileReference section */ 34 | 35 | /* Begin PBXFrameworksBuildPhase section */ 36 | 008A74C021C54F370066FB87 /* Frameworks */ = { 37 | isa = PBXFrameworksBuildPhase; 38 | buildActionMask = 2147483647; 39 | files = ( 40 | 883A482A9C08A38252B63441 /* Pods_DrawingWithSCNLine.framework in Frameworks */, 41 | ); 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXFrameworksBuildPhase section */ 45 | 46 | /* Begin PBXGroup section */ 47 | 008A74BA21C54F370066FB87 = { 48 | isa = PBXGroup; 49 | children = ( 50 | 008A74C521C54F370066FB87 /* DrawingWithSCNLine */, 51 | 008A74C421C54F370066FB87 /* Products */, 52 | 402FA7C729E1B78CC942BD41 /* Pods */, 53 | E0A9F0EF4AE4E8916DFA59E3 /* Frameworks */, 54 | ); 55 | sourceTree = ""; 56 | }; 57 | 008A74C421C54F370066FB87 /* Products */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 008A74C321C54F370066FB87 /* DrawingWithSCNLine.app */, 61 | ); 62 | name = Products; 63 | sourceTree = ""; 64 | }; 65 | 008A74C521C54F370066FB87 /* DrawingWithSCNLine */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | 008A74C621C54F370066FB87 /* AppDelegate.swift */, 69 | 008A74C821C54F370066FB87 /* art.scnassets */, 70 | 008A74CA21C54F370066FB87 /* ViewController.swift */, 71 | 008A74CC21C54F370066FB87 /* ViewController+ARSCNViewDelegate.swift */, 72 | 008A74D921C54FE90066FB87 /* ViewController+Drawing.swift */, 73 | 008A74CE21C54F3A0066FB87 /* Assets.xcassets */, 74 | 008A74D021C54F3A0066FB87 /* LaunchScreen.storyboard */, 75 | 008A74D321C54F3A0066FB87 /* Info.plist */, 76 | ); 77 | path = DrawingWithSCNLine; 78 | sourceTree = ""; 79 | }; 80 | 402FA7C729E1B78CC942BD41 /* Pods */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 313C49C0341C27D73595E5A0 /* Pods-DrawingWithSCNLine.debug.xcconfig */, 84 | 24D3352DCC93E34A83387064 /* Pods-DrawingWithSCNLine.release.xcconfig */, 85 | ); 86 | path = Pods; 87 | sourceTree = ""; 88 | }; 89 | E0A9F0EF4AE4E8916DFA59E3 /* Frameworks */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 7ACA251F27AE4B16A60E95E1 /* Pods_DrawingWithSCNLine.framework */, 93 | ); 94 | name = Frameworks; 95 | sourceTree = ""; 96 | }; 97 | /* End PBXGroup section */ 98 | 99 | /* Begin PBXNativeTarget section */ 100 | 008A74C221C54F370066FB87 /* DrawingWithSCNLine */ = { 101 | isa = PBXNativeTarget; 102 | buildConfigurationList = 008A74D621C54F3A0066FB87 /* Build configuration list for PBXNativeTarget "DrawingWithSCNLine" */; 103 | buildPhases = ( 104 | B7C650C357C4CC057F9E63F0 /* [CP] Check Pods Manifest.lock */, 105 | 008A74BF21C54F370066FB87 /* Sources */, 106 | 008A74C021C54F370066FB87 /* Frameworks */, 107 | 008A74C121C54F370066FB87 /* Resources */, 108 | 6627F7C89538A3427F7C1BB2 /* [CP] Embed Pods Frameworks */, 109 | ); 110 | buildRules = ( 111 | ); 112 | dependencies = ( 113 | ); 114 | name = DrawingWithSCNLine; 115 | productName = DrawingWithSCNLine; 116 | productReference = 008A74C321C54F370066FB87 /* DrawingWithSCNLine.app */; 117 | productType = "com.apple.product-type.application"; 118 | }; 119 | /* End PBXNativeTarget section */ 120 | 121 | /* Begin PBXProject section */ 122 | 008A74BB21C54F370066FB87 /* Project object */ = { 123 | isa = PBXProject; 124 | attributes = { 125 | LastSwiftUpdateCheck = 1010; 126 | LastUpgradeCheck = 1010; 127 | ORGANIZATIONNAME = "Max Cobb"; 128 | TargetAttributes = { 129 | 008A74C221C54F370066FB87 = { 130 | CreatedOnToolsVersion = 10.1; 131 | LastSwiftMigration = 1020; 132 | }; 133 | }; 134 | }; 135 | buildConfigurationList = 008A74BE21C54F370066FB87 /* Build configuration list for PBXProject "DrawingWithSCNLine" */; 136 | compatibilityVersion = "Xcode 9.3"; 137 | developmentRegion = en; 138 | hasScannedForEncodings = 0; 139 | knownRegions = ( 140 | en, 141 | Base, 142 | ); 143 | mainGroup = 008A74BA21C54F370066FB87; 144 | productRefGroup = 008A74C421C54F370066FB87 /* Products */; 145 | projectDirPath = ""; 146 | projectRoot = ""; 147 | targets = ( 148 | 008A74C221C54F370066FB87 /* DrawingWithSCNLine */, 149 | ); 150 | }; 151 | /* End PBXProject section */ 152 | 153 | /* Begin PBXResourcesBuildPhase section */ 154 | 008A74C121C54F370066FB87 /* Resources */ = { 155 | isa = PBXResourcesBuildPhase; 156 | buildActionMask = 2147483647; 157 | files = ( 158 | 008A74D221C54F3A0066FB87 /* LaunchScreen.storyboard in Resources */, 159 | 008A74CF21C54F3A0066FB87 /* Assets.xcassets in Resources */, 160 | 008A74C921C54F370066FB87 /* art.scnassets in Resources */, 161 | ); 162 | runOnlyForDeploymentPostprocessing = 0; 163 | }; 164 | /* End PBXResourcesBuildPhase section */ 165 | 166 | /* Begin PBXShellScriptBuildPhase section */ 167 | 6627F7C89538A3427F7C1BB2 /* [CP] Embed Pods Frameworks */ = { 168 | isa = PBXShellScriptBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | ); 172 | inputFileListPaths = ( 173 | "${PODS_ROOT}/Target Support Files/Pods-DrawingWithSCNLine/Pods-DrawingWithSCNLine-frameworks-${CONFIGURATION}-input-files.xcfilelist", 174 | ); 175 | name = "[CP] Embed Pods Frameworks"; 176 | outputFileListPaths = ( 177 | "${PODS_ROOT}/Target Support Files/Pods-DrawingWithSCNLine/Pods-DrawingWithSCNLine-frameworks-${CONFIGURATION}-output-files.xcfilelist", 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | shellPath = /bin/sh; 181 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DrawingWithSCNLine/Pods-DrawingWithSCNLine-frameworks.sh\"\n"; 182 | showEnvVarsInLog = 0; 183 | }; 184 | B7C650C357C4CC057F9E63F0 /* [CP] Check Pods Manifest.lock */ = { 185 | isa = PBXShellScriptBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | ); 189 | inputFileListPaths = ( 190 | ); 191 | inputPaths = ( 192 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 193 | "${PODS_ROOT}/Manifest.lock", 194 | ); 195 | name = "[CP] Check Pods Manifest.lock"; 196 | outputFileListPaths = ( 197 | ); 198 | outputPaths = ( 199 | "$(DERIVED_FILE_DIR)/Pods-DrawingWithSCNLine-checkManifestLockResult.txt", 200 | ); 201 | runOnlyForDeploymentPostprocessing = 0; 202 | shellPath = /bin/sh; 203 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 204 | showEnvVarsInLog = 0; 205 | }; 206 | /* End PBXShellScriptBuildPhase section */ 207 | 208 | /* Begin PBXSourcesBuildPhase section */ 209 | 008A74BF21C54F370066FB87 /* Sources */ = { 210 | isa = PBXSourcesBuildPhase; 211 | buildActionMask = 2147483647; 212 | files = ( 213 | 008A74CD21C54F370066FB87 /* ViewController+ARSCNViewDelegate.swift in Sources */, 214 | 008A74DA21C54FE90066FB87 /* ViewController+Drawing.swift in Sources */, 215 | 008A74CB21C54F370066FB87 /* ViewController.swift in Sources */, 216 | 008A74C721C54F370066FB87 /* AppDelegate.swift in Sources */, 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | }; 220 | /* End PBXSourcesBuildPhase section */ 221 | 222 | /* Begin PBXVariantGroup section */ 223 | 008A74D021C54F3A0066FB87 /* LaunchScreen.storyboard */ = { 224 | isa = PBXVariantGroup; 225 | children = ( 226 | 008A74D121C54F3A0066FB87 /* Base */, 227 | ); 228 | name = LaunchScreen.storyboard; 229 | sourceTree = ""; 230 | }; 231 | /* End PBXVariantGroup section */ 232 | 233 | /* Begin XCBuildConfiguration section */ 234 | 008A74D421C54F3A0066FB87 /* Debug */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | ALWAYS_SEARCH_USER_PATHS = NO; 238 | CLANG_ANALYZER_NONNULL = YES; 239 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 240 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 241 | CLANG_CXX_LIBRARY = "libc++"; 242 | CLANG_ENABLE_MODULES = YES; 243 | CLANG_ENABLE_OBJC_ARC = YES; 244 | CLANG_ENABLE_OBJC_WEAK = YES; 245 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 246 | CLANG_WARN_BOOL_CONVERSION = YES; 247 | CLANG_WARN_COMMA = YES; 248 | CLANG_WARN_CONSTANT_CONVERSION = YES; 249 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 250 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 251 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 252 | CLANG_WARN_EMPTY_BODY = YES; 253 | CLANG_WARN_ENUM_CONVERSION = YES; 254 | CLANG_WARN_INFINITE_RECURSION = YES; 255 | CLANG_WARN_INT_CONVERSION = YES; 256 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 257 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 258 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 259 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 260 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 261 | CLANG_WARN_STRICT_PROTOTYPES = YES; 262 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 263 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 264 | CLANG_WARN_UNREACHABLE_CODE = YES; 265 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 266 | CODE_SIGN_IDENTITY = "iPhone Developer"; 267 | COPY_PHASE_STRIP = NO; 268 | DEBUG_INFORMATION_FORMAT = dwarf; 269 | ENABLE_STRICT_OBJC_MSGSEND = YES; 270 | ENABLE_TESTABILITY = YES; 271 | GCC_C_LANGUAGE_STANDARD = gnu11; 272 | GCC_DYNAMIC_NO_PIC = NO; 273 | GCC_NO_COMMON_BLOCKS = YES; 274 | GCC_OPTIMIZATION_LEVEL = 0; 275 | GCC_PREPROCESSOR_DEFINITIONS = ( 276 | "DEBUG=1", 277 | "$(inherited)", 278 | ); 279 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 280 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 281 | GCC_WARN_UNDECLARED_SELECTOR = YES; 282 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 283 | GCC_WARN_UNUSED_FUNCTION = YES; 284 | GCC_WARN_UNUSED_VARIABLE = YES; 285 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 286 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 287 | MTL_FAST_MATH = YES; 288 | ONLY_ACTIVE_ARCH = YES; 289 | SDKROOT = iphoneos; 290 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 291 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 292 | }; 293 | name = Debug; 294 | }; 295 | 008A74D521C54F3A0066FB87 /* Release */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ALWAYS_SEARCH_USER_PATHS = NO; 299 | CLANG_ANALYZER_NONNULL = YES; 300 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 301 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 302 | CLANG_CXX_LIBRARY = "libc++"; 303 | CLANG_ENABLE_MODULES = YES; 304 | CLANG_ENABLE_OBJC_ARC = YES; 305 | CLANG_ENABLE_OBJC_WEAK = YES; 306 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 307 | CLANG_WARN_BOOL_CONVERSION = YES; 308 | CLANG_WARN_COMMA = YES; 309 | CLANG_WARN_CONSTANT_CONVERSION = YES; 310 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 311 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 312 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 313 | CLANG_WARN_EMPTY_BODY = YES; 314 | CLANG_WARN_ENUM_CONVERSION = YES; 315 | CLANG_WARN_INFINITE_RECURSION = YES; 316 | CLANG_WARN_INT_CONVERSION = YES; 317 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 318 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 319 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 320 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 321 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 322 | CLANG_WARN_STRICT_PROTOTYPES = YES; 323 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 324 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 325 | CLANG_WARN_UNREACHABLE_CODE = YES; 326 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 327 | CODE_SIGN_IDENTITY = "iPhone Developer"; 328 | COPY_PHASE_STRIP = NO; 329 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 330 | ENABLE_NS_ASSERTIONS = NO; 331 | ENABLE_STRICT_OBJC_MSGSEND = YES; 332 | GCC_C_LANGUAGE_STANDARD = gnu11; 333 | GCC_NO_COMMON_BLOCKS = YES; 334 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 335 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 336 | GCC_WARN_UNDECLARED_SELECTOR = YES; 337 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 338 | GCC_WARN_UNUSED_FUNCTION = YES; 339 | GCC_WARN_UNUSED_VARIABLE = YES; 340 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 341 | MTL_ENABLE_DEBUG_INFO = NO; 342 | MTL_FAST_MATH = YES; 343 | SDKROOT = iphoneos; 344 | SWIFT_COMPILATION_MODE = wholemodule; 345 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 346 | VALIDATE_PRODUCT = YES; 347 | }; 348 | name = Release; 349 | }; 350 | 008A74D721C54F3A0066FB87 /* Debug */ = { 351 | isa = XCBuildConfiguration; 352 | baseConfigurationReference = 313C49C0341C27D73595E5A0 /* Pods-DrawingWithSCNLine.debug.xcconfig */; 353 | buildSettings = { 354 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 355 | CODE_SIGN_STYLE = Automatic; 356 | DEVELOPMENT_TEAM = N2RTJDRM64; 357 | INFOPLIST_FILE = DrawingWithSCNLine/Info.plist; 358 | LD_RUNPATH_SEARCH_PATHS = ( 359 | "$(inherited)", 360 | "@executable_path/Frameworks", 361 | ); 362 | PRODUCT_BUNDLE_IDENTIFIER = com.maxxfrazer.SCNLine.Example; 363 | PRODUCT_NAME = "$(TARGET_NAME)"; 364 | SWIFT_VERSION = 5.0; 365 | TARGETED_DEVICE_FAMILY = "1,2"; 366 | }; 367 | name = Debug; 368 | }; 369 | 008A74D821C54F3A0066FB87 /* Release */ = { 370 | isa = XCBuildConfiguration; 371 | baseConfigurationReference = 24D3352DCC93E34A83387064 /* Pods-DrawingWithSCNLine.release.xcconfig */; 372 | buildSettings = { 373 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 374 | CODE_SIGN_STYLE = Automatic; 375 | DEVELOPMENT_TEAM = N2RTJDRM64; 376 | INFOPLIST_FILE = DrawingWithSCNLine/Info.plist; 377 | LD_RUNPATH_SEARCH_PATHS = ( 378 | "$(inherited)", 379 | "@executable_path/Frameworks", 380 | ); 381 | PRODUCT_BUNDLE_IDENTIFIER = com.maxxfrazer.SCNLine.Example; 382 | PRODUCT_NAME = "$(TARGET_NAME)"; 383 | SWIFT_VERSION = 5.0; 384 | TARGETED_DEVICE_FAMILY = "1,2"; 385 | }; 386 | name = Release; 387 | }; 388 | /* End XCBuildConfiguration section */ 389 | 390 | /* Begin XCConfigurationList section */ 391 | 008A74BE21C54F370066FB87 /* Build configuration list for PBXProject "DrawingWithSCNLine" */ = { 392 | isa = XCConfigurationList; 393 | buildConfigurations = ( 394 | 008A74D421C54F3A0066FB87 /* Debug */, 395 | 008A74D521C54F3A0066FB87 /* Release */, 396 | ); 397 | defaultConfigurationIsVisible = 0; 398 | defaultConfigurationName = Release; 399 | }; 400 | 008A74D621C54F3A0066FB87 /* Build configuration list for PBXNativeTarget "DrawingWithSCNLine" */ = { 401 | isa = XCConfigurationList; 402 | buildConfigurations = ( 403 | 008A74D721C54F3A0066FB87 /* Debug */, 404 | 008A74D821C54F3A0066FB87 /* Release */, 405 | ); 406 | defaultConfigurationIsVisible = 0; 407 | defaultConfigurationName = Release; 408 | }; 409 | /* End XCConfigurationList section */ 410 | }; 411 | rootObject = 008A74BB21C54F370066FB87 /* Project object */; 412 | } 413 | -------------------------------------------------------------------------------- /Example/DrawingWithSCNLine/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DrawingWithSCNLine 4 | // 5 | // Created by Max Cobb on 12/15/18. 6 | // Copyright © 2018 Max Cobb. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application( 17 | _ application: UIApplication, 18 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 19 | ) -> Bool { 20 | let window = UIWindow(frame: UIScreen.main.bounds) 21 | window.rootViewController = ViewController() 22 | window.makeKeyAndVisible() 23 | self.window = window 24 | return true 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Example/DrawingWithSCNLine/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 | } -------------------------------------------------------------------------------- /Example/DrawingWithSCNLine/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/DrawingWithSCNLine/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 | -------------------------------------------------------------------------------- /Example/DrawingWithSCNLine/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSCameraUsageDescription 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | arkit 31 | 32 | UIStatusBarHidden 33 | 34 | UISupportedInterfaceOrientations 35 | 36 | UIInterfaceOrientationPortrait 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Example/DrawingWithSCNLine/ViewController+ARSCNViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController+ARSCNViewDelegate.swift 3 | // DrawingWithSCNLine 4 | // 5 | // Created by Max Cobb on 12/15/18. 6 | // Copyright © 2018 Max Cobb. All rights reserved. 7 | // 8 | 9 | import ARKit 10 | 11 | extension ViewController: ARSCNViewDelegate {} 12 | -------------------------------------------------------------------------------- /Example/DrawingWithSCNLine/ViewController+Drawing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController+Drawing.swift 3 | // DrawingWithSCNLine 4 | // 5 | // Created by Max Cobb on 12/15/18. 6 | // Copyright © 2018 Max Cobb. All rights reserved. 7 | // 8 | 9 | import ARKit 10 | import SCNLine 11 | 12 | var lastPoint = SCNVector3Zero 13 | var minimumMovement: Float = 0.005 14 | 15 | private extension SCNVector3 { 16 | func distance(to vector: SCNVector3) -> Float { 17 | let diff = SCNVector3(x - vector.x, y - vector.y, z - vector.z) 18 | return sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z) 19 | } 20 | } 21 | 22 | extension ViewController { 23 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 24 | guard let location = touches.first?.location(in: nil) else { 25 | return 26 | } 27 | pointTouching = location 28 | 29 | begin() 30 | isDrawing = true 31 | } 32 | override func touchesMoved(_ touches: Set, with event: UIEvent?) { 33 | guard let location = touches.first?.location(in: nil) else { 34 | return 35 | } 36 | pointTouching = location 37 | } 38 | 39 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 40 | isDrawing = false 41 | reset() 42 | } 43 | 44 | private func begin() { 45 | drawingNode = SCNLineNode(with: [], radius: 0.01, edges: 12, maxTurning: 12) 46 | // Creating a random colored material. 47 | let material = SCNMaterial() 48 | material.diffuse.contents = UIColor( 49 | displayP3Red: CGFloat.random(in: 0...1), 50 | green: CGFloat.random(in: 0...1), 51 | blue: CGFloat.random(in: 0...1), 52 | alpha: 1 53 | ) 54 | material.isDoubleSided = true 55 | drawingNode?.lineMaterials = [material] 56 | 57 | sceneView.scene.rootNode.addChildNode(drawingNode!) 58 | } 59 | 60 | private func addPointAndCreateVertices() { 61 | guard let lastHit = self.sceneView.hitTest(self.pointTouching, options: [ 62 | SCNHitTestOption.rootNode: cameraFrameNode, SCNHitTestOption.ignoreHiddenNodes: false 63 | ]).first else { 64 | return 65 | } 66 | if lastHit.worldCoordinates.distance(to: lastPoint) > minimumMovement { 67 | hitVertices.append(lastHit.worldCoordinates) 68 | lastPoint = lastHit.worldCoordinates 69 | updateGeometry(with: lastPoint) 70 | } 71 | } 72 | 73 | private func updateGeometry(with point: SCNVector3) { 74 | guard hitVertices.count > 1, let drawNode = drawingNode else { 75 | return 76 | } 77 | drawNode.add(point: point) 78 | } 79 | 80 | private func reset() { 81 | hitVertices.removeAll() 82 | drawingNode = nil 83 | } 84 | 85 | func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { 86 | if isDrawing { 87 | addPointAndCreateVertices() 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /Example/DrawingWithSCNLine/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // DrawingWithSCNLine 4 | // 5 | // Created by Max Cobb on 12/15/18. 6 | // Copyright © 2018 Max Cobb. All rights reserved. 7 | // 8 | 9 | import ARKit 10 | import SCNLine 11 | 12 | class ViewController: UIViewController { 13 | 14 | var sceneView = ARSCNView(frame: .zero) 15 | 16 | var drawingNode: SCNLineNode? 17 | 18 | var centerVerticesCount: Int32 = 0 19 | var hitVertices: [SCNVector3] = [] 20 | 21 | var pointTouching: CGPoint = .zero 22 | var isDrawing: Bool = false 23 | 24 | /// Used for calculating where to draw using hitTesting 25 | var cameraFrameNode = SCNNode(geometry: SCNFloor()) 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | self.sceneView.frame = self.view.bounds 31 | self.sceneView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 32 | self.view.addSubview(sceneView) 33 | 34 | // Set the view's delegate 35 | sceneView.delegate = self 36 | 37 | // Show statistics such as fps and timing information 38 | sceneView.showsStatistics = true 39 | 40 | sceneView.autoenablesDefaultLighting = true 41 | 42 | self.cameraFrameNode.isHidden = true 43 | self.sceneView.pointOfView?.addChildNode(self.cameraFrameNode) 44 | cameraFrameNode.position.z = -0.5 45 | cameraFrameNode.eulerAngles.x = -.pi / 2 46 | } 47 | 48 | override func viewWillAppear(_ animated: Bool) { 49 | super.viewWillAppear(animated) 50 | 51 | // Create a session configuration 52 | let configuration = ARWorldTrackingConfiguration() 53 | 54 | // Run the view's session 55 | sceneView.session.run(configuration) 56 | } 57 | 58 | override func viewWillDisappear(_ animated: Bool) { 59 | super.viewWillDisappear(animated) 60 | 61 | // Pause the view's session 62 | sceneView.session.pause() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Example/DrawingWithSCNLine/art.scnassets: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxxfrazer/SceneKit-SCNLine/2162cf126307d156d2bff987ccc4b6439b288500/Example/DrawingWithSCNLine/art.scnassets -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | project 'DrawingWithSCNLine.xcodeproj' 2 | 3 | # Uncomment the next line to define a global platform for your project 4 | platform :ios, '12.0' 5 | 6 | target 'DrawingWithSCNLine' do 7 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 8 | use_frameworks! 9 | 10 | # Pods for DrawingWithSCNLine 11 | pod 'SCNLine', :path => '../' 12 | end 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Max 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SCNLine", 7 | platforms: [.iOS(.v11), .macOS(.v11)], // Support for iOS 11 and macOS Big Sur. 8 | products: [.library(name: "SCNLine", targets: ["SCNLine"])], 9 | targets: [ 10 | .target(name: "SCNLine"), 11 | .testTarget( 12 | name: "SCNLineTests", 13 | dependencies: ["SCNLine"]) 14 | ], 15 | swiftLanguageVersions: [.v5] 16 | ) 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SCNLine 2 | 3 | Functions and classes for creating thick line geometries in a application using SceneKit. 4 | 5 | [![Actions Status](https://github.com/maxxfrazer/SceneKit-SCNLine/workflows/linter/badge.svg)](https://github.com/maxxfrazer/SceneKit-SCNLine/actions) 6 | [![Actions Status](https://github.com/maxxfrazer/SceneKit-SCNLine/workflows/build/badge.svg)](https://github.com/maxxfrazer/SceneKit-SCNLine/actions) 7 | [![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg?style=flat)](https://swift.org/) 8 | 9 | ![Line Drawing Hello 1](https://github.com/maxxfrazer/SceneKit-SCNLine/blob/master/media/lines-hello-lighton.gif) 10 | 11 | ## Introduction 12 | 13 | SCNLine is a class for drawing lines of a given thickness in 3D space. 14 | In most situations in 3D projects the typically used method would use [GL_LINES](https://www.glprogramming.com/red/chapter02.html#name14); which can be exercised in SceneKit with [this primitive type](https://developer.apple.com/documentation/scenekit/scngeometryprimitivetype/line). However [glLineWidth](https://developer.apple.com/documentation/opengles/1617268-gllinewidth?language=occ) is now depricated, which is initially why I made this class. 15 | 16 | For more information on drawing primitive types in OpenGL or otherwise, please refer to [this document](http://15462.courses.cs.cmu.edu/spring2018content/lectures/00_opengl/00_opengl_slides.pdf), or if you want to see how it's applied in SceneKit, check out my [first Medium article about building primitive geometries](https://link.medium.com/umwbtn8afT). 17 | 18 | Please feel free to use and contribute this library however you like. 19 | I only ask that you let me know when you're doing so; that way I can see some cool uses of it! 20 | 21 | ## Installation 22 | 23 | ### Swift Package Manager 24 | 25 | Add the URL of this repository to your Xcode 11+ Project. 26 | 27 | `https://github.com/maxxfrazer/SceneKit-SCNLine.git` 28 | 29 | ### CocoaPods 30 | 31 | Add to Podfile: `pod 'SCNLine', '~> 1.0'` 32 | 33 | 34 | ### Import 35 | 36 | Add to .swift file: `import SCNLine` 37 | 38 | ## Example 39 | 40 | It's as easy as this to a line geometry: 41 | 42 | ``` 43 | let lineGeometry = SCNGeometry.line(points: [ 44 | SCNVector3(0,-1,0), 45 | SCNVector3(0,-1,-1), 46 | SCNVector3(1,-1,-1) 47 | ], radius: 0.1).0 48 | ``` 49 | 50 | Or using the node directly SCNLineNode: 51 | ``` 52 | drawingNode = SCNLineNode( 53 | with: [SCNVector3(0,-1,0), SCNVector3(0,-1,-1), SCNVector3(1,-1,-1)], 54 | radius: 0.01, 55 | edges: 12, 56 | maxTurning: 12 57 | ) 58 | drawingNode.add(point: SCNVector3(1,-2,-2)) 59 | ``` 60 | The latter is recommended if you want to update the line at a later time by adding a point to it. 61 | 62 | This will draw a line of radius 10cm from below the origin, forwards and then to the right in an ARKit setup. 63 | The y value is set to -1 just as an example that assumes the origin of your scene graph is about 1m above the ground. 64 | 65 | Other parameters that can be passed into SCNGeometry.path: 66 | 67 | | name | description | default | example | 68 | |---------------|---------------------------------------------------------------------------------|--------------------|---------------------------------| 69 | | points | Array of SCNVector3 points to make up the line | no default | [SCNVector3(0,-1,0), SCNVector3(0,-1,-1), SCNVector3(1,-1,-1)] | 70 | | radius | Radius of the line in meters | no default | 0.1 | 71 | | edges | The number of vertices used around the edge of the tube shaped line to be exteneded throughout the geometry. | 12, minimum is 3 | 16 | 72 | | maxTurning | Maximum number of points to make up the curved look when the line is turning to a new direction. | 4 | 16 | 73 | 74 | 75 | While in the examples below it shows that this can be used for drawing apps I would not recommend this class for that in its current state, because the current class regathers vertices from the very beginning of the line right to the end, which is very inefficient as most will remain the same. 76 | 77 | Here's some basic examples of what you can do with this Pod: 78 | 79 | ![Line Drawing Example 1](https://github.com/maxxfrazer/SceneKit-SCNLine/blob/master/media/lines-drawing-1.gif) 80 | ![Line Drawing Hello 2](https://github.com/maxxfrazer/SceneKit-SCNLine/blob/master/media/lines-hello-lightoff.gif) 81 | -------------------------------------------------------------------------------- /SCNLine.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 3 | s.name = "SCNLine" 4 | s.version = ENV['LIB_VERSION'] || "1.3.0" 5 | s.summary = "SCNLine lets you draw tubes." 6 | s.description = <<-DESC 7 | draw a thick line in SceneKit 8 | DESC 9 | s.homepage = "https://github.com/maxxfrazer/SCNLine" 10 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 11 | s.license = "MIT" 12 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 13 | s.author = "Max Cobb" 14 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 15 | s.source = { :git => "https://github.com/maxxfrazer/SceneKit-SCNLine.git", :tag => "#{s.version}" } 16 | s.swift_version = '5.0' 17 | s.ios.deployment_target = '9.0' 18 | s.osx.deployment_target = "11" 19 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 20 | s.source_files = "Sources/SCNLine/*.swift" 21 | end 22 | -------------------------------------------------------------------------------- /SCNLine.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 007C2DD721C45A2600673994 /* SCNLine.h in Headers */ = {isa = PBXBuildFile; fileRef = 007C2DD521C45A2600673994 /* SCNLine.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 007C2DDE21C45A3B00673994 /* SCNGeometry+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 007C2DDD21C45A3B00673994 /* SCNGeometry+Extensions.swift */; }; 12 | 007C2DE021C45A6A00673994 /* SCNVector3+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 007C2DDF21C45A6A00673994 /* SCNVector3+Extensions.swift */; }; 13 | 00A42F4921F94F03004C856A /* SCNLineNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A42F4821F94F03004C856A /* SCNLineNode.swift */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 007C2DD221C45A2600673994 /* SCNLine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SCNLine.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 18 | 007C2DD521C45A2600673994 /* SCNLine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCNLine.h; sourceTree = ""; }; 19 | 007C2DD621C45A2600673994 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 20 | 007C2DDD21C45A3B00673994 /* SCNGeometry+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SCNGeometry+Extensions.swift"; sourceTree = ""; }; 21 | 007C2DDF21C45A6A00673994 /* SCNVector3+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SCNVector3+Extensions.swift"; sourceTree = ""; }; 22 | 00A42F4821F94F03004C856A /* SCNLineNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCNLineNode.swift; sourceTree = ""; }; 23 | /* End PBXFileReference section */ 24 | 25 | /* Begin PBXFrameworksBuildPhase section */ 26 | 007C2DCF21C45A2600673994 /* Frameworks */ = { 27 | isa = PBXFrameworksBuildPhase; 28 | buildActionMask = 2147483647; 29 | files = ( 30 | ); 31 | runOnlyForDeploymentPostprocessing = 0; 32 | }; 33 | /* End PBXFrameworksBuildPhase section */ 34 | 35 | /* Begin PBXGroup section */ 36 | 007C2DC821C45A2600673994 = { 37 | isa = PBXGroup; 38 | children = ( 39 | 007C2DD421C45A2600673994 /* SCNLine */, 40 | 007C2DD321C45A2600673994 /* Products */, 41 | ); 42 | sourceTree = ""; 43 | }; 44 | 007C2DD321C45A2600673994 /* Products */ = { 45 | isa = PBXGroup; 46 | children = ( 47 | 007C2DD221C45A2600673994 /* SCNLine.framework */, 48 | ); 49 | name = Products; 50 | sourceTree = ""; 51 | }; 52 | 007C2DD421C45A2600673994 /* SCNLine */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 007C2DD521C45A2600673994 /* SCNLine.h */, 56 | 007C2DD621C45A2600673994 /* Info.plist */, 57 | 007C2DDD21C45A3B00673994 /* SCNGeometry+Extensions.swift */, 58 | 00A42F4821F94F03004C856A /* SCNLineNode.swift */, 59 | 007C2DDF21C45A6A00673994 /* SCNVector3+Extensions.swift */, 60 | ); 61 | path = SCNLine; 62 | sourceTree = ""; 63 | }; 64 | /* End PBXGroup section */ 65 | 66 | /* Begin PBXHeadersBuildPhase section */ 67 | 007C2DCD21C45A2600673994 /* Headers */ = { 68 | isa = PBXHeadersBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | 007C2DD721C45A2600673994 /* SCNLine.h in Headers */, 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | /* End PBXHeadersBuildPhase section */ 76 | 77 | /* Begin PBXNativeTarget section */ 78 | 007C2DD121C45A2600673994 /* SCNLine */ = { 79 | isa = PBXNativeTarget; 80 | buildConfigurationList = 007C2DDA21C45A2600673994 /* Build configuration list for PBXNativeTarget "SCNLine" */; 81 | buildPhases = ( 82 | 007C2DCD21C45A2600673994 /* Headers */, 83 | 007C2DCE21C45A2600673994 /* Sources */, 84 | 007C2DCF21C45A2600673994 /* Frameworks */, 85 | 007C2DD021C45A2600673994 /* Resources */, 86 | 00614398225159C7005ED3D7 /* SwiftLint */, 87 | ); 88 | buildRules = ( 89 | ); 90 | dependencies = ( 91 | ); 92 | name = SCNLine; 93 | productName = SCNLine; 94 | productReference = 007C2DD221C45A2600673994 /* SCNLine.framework */; 95 | productType = "com.apple.product-type.framework"; 96 | }; 97 | /* End PBXNativeTarget section */ 98 | 99 | /* Begin PBXProject section */ 100 | 007C2DC921C45A2600673994 /* Project object */ = { 101 | isa = PBXProject; 102 | attributes = { 103 | LastUpgradeCheck = 1010; 104 | ORGANIZATIONNAME = "Max Cobb"; 105 | TargetAttributes = { 106 | 007C2DD121C45A2600673994 = { 107 | CreatedOnToolsVersion = 10.1; 108 | LastSwiftMigration = 1020; 109 | }; 110 | }; 111 | }; 112 | buildConfigurationList = 007C2DCC21C45A2600673994 /* Build configuration list for PBXProject "SCNLine" */; 113 | compatibilityVersion = "Xcode 9.3"; 114 | developmentRegion = en; 115 | hasScannedForEncodings = 0; 116 | knownRegions = ( 117 | en, 118 | Base, 119 | ); 120 | mainGroup = 007C2DC821C45A2600673994; 121 | productRefGroup = 007C2DD321C45A2600673994 /* Products */; 122 | projectDirPath = ""; 123 | projectRoot = ""; 124 | targets = ( 125 | 007C2DD121C45A2600673994 /* SCNLine */, 126 | ); 127 | }; 128 | /* End PBXProject section */ 129 | 130 | /* Begin PBXResourcesBuildPhase section */ 131 | 007C2DD021C45A2600673994 /* Resources */ = { 132 | isa = PBXResourcesBuildPhase; 133 | buildActionMask = 2147483647; 134 | files = ( 135 | ); 136 | runOnlyForDeploymentPostprocessing = 0; 137 | }; 138 | /* End PBXResourcesBuildPhase section */ 139 | 140 | /* Begin PBXShellScriptBuildPhase section */ 141 | 00614398225159C7005ED3D7 /* SwiftLint */ = { 142 | isa = PBXShellScriptBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | ); 146 | inputFileListPaths = ( 147 | ); 148 | inputPaths = ( 149 | ); 150 | name = SwiftLint; 151 | outputFileListPaths = ( 152 | ); 153 | outputPaths = ( 154 | ); 155 | runOnlyForDeploymentPostprocessing = 0; 156 | shellPath = /bin/sh; 157 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint autocorrect && swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 158 | }; 159 | /* End PBXShellScriptBuildPhase section */ 160 | 161 | /* Begin PBXSourcesBuildPhase section */ 162 | 007C2DCE21C45A2600673994 /* Sources */ = { 163 | isa = PBXSourcesBuildPhase; 164 | buildActionMask = 2147483647; 165 | files = ( 166 | 007C2DDE21C45A3B00673994 /* SCNGeometry+Extensions.swift in Sources */, 167 | 007C2DE021C45A6A00673994 /* SCNVector3+Extensions.swift in Sources */, 168 | 00A42F4921F94F03004C856A /* SCNLineNode.swift in Sources */, 169 | ); 170 | runOnlyForDeploymentPostprocessing = 0; 171 | }; 172 | /* End PBXSourcesBuildPhase section */ 173 | 174 | /* Begin XCBuildConfiguration section */ 175 | 007C2DD821C45A2600673994 /* Debug */ = { 176 | isa = XCBuildConfiguration; 177 | buildSettings = { 178 | ALWAYS_SEARCH_USER_PATHS = NO; 179 | CLANG_ANALYZER_NONNULL = YES; 180 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 181 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 182 | CLANG_CXX_LIBRARY = "libc++"; 183 | CLANG_ENABLE_MODULES = YES; 184 | CLANG_ENABLE_OBJC_ARC = YES; 185 | CLANG_ENABLE_OBJC_WEAK = YES; 186 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 187 | CLANG_WARN_BOOL_CONVERSION = YES; 188 | CLANG_WARN_COMMA = YES; 189 | CLANG_WARN_CONSTANT_CONVERSION = YES; 190 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 191 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 192 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 193 | CLANG_WARN_EMPTY_BODY = YES; 194 | CLANG_WARN_ENUM_CONVERSION = YES; 195 | CLANG_WARN_INFINITE_RECURSION = YES; 196 | CLANG_WARN_INT_CONVERSION = YES; 197 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 198 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 199 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 200 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 201 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 202 | CLANG_WARN_STRICT_PROTOTYPES = YES; 203 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 204 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 205 | CLANG_WARN_UNREACHABLE_CODE = YES; 206 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 207 | CODE_SIGN_IDENTITY = "iPhone Developer"; 208 | COPY_PHASE_STRIP = NO; 209 | CURRENT_PROJECT_VERSION = 1; 210 | DEBUG_INFORMATION_FORMAT = dwarf; 211 | ENABLE_STRICT_OBJC_MSGSEND = YES; 212 | ENABLE_TESTABILITY = YES; 213 | GCC_C_LANGUAGE_STANDARD = gnu11; 214 | GCC_DYNAMIC_NO_PIC = NO; 215 | GCC_NO_COMMON_BLOCKS = YES; 216 | GCC_OPTIMIZATION_LEVEL = 0; 217 | GCC_PREPROCESSOR_DEFINITIONS = ( 218 | "DEBUG=1", 219 | "$(inherited)", 220 | ); 221 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 222 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 223 | GCC_WARN_UNDECLARED_SELECTOR = YES; 224 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 225 | GCC_WARN_UNUSED_FUNCTION = YES; 226 | GCC_WARN_UNUSED_VARIABLE = YES; 227 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 228 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 229 | MTL_FAST_MATH = YES; 230 | ONLY_ACTIVE_ARCH = YES; 231 | SDKROOT = iphoneos; 232 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 233 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 234 | VERSIONING_SYSTEM = "apple-generic"; 235 | VERSION_INFO_PREFIX = ""; 236 | }; 237 | name = Debug; 238 | }; 239 | 007C2DD921C45A2600673994 /* Release */ = { 240 | isa = XCBuildConfiguration; 241 | buildSettings = { 242 | ALWAYS_SEARCH_USER_PATHS = NO; 243 | CLANG_ANALYZER_NONNULL = YES; 244 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 245 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 246 | CLANG_CXX_LIBRARY = "libc++"; 247 | CLANG_ENABLE_MODULES = YES; 248 | CLANG_ENABLE_OBJC_ARC = YES; 249 | CLANG_ENABLE_OBJC_WEAK = YES; 250 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 251 | CLANG_WARN_BOOL_CONVERSION = YES; 252 | CLANG_WARN_COMMA = YES; 253 | CLANG_WARN_CONSTANT_CONVERSION = YES; 254 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 255 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 256 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 257 | CLANG_WARN_EMPTY_BODY = YES; 258 | CLANG_WARN_ENUM_CONVERSION = YES; 259 | CLANG_WARN_INFINITE_RECURSION = YES; 260 | CLANG_WARN_INT_CONVERSION = YES; 261 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 263 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 264 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 265 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 266 | CLANG_WARN_STRICT_PROTOTYPES = YES; 267 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 268 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 269 | CLANG_WARN_UNREACHABLE_CODE = YES; 270 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 271 | CODE_SIGN_IDENTITY = "iPhone Developer"; 272 | COPY_PHASE_STRIP = NO; 273 | CURRENT_PROJECT_VERSION = 1; 274 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 275 | ENABLE_NS_ASSERTIONS = NO; 276 | ENABLE_STRICT_OBJC_MSGSEND = YES; 277 | GCC_C_LANGUAGE_STANDARD = gnu11; 278 | GCC_NO_COMMON_BLOCKS = YES; 279 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 280 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 281 | GCC_WARN_UNDECLARED_SELECTOR = YES; 282 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 283 | GCC_WARN_UNUSED_FUNCTION = YES; 284 | GCC_WARN_UNUSED_VARIABLE = YES; 285 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 286 | MTL_ENABLE_DEBUG_INFO = NO; 287 | MTL_FAST_MATH = YES; 288 | SDKROOT = iphoneos; 289 | SWIFT_COMPILATION_MODE = wholemodule; 290 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 291 | VALIDATE_PRODUCT = YES; 292 | VERSIONING_SYSTEM = "apple-generic"; 293 | VERSION_INFO_PREFIX = ""; 294 | }; 295 | name = Release; 296 | }; 297 | 007C2DDB21C45A2600673994 /* Debug */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | CLANG_ENABLE_MODULES = YES; 301 | CODE_SIGN_IDENTITY = ""; 302 | CODE_SIGN_STYLE = Automatic; 303 | DEFINES_MODULE = YES; 304 | DEVELOPMENT_TEAM = N2RTJDRM64; 305 | DYLIB_COMPATIBILITY_VERSION = 1; 306 | DYLIB_CURRENT_VERSION = 1; 307 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 308 | INFOPLIST_FILE = SCNLine/Info.plist; 309 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 310 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 311 | LD_RUNPATH_SEARCH_PATHS = ( 312 | "$(inherited)", 313 | "@executable_path/Frameworks", 314 | "@loader_path/Frameworks", 315 | ); 316 | PRODUCT_BUNDLE_IDENTIFIER = com.maxxfrazer.SCNLine; 317 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 318 | SKIP_INSTALL = YES; 319 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 320 | SWIFT_VERSION = 5.0; 321 | TARGETED_DEVICE_FAMILY = "1,2"; 322 | }; 323 | name = Debug; 324 | }; 325 | 007C2DDC21C45A2600673994 /* Release */ = { 326 | isa = XCBuildConfiguration; 327 | buildSettings = { 328 | CLANG_ENABLE_MODULES = YES; 329 | CODE_SIGN_IDENTITY = ""; 330 | CODE_SIGN_STYLE = Automatic; 331 | DEFINES_MODULE = YES; 332 | DEVELOPMENT_TEAM = N2RTJDRM64; 333 | DYLIB_COMPATIBILITY_VERSION = 1; 334 | DYLIB_CURRENT_VERSION = 1; 335 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 336 | INFOPLIST_FILE = SCNLine/Info.plist; 337 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 338 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 339 | LD_RUNPATH_SEARCH_PATHS = ( 340 | "$(inherited)", 341 | "@executable_path/Frameworks", 342 | "@loader_path/Frameworks", 343 | ); 344 | PRODUCT_BUNDLE_IDENTIFIER = com.maxxfrazer.SCNLine; 345 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 346 | SKIP_INSTALL = YES; 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 | 007C2DCC21C45A2600673994 /* Build configuration list for PBXProject "SCNLine" */ = { 356 | isa = XCConfigurationList; 357 | buildConfigurations = ( 358 | 007C2DD821C45A2600673994 /* Debug */, 359 | 007C2DD921C45A2600673994 /* Release */, 360 | ); 361 | defaultConfigurationIsVisible = 0; 362 | defaultConfigurationName = Release; 363 | }; 364 | 007C2DDA21C45A2600673994 /* Build configuration list for PBXNativeTarget "SCNLine" */ = { 365 | isa = XCConfigurationList; 366 | buildConfigurations = ( 367 | 007C2DDB21C45A2600673994 /* Debug */, 368 | 007C2DDC21C45A2600673994 /* Release */, 369 | ); 370 | defaultConfigurationIsVisible = 0; 371 | defaultConfigurationName = Release; 372 | }; 373 | /* End XCConfigurationList section */ 374 | }; 375 | rootObject = 007C2DC921C45A2600673994 /* Project object */; 376 | } 377 | -------------------------------------------------------------------------------- /SCNLine.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SCNLine.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/SCNLine/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/SCNLine/SCNGeometry+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SCNGeometry+Extensions.swift 3 | // SCNLine 4 | // 5 | // Created by Max Cobb on 12/14/18. 6 | // Copyright © 2018 Max Cobb. All rights reserved. 7 | // 8 | 9 | import SceneKit 10 | 11 | public struct GeometryParts { 12 | public var vertices: [SCNVector3] 13 | public var normals: [SCNVector3] 14 | public var uvs: [CGPoint] 15 | public var indices: [UInt32] 16 | func buildGeometry() -> SCNGeometry { 17 | let src = SCNGeometrySource(vertices: self.vertices) 18 | let normals = SCNGeometrySource(normals: self.normals) 19 | let textureMap = SCNGeometrySource(textureCoordinates: self.uvs) 20 | let inds = SCNGeometryElement(indices: self.indices, primitiveType: .triangles) 21 | return SCNGeometry(sources: [src, normals, textureMap], elements: [inds]) 22 | } 23 | } 24 | 25 | private extension simd_quatf { 26 | func act(_ vector: SCNVector3) -> SCNVector3 { 27 | let vec = self.act(SIMD3([vector.fx, vector.fy, vector.fz])) 28 | return SCNVector3(UFloat(vec.x), UFloat(vec.y), UFloat(vec.z)) // Use UFloats 29 | } 30 | func split(by factor: Float = 2) -> simd_quatf { 31 | if self.angle == 0 { 32 | return self 33 | } else { 34 | return simd_quatf(angle: self.angle / factor, axis: self.axis) 35 | } 36 | } 37 | static func zero() -> simd_quatf { 38 | return simd_quatf(angle: 0, axis: [1, 0, 0]) 39 | } 40 | } 41 | private func rotationBetween2Vectors(start: SCNVector3, end: SCNVector3) -> simd_quatf { 42 | return simd_quaternion( 43 | simd_float3([start.fx, start.fy, start.fz]), 44 | simd_float3([end.fx, end.fy, end.fz]) 45 | ) // Uses Float variables instead of the CGFloats on macOS. 46 | } 47 | public extension SCNGeometry { 48 | 49 | private static func getCircularPoints( 50 | radius: Float, edges: Int, 51 | orientation: simd_quatf = simd_quatf(angle: 0, axis: SIMD3([1, 0, 0])) 52 | ) -> [SCNVector3] { 53 | var angle: Float = 0 54 | var verts = [SCNVector3]() 55 | let angleAdd = Float.pi * 2 / Float(edges) 56 | for index in 0.. 0 { 61 | verts.append(verts.last!) 62 | } 63 | } 64 | verts.append(verts.first!) 65 | return verts 66 | } 67 | 68 | /// Create a thick line following a series of points in 3D space 69 | /// 70 | /// - Parameters: 71 | /// - points: Points that the tube will follow through 72 | /// - radius: Radius of the line or tube 73 | /// - edges: Number of edges the extended shape should have, recommend at least 3 74 | /// - maxTurning: Maximum number of additional points to be added on turns. Varies depending on degree change. 75 | /// - Returns: Returns a tuple of the geometry and a CGFloat containing the 76 | /// distance of the entire tube, including added turns. 77 | static func line( 78 | points: [SCNVector3], radius: Float, edges: Int = 12, 79 | maxTurning: Int = 4 80 | ) -> (SCNGeometry, CGFloat) { 81 | 82 | let (geomParts, lineLength) = SCNGeometry.getAllLineParts( 83 | points: points, radius: radius, 84 | edges: edges, maxTurning: maxTurning 85 | ) 86 | if geomParts.vertices.isEmpty { 87 | return (SCNGeometry(sources: [], elements: []), lineLength) 88 | } 89 | return (geomParts.buildGeometry(), lineLength) 90 | } 91 | 92 | static func buildGeometry( 93 | vertices: [SCNVector3], normals: [SCNVector3], 94 | uv: [CGPoint], indices: [UInt32] 95 | ) -> SCNGeometry { 96 | let src = SCNGeometrySource(vertices: vertices) 97 | let normals = SCNGeometrySource(normals: normals) 98 | let textureMap = SCNGeometrySource(textureCoordinates: uv) 99 | let inds = SCNGeometryElement(indices: indices, primitiveType: .triangles) 100 | 101 | return SCNGeometry(sources: [src, normals, textureMap], elements: [inds]) 102 | } 103 | 104 | /// This function takes in all the geometry parameters to get the vertices, normals etc 105 | /// It's currently grossly long, needs cleaning up as a priority. 106 | /// 107 | /// - Parameters: 108 | /// - points: points for the line to be created 109 | /// - radius: radius of the line 110 | /// - edges: edges around each point 111 | /// - maxTurning: the maximum number of points to build up a turn 112 | /// - Returns: All the bits to create the geometry from and the length of the result 113 | static func getAllLineParts( 114 | points: [SCNVector3], radius: Float, edges: Int = 12, 115 | maxTurning: Int = 4 116 | ) -> (GeometryParts, CGFloat) { 117 | guard points.count >= 2, var lastLocation = points.first else { 118 | return (GeometryParts(vertices: [], normals: [], uvs: [], indices: []), 0) 119 | } 120 | var trueNormals = [SCNVector3]() 121 | var trueUVMap = [CGPoint]() 122 | var trueVs = [SCNVector3]() 123 | var trueInds = [UInt32]() 124 | 125 | var lastForward = SCNVector3(0, 1, 0) 126 | var cPoints = SCNGeometry.getCircularPoints(radius: radius, edges: edges) 127 | let textureXs = cPoints.enumerated().map { CGFloat($0.offset) / CGFloat(edges - 1) } 128 | 129 | var lineLength: CGFloat = 0 130 | for (index, point) in points.enumerated() { 131 | let newRotation: simd_quatf 132 | 133 | if index == 0 { 134 | let startDirection = (points[index + 1] - point).normalized() 135 | let initialRotation = rotationBetween2Vectors(start: lastForward, end: startDirection) 136 | cPoints = SCNGeometry.getCircularPoints(radius: radius, edges: edges, orientation: initialRotation) 137 | lastForward = startDirection.normalized() 138 | newRotation = simd_quatf.zero() 139 | } else if index < points.count - 1 { 140 | trueVs.append(contentsOf: Array(trueVs.suffix(edges * 2))) 141 | trueUVMap.append(contentsOf: Array(trueUVMap.suffix(edges * 2))) 142 | trueNormals.append(contentsOf: cPoints.map { $0.normalized() }) 143 | 144 | let direction = (points[index + 1] - points[index]).normalized() 145 | newRotation = rotationBetween2Vectors(start: lastForward, end: direction) 146 | } else { 147 | newRotation = simd_quatf(angle: 0, axis: SIMD3([1, 0, 0])) 148 | } 149 | 150 | if index > 0 { 151 | let halfRotation = newRotation.split(by: 2) 152 | if point.distance(vector: points[index - 1]) > radius * 2 { 153 | let mTurn = max(1, min(newRotation.angle / .pi, 1) * Float(maxTurning)) 154 | 155 | if mTurn > 1 { 156 | let partRotation = newRotation.split(by: Float(mTurn)) 157 | let halfForward = newRotation.split(by: 2).act(lastForward) 158 | 159 | for i in 0.. 10 | 11 | //! Project version number for SCNLine. 12 | FOUNDATION_EXPORT double SCNLineVersionNumber; 13 | 14 | //! Project version string for SCNLine. 15 | FOUNDATION_EXPORT const unsigned char SCNLineVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/SCNLine/SCNLineNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SCNLineNode.swift 3 | // SCNLine 4 | // 5 | // Created by Max Cobb on 1/23/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | import SceneKit 10 | 11 | public class SCNLineNode: SCNNode { 12 | private var vertices = [SCNVector3]() 13 | public private(set) var length: CGFloat = 0 14 | public private(set) var points: [SCNVector3] { 15 | didSet { 16 | self.update() 17 | } 18 | } 19 | public var radius: Float { 20 | didSet { 21 | self.update() 22 | } 23 | } 24 | public var edges: Int { 25 | didSet { 26 | self.update() 27 | } 28 | } 29 | public var lineMaterials = [SCNMaterial()] { 30 | didSet { 31 | // Only updating the materials, to avoid rebuilding the geometry. 32 | self.geometry?.materials = lineMaterials 33 | } 34 | } 35 | public var maxTurning: Int { 36 | didSet { 37 | self.update() 38 | } 39 | } 40 | public private(set) var gParts: GeometryParts? 41 | 42 | /// Initialiser for a SCNLineNode 43 | /// 44 | /// - Parameters: 45 | /// - points: array of points to be joined up to form the line 46 | /// - radius: radius of the line 47 | /// - edges: number of edges around the line/tube at every point 48 | /// - maxTurning: multiplier to dictate how smooth the turns should be 49 | public init(with points: [SCNVector3] = [], radius: Float = 1, edges: Int = 12, maxTurning: Int = 4) { 50 | self.points = points 51 | self.radius = radius 52 | self.edges = edges 53 | self.maxTurning = maxTurning 54 | super.init() 55 | if !points.isEmpty { 56 | let (geomParts, len) = SCNGeometry.getAllLineParts( 57 | points: points, radius: radius, 58 | edges: edges, maxTurning: maxTurning 59 | ) 60 | self.gParts = geomParts 61 | self.geometry = geomParts.buildGeometry() 62 | self.length = len 63 | } 64 | } 65 | 66 | /// Add a point to the collection for this SCNLineNode 67 | /// 68 | /// - Parameter point: point to be added to the line 69 | public func add(point: SCNVector3) { 70 | // TODO: optimise this function to not recalculate all points 71 | self.add(points: [point]) 72 | } 73 | 74 | /// Add a point to the collection for this SCNLineNode 75 | /// 76 | /// - Parameter points: points to be added to the line 77 | public func add(points: [SCNVector3]) { 78 | // TODO: optimise this function to not recalculate all points 79 | self.points.append(contentsOf: points) 80 | } 81 | 82 | public required init?(coder aDecoder: NSCoder) { 83 | fatalError("init(coder:) has not been implemented") 84 | } 85 | 86 | private func update() { 87 | if points.count > 1 { 88 | let (geomParts, len) = SCNGeometry.getAllLineParts( 89 | points: points, radius: radius, 90 | edges: edges, maxTurning: maxTurning 91 | ) 92 | self.gParts = geomParts 93 | self.geometry = geomParts.buildGeometry() 94 | self.geometry?.materials = self.lineMaterials 95 | self.length = len 96 | } else { 97 | self.geometry = nil 98 | self.length = 0 99 | } 100 | } 101 | 102 | private func getlastAverages() -> SCNVector3 { 103 | let len = self.gParts!.vertices.count - 1 104 | let lastPoints = self.gParts?.vertices[(len - self.edges * 4)...(len - self.edges * 2)] 105 | let avg = lastPoints!.reduce(SCNVector3Zero, { (total, npoint) -> SCNVector3 in 106 | return total + npoint 107 | }) / Float(self.edges * 2) 108 | return avg 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/SCNLine/SCNVector3+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SCNVector3+Extensions.swift 3 | // SCNPath 4 | // 5 | // Created by Max Cobb on 12/10/18. 6 | // Copyright © 2018 Max Cobb. All rights reserved. 7 | // 8 | 9 | import SceneKit 10 | 11 | // macOS uses CGFloats for describing SCNvector3. 12 | // In order to shae the same code, this typealias allows for conversion between Floats and CGFloats when needed 13 | #if os(macOS) 14 | typealias UFloat = CGFloat 15 | #elseif os(iOS) || os(visionOS) 16 | typealias UFloat = Float 17 | #elseif os(watchOS) 18 | typealias UFloat = Float 19 | #endif 20 | 21 | internal extension SCNVector3 { 22 | 23 | // Universal type for macOS and iOS allowing simultaneous compatiblity when doing operations. 24 | // (Remember x, y and z are CGFloats on macOS) 25 | var fx: Float {Float(x)} 26 | var fy: Float {Float(y)} 27 | var fz: Float {Float(z)} 28 | 29 | /** 30 | * Returns the length (magnitude) of the vector described by the SCNVector3 31 | */ 32 | var length: Float { 33 | return sqrtf(self.lenSq) 34 | } 35 | 36 | func angleChange(to: SCNVector3) -> Float { 37 | let dot = self.normalized().dot(vector: to.normalized()) 38 | return acos(dot / sqrt(self.lenSq * to.lenSq)) 39 | } 40 | 41 | /** 42 | * Returns the squared length (magnitude) of the vector described by the SCNVector3 43 | */ 44 | var lenSq: Float { 45 | return fx*fx + fy*fy + fz*fz 46 | } 47 | 48 | /** 49 | * Normalizes the vector described by the SCNVector3 to length 1.0 and returns 50 | * the result as a new SCNVector3. 51 | */ 52 | func normalized() -> SCNVector3 { 53 | return self / self.length 54 | } 55 | 56 | /** 57 | * Calculates the distance between two SCNVector3. Pythagoras! 58 | */ 59 | func distance(vector: SCNVector3) -> Float { 60 | return (self - vector).length 61 | } 62 | 63 | /** 64 | * Calculates the dot product between two SCNVector3. 65 | */ 66 | func dot(vector: SCNVector3) -> Float { 67 | return fx * vector.fx + fy * vector.fy + fz * vector.fz 68 | } 69 | 70 | /** 71 | * Calculates the cross product between two SCNVector3. 72 | */ 73 | func cross(vector: SCNVector3) -> SCNVector3 { 74 | return SCNVector3( 75 | y * vector.z - z * vector.y, 76 | z * vector.x - x * vector.z, 77 | x * vector.y - y * vector.x 78 | ) 79 | } 80 | 81 | func flattened() -> SCNVector3 { 82 | return SCNVector3(self.x, 0, self.z) 83 | } 84 | 85 | /// Given a point and origin, rotate along X/Z plane by radian amount 86 | /// 87 | /// - parameter origin: Origin for the start point to be rotated about 88 | /// - parameter by: Value in radians for the point to be rotated by 89 | /// 90 | /// - returns: New SCNVector3 that has the rotation applied 91 | func rotate(about origin: SCNVector3, by: Float) -> SCNVector3 { 92 | let pointRepositionedXY = [self.fx - origin.fx, self.fz - origin.fz] 93 | let sinAngle = sin(by) 94 | let cosAngle = cos(by) 95 | return SCNVector3( 96 | x: UFloat(pointRepositionedXY[0] * cosAngle - pointRepositionedXY[1] * sinAngle + origin.fx), 97 | y: self.y, 98 | z: UFloat(pointRepositionedXY[0] * sinAngle + pointRepositionedXY[1] * cosAngle + origin.fz) 99 | ) 100 | } 101 | } 102 | 103 | /** 104 | * Adds two SCNVector3 vectors and returns the result as a new SCNVector3. 105 | */ 106 | internal func + (left: SCNVector3, right: SCNVector3) -> SCNVector3 { 107 | return SCNVector3Make(left.x + right.x, left.y + right.y, left.z + right.z) 108 | } 109 | internal func + (left: CGPoint, right: CGPoint) -> CGPoint { 110 | return CGPoint(x: left.x + right.x, y: left.y + right.y) 111 | } 112 | 113 | /** 114 | * Subtracts two SCNVector3 vectors and returns the result as a new SCNVector3. 115 | */ 116 | internal func - (left: SCNVector3, right: SCNVector3) -> SCNVector3 { 117 | return SCNVector3Make(left.x - right.x, left.y - right.y, left.z - right.z) 118 | } 119 | 120 | /** 121 | * Multiplies the x, y and z fields of a SCNVector3 with the same scalar value and 122 | * returns the result as a new SCNVector3. 123 | */ 124 | internal func * (vector: SCNVector3, scalar: Float) -> SCNVector3 { 125 | return SCNVector3Make(UFloat(vector.fx * scalar), UFloat(vector.fy * scalar), UFloat(vector.fz * scalar)) 126 | } 127 | 128 | /** 129 | * Multiplies the x and y fields of a SCNVector3 with the same scalar value. 130 | */ 131 | internal func *= (vector: inout SCNVector3, scalar: Float) { 132 | vector = (vector * scalar) 133 | } 134 | 135 | /** 136 | * Divides two SCNVector3 vectors abd returns the result as a new SCNVector3 137 | */ 138 | internal func / (left: SCNVector3, right: SCNVector3) -> SCNVector3 { 139 | return SCNVector3Make(left.x / right.x, left.y / right.y, left.z / right.z) 140 | } 141 | 142 | /** 143 | * Divides the x, y and z fields of a SCNVector3 by the same scalar value and 144 | * returns the result as a new SCNVector3. 145 | */ 146 | internal func / (vector: SCNVector3, scalar: Float) -> SCNVector3 { 147 | return SCNVector3Make(UFloat(vector.fx / scalar), UFloat(vector.fy / scalar), UFloat(vector.fz / scalar)) 148 | } 149 | -------------------------------------------------------------------------------- /Tests/SCNLineTests/SCNLineTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SceneKit 3 | @testable import SCNLine 4 | 5 | final class SCNLineTests: XCTestCase { 6 | func testGetAllLineParts() { 7 | // Setup test inputs 8 | let radius: Float = 1.0 9 | let edges: Int = 12 10 | let maxTurning: Int = 4 11 | 12 | let points: [SCNVector3] = [ 13 | SCNVector3(x: 0.0, y: 0.0, z: 0.0), 14 | SCNVector3(x: 1.0, y: 1.0, z: 1.0), 15 | SCNVector3(x: 2.0, y: 2.0, z: 2.0) 16 | ] 17 | 18 | let (geometryParts, length) = SCNGeometry.getAllLineParts( 19 | points: points, radius: radius, edges: edges, maxTurning: maxTurning 20 | ) 21 | 22 | // Test the output 23 | XCTAssertEqual(geometryParts.vertices.count, 96) 24 | XCTAssertEqual(geometryParts.normals.count, geometryParts.vertices.count) 25 | XCTAssertEqual(geometryParts.uvs.count, geometryParts.vertices.count) 26 | XCTAssertGreaterThan(length, 3) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /media/lines-drawing-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxxfrazer/SceneKit-SCNLine/2162cf126307d156d2bff987ccc4b6439b288500/media/lines-drawing-1.gif -------------------------------------------------------------------------------- /media/lines-hello-lightoff.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxxfrazer/SceneKit-SCNLine/2162cf126307d156d2bff987ccc4b6439b288500/media/lines-hello-lightoff.gif -------------------------------------------------------------------------------- /media/lines-hello-lighton.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxxfrazer/SceneKit-SCNLine/2162cf126307d156d2bff987ccc4b6439b288500/media/lines-hello-lighton.gif --------------------------------------------------------------------------------